Clés primaires composites

New in Django 5.2.

Avec Django, chaque modèle dispose d’une clé primaire. Par défaut, la clé primaire consiste en un champ unique.

Dans la plupart des cas, une clé primaire sur un seul champ suffira. Toutefois, en conception de bases de données, la définition d’une clé primaire formée de plusieurs champs est parfois nécessaire.

Pour utiliser une clé primaire composite lors de la définition d’un modèle, définissez l’attribut pk à une clé CompositePrimaryKey:

class Product(models.Model):
    name = models.CharField(max_length=100)


class Order(models.Model):
    reference = models.CharField(max_length=20, primary_key=True)


class OrderLineItem(models.Model):
    pk = models.CompositePrimaryKey("product_id", "order_id")
    product = models.ForeignKey(Product, on_delete=models.CASCADE)
    order = models.ForeignKey(Order, on_delete=models.CASCADE)
    quantity = models.IntegerField()

Cela indiquera à Django de créer une clé primaire composite (PRIMARY KEY (product_id, order_id)) lors de la création de la table.

Une clé primaire composite est représentée par un tuple:

>>> product = Product.objects.create(name="apple")
>>> order = Order.objects.create(reference="A755H")
>>> item = OrderLineItem.objects.create(product=product, order=order, quantity=1)
>>> item.pk
(1, "A755H")

Vous pouvez attribuer un tuple à l’attribut pk. Cela va définir les valeurs de champs associés :

>>> item = OrderLineItem(pk=(2, "B142C"))
>>> item.pk
(2, "B142C")
>>> item.product_id
2
>>> item.order_id
"B142C"

Une clé primaire composite peut aussi être filtrée par un tuple:

>>> OrderLineItem.objects.filter(pk=(1, "A755H")).count()
1

Nous somme toujours en cours de travail pour la prise en charge des clés primaires composites pour les champs relationnels, y compris les champs GenericForeignKey et le site d’administration de Django. À ce stade, les modèles avec clé primaire composite ne peuvent pas être inscrits dans l’administration de Django. Nous espérons pouvoir résoudre ces questions dans de prochaines versions.

Migration vers une clé primaire composite

Django ne prend actuellement pas en charge la migration depuis ou vers une clé primaire composite une fois qu’une table a été créée. Il ne gère pas non plus l’ajout ou la suppression de champs faisant partie de la clé primaire composite.

Si vous souhaitez faire migrer une table existante à partir d’une clé primaire unique vers une clé primaire composite, suivez les instructions de votre moteur de base de données pour le faire.

Après que la clé primaire composite est en place, ajoutez le champ CompositePrimaryKey à votre modèle. Cela permet à Django de reconnaître et de traiter correctement la clé primaire composite.

Même si les opérations de migration (par ex. AddField, AlterField) sur des champs de clé primaire ne sont pas prises en charge, makemigrations détecte tout de même les modifications.

Afin d’éviter les erreurs, il est recommandé d’appliquer de telles migrations avec --fake.

Une autre possibilité est d’utiliser SeparateDatabaseAndState pour exécuter les migrations spécifiques au moteur et les migrations générées par Django dans une seule opération.

Les clés primaires composites et les relations

Les champs relationnels ainsi que les relations génériques ne prennent pas en charge les clés primaires composites.

Par exemple, considérant le modèle OrderLineItem, le code suivant n’est pas possible :

class Foo(models.Model):
    item = models.ForeignKey(OrderLineItem, on_delete=models.CASCADE)

Car ForeignKey ne peut actuellement pas faire référence à des modèles avec clé primaire composite.

Pour contourner cette limite, ForeignObject peut être utilisé comme alternative :

class Foo(models.Model):
    item_order_id = models.IntegerField()
    item_product_id = models.CharField(max_length=20)
    item = models.ForeignObject(
        OrderLineItem,
        on_delete=models.CASCADE,
        from_fields=("item_order_id", "item_product_id"),
        to_fields=("order_id", "product_id"),
    )

ForeignObject ressemble beaucoup à ForeignKey, sauf qu’il ne crée pas de colonne (par ex. item_id), de contrainte de clé étrangère ni d’index en base de données.

Avertissement

ForeignObject est une API interne. Cela signifie qu’il n’est pas couvert par notre politique d’obsolescence.

Les clés primaires composites et les fonctions de base de données

Beaucoup de fonctions de base de données n’acceptent qu’une seule expression.

MAX("order_id")  -- OK
MAX("product_id", "order_id")  -- ERROR

Dans ces cas, si on fournit une référence à une clé primaire composite, une erreur ValueError sera produite, car cette clé est composée de plusieurs colonnes. Une exception est admise pour Count.

Max("order_id")  # OK
Max("pk")  # ValueError
Count("pk")  # OK

Les clés primaires composites dans les formulaires

Comme une clé primaire composite est un champ virtuel, c’est-à-dire un champ qui ne représente pas une seule colonne de base de données, ce champ est exclu des formulaires ModelForms.

Par exemple, en prenant l’exemple de ce formulaire

class OrderLineItemForm(forms.ModelForm):
    class Meta:
        model = OrderLineItem
        fields = "__all__"

Ce formulaire ne possède pas de champ de formulaire pk pour la clé primaire composite :

>>> OrderLineItemForm()
<OrderLineItemForm bound=False, valid=Unknown, fields=(product;order;quantity)>

Si on veut définir la clé primaire composite pk comme champ de formulaire, une erreur FieldError de champ inconnu est générée.

Les champs de clé primaire sont en lecture seule

Si vous modifiez la valeur d’une clé primaire d’un objet existant et que vous l’enregistrez, un nouvel objet est créé en parallèle à l’ancien (voir Field.primary_key).

Ceci se vérifie aussi pour les clés primaires composites. Ainsi, vous pouvez définir Field.editable à False pour tous les champs de clé primaire afin de les exclure des formulaires ModelForm.

Les clés primaires composites dans la validation des modèles

Comme pk n’est qu’un champ virtuel, l’inclusion de pk comme nom de champ dans l’argument exclude de Model.clean_fields() n’a aucun effet. Pour exclure les champs de clé primaire composite de la validation des modèles, indiquez chacun des champs concernés individuellement. Model.validate_unique() peut tout de même être appelée avec exclude={"pk"} pour omettre les contrôles d’unicité.

Construction d’applications compatibles avec les clés primaires composites

Avant l’introduction des clés primaires composites, le champ unique composant la clé primaire pouvait être consulté en interrogeant l’attribut primary_key de ses champs :

>>> pk_field = None
>>> for field in Product._meta.get_fields():
...     if field.primary_key:
...         pk_field = field
...         break
...
>>> pk_field
<django.db.models.fields.AutoField: id>

Maintenant qu’une clé primaire peut être composée de plusieurs champs, il n’est plus possible de se fier à l’attribut primary_key pour identifier la composition de la clé primaire, car il vaudra False pour maintenir la règle qu’au plus un seul champ par modèle peut avoir cet attribut à True:

>>> pk_fields = []
>>> for field in OrderLineItem._meta.get_fields():
...     if field.primary_key:
...         pk_fields.append(field)
...
>>> pk_fields
[]

Si l’on veut construire du code applicatif qui sache gérer correctement les clés primaires composites, l’attribut _meta.pk_fields est maintenant disponible :

>>> Product._meta.pk_fields
[<django.db.models.fields.AutoField: id>]
>>> OrderLineItem._meta.pk_fields
[
    <django.db.models.fields.ForeignKey: product>,
    <django.db.models.fields.ForeignKey: order>
]