复合主键

New in Django 5.2.

在Django中,每个模型都有一个公钥。默认情况下,此公钥由一个字段组成。

在大多数情况下,单个公钥就足够了。然而,在数据库设计中,有时需要定义由多个字段组成的公钥。

要使用复合公钥,请在定义模型时设置 pk 属性要成为 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()

这将指示Django创建复合公钥 (PRIMARY KEY (product_id, order_id) )创建表时。

复合公钥由 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")

您可以分配一个 tuplepk 属性这会设置关联的字段值:

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

复合公钥也可以通过 tuple :

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

我们仍在研究复合主要密钥支持 relational fields ,包括 GenericForeignKey 字段和Django管理员。目前无法在Django管理员中注册具有复合公钥的模型。您可以在未来的版本中看到这一点。

迁移到复合主键

创建表后,Django不支持迁移到或从复合公钥迁移。它也不支持从复合公钥中添加或删除字段。

如果您想将现有表从单个公钥迁移到复合公钥,请按照数据库后台的说明进行操作。

复合公钥到位后,添加 CompositePrimaryKey 字段到您的模型。这使得Django能够正确识别和处理复合公钥。

而迁移操作(例如 AddField , AlterField )不支持在主要关键字段上, makemigrations 仍然会检测到变化。

为了避免错误,建议将此类迁移与 --fake .

或者, SeparateDatabaseAndState 可用于在单个操作中执行特定于后台的迁移和Django生成的迁移。

复合主要关键字和关系

Relationship fields ,包括 generic relations 不支持复合公钥。

例如,给定 OrderLineItem 模型,不支持以下内容::

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

因为 ForeignKey 当前无法引用具有复合主键的模型。

为了克服这一限制, ForeignObject 可用作替代品::

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 很像 ForeignKey ,除了它不会创建任何列(例如 item_id )、数据库中的外卡约束或索引。

警告

ForeignObject 是一个内部API。这意味着它不属于我们的范围 deprecation policy .

复合公钥和数据库功能

许多数据库函数只接受单个表达。

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

在这些情况下,提供复合主要关键字引用会引发 ValueError ,因为它由多个列表达式组成。但有一个例外, Count .

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

表单中的复合公钥

由于复合公钥是一个虚拟字段,该字段不代表单个数据库列,因此该字段被排除在模型表单中。

例如,采用以下形式::

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

此表单没有表单字段 pk 对于复合公钥:

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

设置主要复合字段 pk 由于表单字段引发未知字段 FieldError .

主要关键字段是只读的

如果您更改现有对象上的公钥的值并保存它,则将在旧对象旁边创建一个新对象(请参阅 Field.primary_key ).

复合公钥也是如此。因此,您可能想设置 Field.editableFalse 将它们从模型表单中排除。

模型验证中的复合关键字

以来 pk 只是一个虚拟领域,包括 pk 作为字段名称 exclude 论点 Model.clean_fields() 没有效果。将复合主关键字字段排除在 model validation ,单独指定每个字段。 Model.validate_unique() 仍然可以与 exclude={"pk"} 跳过唯一性检查。

构建复合主键就绪应用程序

在引入复合主键之前,组成模型主键的单个字段可以通过内省 primary key 其字段的属性:

>>> 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>

既然公钥可以由多个字段组成, primary key 不再依赖属性来识别主密钥的成员,因为它将被设置为 False 维护不变量,即每个模型最多有一个字段将此属性设置为 True :

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

为了构建正确处理复合公钥的应用程序代码, _meta.pk_fields 应改用属性:

>>> 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>
]