Django: use una propiedad como clave externa

8

La base de datos de mi aplicación se completa y se mantiene sincronizada con fuentes de datos externas. Tengo un modelo abstracto del que derivan todos los modelos de mi aplicación Django 2.2, definidos de la siguiente manera:

class CommonModel(models.Model):
    # Auto-generated by Django, but included in this example for clarity.
  # id = models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')
    ORIGIN_SOURCEA = '1'
    ORIGIN_SOURCEB = '2'
    ORIGIN_CHOICES = [
        (ORIGIN_SOURCEA, 'Source A'),
        (ORIGIN_SOURCEB, 'Source B'),
    ]
    object_origin = models.IntegerField(choices=ORIGIN_CHOICES)
    object_id = models.IntegerField()

class A(CommonModel):
    some_stuff = models.CharField()

class B(CommonModel):
    other_stuff = models.IntegerField()
    to_a_fk = models.ForeignKey("myapp.A", on_delete=models.CASCADE)

class C(CommonModel):
    more_stuff = models.CharField()
    b_m2m = models.ManyToManyField("myapp.B")

El object_idcampo no se puede establecer como único ya que cada fuente de datos que uso en mi aplicación puede tener un objeto con un object_id = 1. De ahí la necesidad de rastrear el origen del objeto, por el campo object_origin.

Desafortunadamente, el ORM de Django no admite claves externas de más de una columna.

Problema

Mientras idmantengo la clave primaria autogenerada en la base de datos ( ), me gustaría hacer que mi clave externa y las relaciones de muchos a muchos sucedan en ambos campos object_idy en object_originlugar de la clave primaria id.

Lo que he intentado

Pensé en hacer algo como esto:

class CommonModel(models.Model):
    # Auto-generated by Django, but included in this example for clarity.
  # id = models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')
    ORIGIN_SOURCEA = '1'
    ORIGIN_SOURCEB = '2'
    ORIGIN_CHOICES = [
        (ORIGIN_SOURCEA, 'Source A'),
        (ORIGIN_SOURCEB, 'Source B'),
    ]
    object_origin = models.IntegerField(choices=ORIGIN_CHOICES)
    object_id = models.IntegerField()

    def _get_composed_object_origin_id(self):
        return f"{self.object_origin}:{self.object_id}"
    composed_object_origin_id = property(_get_composed_object_origin_id)

class A(CommonModel):
    some_stuff = models.CharField()

class B(CommonModel):
    other_stuff = models.IntegerField()
    to_a_fk = models.ForeignKey("myapp.A", to_field="composed_object_origin_id", on_delete=models.CASCADE)

Pero Django se queja al respecto:

myapp.B.to_a_fk: (fields.E312) The to_field 'composed_object_origin_id' doesn't exist on the related model 'myapp.A'.

Y suena legítimo, Django exceptuó el archivo dado to_fieldpara ser un campo de base de datos. Pero no hay necesidad de agregar un nuevo campo a mi CommonModelya que composed_object_type_idestá construido con dos campos no anulables ...

Spacebrain
fuente
2
Idea interesante, pero esto parece un problema xy desde mi perspectiva ... ¿Por qué necesitas esto?
Restablece a Mónica

Respuestas:

6

Usted mencionó en su comentario en la otra respuesta que object_id no es único pero es único en combinación con object_type, entonces, ¿podría usar un unique_togetheren la metaclase? es decir

class CommonModel(models.Model):
    object_type = models.IntegerField()
    object_id = models.IntegerField()

    class Meta:
        unique_together = (
            ("object_type", "object_id"),
        )
Kathy Rindhoops
fuente
1

¿Tiene / puede establecer el uniqueatributo en el object_idcampo?

class CommonModel(models.Model):
    object_type = models.IntegerField()
    object_id = models.IntegerField(unique=True)

Si esto no funciona, cambiaría el tipo de campo a un uuidcampo:

class CommonModel(models.Model):
    object_type = models.IntegerField()
    object_uuid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False)
