Django - Use uma propriedade como uma chave estrangeira

8

O banco de dados do meu aplicativo é preenchido e mantido sincronizado com fontes de dados externas. Eu tenho um modelo abstrato a partir do qual todos os modelos do meu aplicativo Django 2.2 derivam, definidos da seguinte maneira:

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

O object_idcampo não pode ser definido como exclusivo, pois cada fonte de dados que eu uso no meu aplicativo pode ter um objeto com um object_id = 1. Daí a necessidade de rastrear a origem do objeto, pelo campo object_origin.

Infelizmente, o ORM do Django não suporta chaves estrangeiras de mais de uma coluna.

Problema

Enquanto mantém a chave primária gerada automaticamente no banco de dados ( id), gostaria que minha chave estrangeira e as relações muitos-para-muitos acontecessem nos campos object_ide object_originem vez da chave primária id.

O que eu tentei

Pensei em fazer algo assim:

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)

Mas o Django reclama disso:

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

E parece legítimo, o Django exceto o arquivado dado to_fieldcomo um campo de banco de dados. Mas não há necessidade de adicionar um novo campo ao meu, CommonModelpois composed_object_type_idé construído usando dois campos não anuláveis ​​...

Spacebrain
fonte
2
Idéia interessante, mas isso parece um problema xy da minha perspectiva ... Por que você precisa disso?
Restabelecer Monica

Respostas:

6

Você mencionou em seu comentário na outra resposta que object_id não é exclusivo, mas é único em combinação com object_type, então você poderia usar um unique_togetherna metaclasse? ie

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

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

Você tem / pode definir o uniqueatributo no object_idcampo?

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

Se isso não funcionar, eu mudaria o tipo de campo para um uuidcampo:

class CommonModel(models.Model):
    object_type = models.IntegerField()
    object_uuid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False)
Victor Hug
fonte
Infelizmente, object_idnão pode ser definido como único, porque há casos em que não é exclusivo. Na verdade, na fonte de dados externa que me fornece os dados que uso no meu aplicativo, a chave primária é composta por dois campos: object_typee object_id.
Spacebrain
Se object_idnão for exclusivo, você não deve criar uma chave estrangeira. Isso pode causar erros no banco de dados e você não deseja isso. Se você não quiser usar o pk, também poderá gerenciar o racionamento em si mesmo nas models.Modelfunções internas.
Victor Hug
Bem, object_typee object_idjuntos são garantidos para ser único. Mas object_idsozinho não é.
Spacebrain
Aqui você pode encontrar um pacote pip para criar uma restrição de chave estrangeira para um composto (duas chaves): django-composite-foreignkey.readthedocs.io/en/latest/…
Victor Hug
Infelizmente, os aplicativos Django 2.1+ não são suportados e parece que esta biblioteca não é mantida ativamente.
Spacebrain
1

Você é mencionado em sua pergunta como " Infelizmente, o ORM do Django não suporta chaves estrangeiras de mais de uma coluna ".

Sim, o Django não fornece esse tipo de suporte porque o Django é mais confiável do que pensamos :)

Então, o Django fornece uma meta opção para superar esse tipo de problema e essa opção é unique_together.

Você pode fornecer conjuntos de nomes de campos que, juntos, devem ser exclusivos, no seu 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']]

Você pode fornecer uma lista de lista, conjuntos de conjuntos ou lista simples, conjunto simples à unique_togetheropção de class meta:.

Sim, mas o Django disse que ...

UniqueConstraint fornece mais funcionalidade que unique_together.

unique_together pode ser preterido no futuro.

Você pode adicionar, em UniqueConstraintvez de unique_togetherno mesmo, class meta:no seu caso, você pode escrever como abaixo ...

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

Portanto, a melhor prática é usar a constraintsopção em vez unique_togetherde class meta:.

MK Patel
fonte
1

Você pode transformar o ID de origem do objeto composto em um campo ( composed_object_origin_id) atualizado savee usado como o 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
    )
Cole
fonte