Claves primarias compuestas

En Django, cada modelo tiene una clave primaria. Por defecto, esta clave primaria consiste en un solo campo.

En la mayoría de los casos, una sola clave primaria debería ser suficiente. En el diseño de bases de datos, sin embargo, definir una clave primaria compuesta por múltiples campos es a veces necesario.

Para utilizar una clave primaria compuesta, al definir un modelo establezca la atributo pk para que sea un 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()

Esto instruirá a Django a crear una clave primaria compuesta (PRIMARY KEY (product_id, order_id)) cuando se cree la tabla.

Una clave primaria compuesta está representada por 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")

Puedes asignar un tuple al atributo pk. Esto establece los valores de los campos asociados:

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

También puedes filtrar una clave primaria compuesta mediante un tuple:

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

Aún estamos trabajando en el soporte para claves primarias compuestas en los campos relacionales, incluidos los campos GenericForeignKey, y la interfaz de administración de Django. Los modelos con claves primarias compuestas no pueden registrarse actualmente en la interfaz de administración de Django. Puedes esperar ver esto en futuras versiones.

Migrar a una clave primaria compuesta

Django no admite la migración a o desde una clave primaria compuesta después de que se crea la tabla. También no admite agregar o eliminar campos de la clave primaria compuesta.

Si deseas migrar una tabla existente de una clave primaria única a una clave primaria compuesta, sigue las instrucciones de tu backend de base de datos para hacerlo.

Una vez que esté en lugar la clave primaria compuesta, agrega el campo CompositePrimaryKey a tu modelo. Esto permite a Django reconocer y manejar apropiadamente la clave primaria compuesta.

Aunque las operaciones de migración (por ejemplo, AddField, AlterField) en los campos de clave primaria no están soportadas, makemigrations seguirá detectando cambios.

Para evitar errores, se recomienda aplicar tales migraciones con --fake.

Alternativamente, SeparateDatabaseAndState puede usarse para ejecutar las migraciones específicas del backend y las generadas por Django en una sola operación.

Claves primarias compuestas y relaciones

Campos de relación, incluyendo relaciones genéricas no admiten claves primarias compuestas.

Por ejemplo, dado el modelo OrderLineItem, lo siguiente no está soportado:

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

Porque ForeignKey no puede referirse actualmente a modelos con claves primarias compuestas.

Para trabajar alrededor de esta limitación, se puede utilizar ForeignObject como alternativa:

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 es muy similar a ForeignKey, excepto que no crea ninguna columna (por ejemplo, item_id), restricciones de clave foránea o índices en la base de datos, y el argumento on_delete se ignora.

Advertencia

ForeignObject es una API interna. Esto significa que no está cubierto por nuestra política de deprecación.

Claves primarias compuestas y funciones de la base de datos

Muchas funciones de la base de datos solo aceptan una expresión única.

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

En estos casos, proporcionar una referencia a una clave primaria compuesta eleva un ValueError, ya que está compuesta por múltiples expresiones de columnas. Se hace una excepción para Count.

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

Claves primarias compuestas en formularios

Como una clave primaria compuesta es un campo virtual, un campo que no representa una columna de base de datos única, este campo se excluye de los ModelForms.

Por ejemplo, considere el siguiente formulario:

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

Esta forma no tiene un campo de formulario pk para la clave primaria compuesta:

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

Establecer el campo primario compuesto pk como un campo de formulario levanta un campo desconocido FieldError.

Los campos de clave primaria son solo de lectura

Si cambias el valor de una clave primaria en un objeto existente y luego lo guardas, se creará un nuevo objeto junto al antiguo (consultar Field.primary_key).

Lo mismo es cierto para las claves primarias compuestas. Por tanto, puedes querer establecer Field.editable en False en todos los campos de clave primaria para excluirlos de ModelForms.

Claves primarias compuestas en la validación del modelo

Dado que pk es solo un campo virtual, incluir pk como nombre de campo en el argumento exclude de Model.clean_fields() no tiene efecto alguno. Para excluir los campos de clave primaria compuesta de la validación del modelo, especifica cada campo individualmente. Model.validate_unique() todavía se puede llamar con exclude={"pk"} para saltar las comprobaciones de unicidad.

Creando aplicaciones listas para usar con claves primarias compuestas

Antes de la introducción de las claves primarias compuestas, el campo que formaba parte de la clave primaria de un modelo se podía recuperar mediante introspección del atributo clave primaria de sus campos:

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

Ahora que una clave primaria puede estar compuesta por múltiples campos, el atributo clave primaria ya no se puede confiar para identificar a los miembros de la clave primaria, ya que se establecerá en False para mantener la invariante de que como máximo un campo por modelo tendrá este atributo establecido en True:

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

Para construir código de aplicación que maneje correctamente claves primarias compuestas se debe utilizar el atributo _meta.pk_fields en lugar de:

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