Victor Hug
fuente
Desafortunadamente, object_idno se puede establecer como único porque hay casos en los que no es único. En realidad, en la fuente de datos externa que me proporciona los datos que uso en mi aplicación, la clave principal se compone de dos campos: object_typey object_id.
Spacebrain
Si object_idno es único, no debe crear una clave externa. Esto podría causar errores en la base de datos y no quiere eso. Si no desea utilizar el pk en su lugar, también puede administrar la relación usted mismo en las models.Modelfunciones integradas.
Victor Hug
Bueno, object_typey object_idjuntos se garantiza que serán únicos. Pero object_idsolo no lo es.
Spacebrain
Aquí puede encontrar un paquete pip para crear una restricción de clave externa para un compuesto (dos claves): django-composite-foreignkey.readthedocs.io/en/latest/…
Victor Hug
Desafortunadamente, las aplicaciones Django 2.1+ no son compatibles y parece que esta biblioteca no se mantiene activamente.
Spacebrain
1

Usted es mencionado en su pregunta como " Desafortunadamente, el ORM de Django no admite claves externas de más de una columna ".

Sí, Django no proporciona ese tipo de soporte porque Django es más confiable de lo que pensamos :)

Entonces, Django proporciona una meta opción para superar este tipo de problema y esa opción es unique_together.

Puede proporcionar conjuntos de nombres de campo que, en conjunto, deben ser únicos, en su caso ...

class CommonModel(models.Model):
    # Auto-generated by Django, but included in this example for clarity.
    # id = models.AutoField(auto_created=True, primary_key=True, 
    serialize=False, verbose_name='ID')
    ORIGIN_SOURCEA = '1'
    ORIGIN_SOURCEB = '2'
    ORIGIN_CHOICES = [
        (ORIGIN_SOURCEA, 'Source A'),
        (ORIGIN_SOURCEB, 'Source B'),
    ]
    object_origin = models.IntegerField(choices=ORIGIN_CHOICES)
    object_id = models.IntegerField()

    class meta:
        unique_together = [['object_origin', 'object_id']]

Puede proporcionar una lista de la lista, conjuntos de conjuntos o lista simple, conjunto simple a unique_togetheropción de class meta:.

Sí, pero Django dijo que ...

UniqueConstraint proporciona más funcionalidades que unique_together.

unique_together puede quedar en desuso en el futuro.

Puede agregar en UniqueConstraintlugar de unique_togetheren el mismo class meta:en su caso, puede escribir de la siguiente manera ...

class CommonModel(models.Model):
    # Auto-generated by Django, but included in this example for clarity.
    # id = models.AutoField(auto_created=True, primary_key=True, 
    serialize=False, verbose_name='ID')
    ORIGIN_SOURCEA = '1'
    ORIGIN_SOURCEB = '2'
    ORIGIN_CHOICES = [
        (ORIGIN_SOURCEA, 'Source A'),
        (ORIGIN_SOURCEB, 'Source B'),
    ]
    object_origin = models.IntegerField(choices=ORIGIN_CHOICES)
    object_id = models.IntegerField()

    class meta:
        constraints = [ models.UniqueConstraint(fields=['object_origin', 'object_id'], name='unique_object')]

Por lo tanto, la mejor práctica es usar la constraintsopción en lugar unique_togetherde class meta:.

MK Patel
fuente
1

Puede hacer que el ID de origen del objeto compuesto sea un campo ( composed_object_origin_id) que se actualice savey se use como to_field.

class CommonModel(models.Model):
    ORIGIN_SOURCEA = "1"
    ORIGIN_SOURCEB = "2"
    ORIGIN_CHOICES = [
        (ORIGIN_SOURCEA, "Source A"),
        (ORIGIN_SOURCEB, "Source B"),
    ]
    object_origin = models.IntegerField(choices=ORIGIN_CHOICES)
    object_id = models.IntegerField()
    composed_object_origin_id = models.CharField(max_length=100, unique=True)

    def save(self, **kwargs):
        self.composed_object_origin_id = f"{self.object_origin}:{self.object_id}"

        # Just in case you use `update_fields`, force inclusion of the composed object origin ID.
        # NOTE: There's definitely a less error-prone way to write this `if` statement but you get
        # the gist. e.g., this does not handle passing `update_fields=None`.
        if "update_fields" in kwargs:
            kwargs["update_fields"].append("composed_object_origin_id")

        super().save(**kwargs)


class A(CommonModel):
    some_stuff = models.CharField(max_length=1)


class B(CommonModel):
    other_stuff = models.IntegerField()
    to_a_fk = models.ForeignKey(
        "myapp.A", to_field="composed_object_origin_id", on_delete=models.CASCADE
    )
Col
fuente