• 6.1
  • Версия документации: 3.1

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

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)») при создании таблицы.

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

>>> 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. Это устанавливает связанные значения полей:

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

Составной первичный ключ также можно фильтровать с помощью кортежа:

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

Мы все еще работаем над поддержкой составного первичного ключа для реляционных полей, включая поля GenericForeignKey и администратора Django. В настоящее время модели с составными первичными ключами невозможно зарегистрировать в администраторе Django. Вы можете ожидать увидеть это в будущих выпусках.

Переход на составной первичный ключ

Django не поддерживает миграцию к составному первичному ключу или обратно после создания таблицы. Он также не поддерживает добавление или удаление полей из составного первичного ключа.

Если вы хотите перенести существующую таблицу с одного первичного ключа на составной первичный ключ, следуйте инструкциям серверной части вашей базы данных.

После создания составного первичного ключа добавьте в модель поле CompositePrimaryKey. Это позволяет Django распознавать и правильно обрабатывать составной первичный ключ.

Хотя операции миграции (например, AddField, AlterField) для полей первичного ключа не поддерживаются, makemigrations все равно обнаружит изменения.

Во избежание ошибок рекомендуется применять такие миграции с помощью --fake.

Альтернативно, SeparateDatabaseAndState может использоваться для выполнения специфичных для серверной части миграций и миграций, сгенерированных Django, за одну операцию.

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

Поля отношений, включая универсальные отношения, не поддерживают составные первичные ключи.

Например, для модели OrderLineItem не поддерживается следующее:

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

Потому что ForeignKey в настоящее время не может ссылаться на модели с составными первичными ключами.

Чтобы обойти это ограничение, в качестве альтернативы можно использовать ForeignObject:

class Foo(models.Model):
    item_order_id = models.CharField(max_length=20)
    item_product_id = models.IntegerField()
    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), ограничений внешнего ключа или индексов в базе данных, а аргумент on_delete игнорируется.

Предупреждение

ForeignObject — это внутренний API. Это означает, что на него не распространяется наша политика устаревания.

Составные первичные ключи и функции базы данных

Многие функции базы данных принимают только одно выражение.

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() не имеет никакого эффекта. Чтобы исключить поля составного первичного ключа из проверки модели <validating-objects>, укажите каждое поле отдельно. 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>
]
Back to Top