Составные первичные ключи

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

Атрибуту pk можно присвоить значение tuple. При этом задаются значения соответствующих полей:

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

Составные первичные ключи в формах

Поскольку составной первичный ключ - это виртуальное поле, которое не представляет ни одного столбца базы данных, это поле исключено из ModelForms.

Например, возьмем следующую форму:

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.editable значение False для всех полей первичного ключа, чтобы исключить их из ModelForms.

Составные первичные ключи при проверке модели

Поскольку 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>
]
Вернуться на верх