Django - использовать свойство как внешний ключ - PullRequest
8 голосов
/ 04 февраля 2020

База данных моего приложения заполняется и синхронизируется с внешними источниками данных. У меня есть абстрактная модель, из которой получены все модели моего приложения Django 2.2, определенные следующим образом:

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

Поле object_id нельзя установить как уникальное поскольку каждый источник данных, который я использую в своем приложении, может иметь объект с object_id = 1. Следовательно, необходимо отследить происхождение объекта с помощью поля object_origin.

К сожалению, ORM Django не поддерживает внешние ключи более чем в одном столбце.

Проблема

Сохраняя автоматически сгенерированный первичный ключ в базе данных (id), я хотел бы, чтобы мой внешний ключ и отношения многие ко многим происходили как на object_id, так и на object_origin поля вместо первичного ключа id.

Что я пробовал

Я думал о том, чтобы сделать что-то вроде этого:

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)

Но Django жалуется об этом:

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

И это звучит как le git, Django, за исключением того, что поле, присвоенное to_field, является полем базы данных. Но нет необходимости добавлять новое поле в мой CommonModel, так как composed_object_type_id построен с использованием двух необнуляемых полей ...

Ответы [ 4 ]

6 голосов
/ 10 февраля 2020

Вы упомянули в своем комментарии в другом ответе, что object_id не уникален, но уникален в сочетании с object_type, так что вы могли бы использовать unique_together в метаклассе? т.е.

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

    class Meta:
        unique_together = (
            ("object_type", "object_id"),
        )
1 голос
/ 14 февраля 2020

Вы можете сделать идентификатор источника составного объекта полем (composed_object_origin_id), которое обновляется в save и используется как 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
    )
1 голос
/ 13 февраля 2020

Вы упоминаетесь в вашем вопросе как " К сожалению, Django ORM не поддерживает более чем один столбец внешних ключей ".

Да, Django не предоставлять такой тип поддержки, потому что Django имеет более надежный, чем мы думаем:)

Итак, Django предоставляют одну мета-опцию для преодоления этого типа проблемы, и эта опция unique_together.

Вы можете предоставить Наборы имен полей, которые, взятые вместе, должны быть уникальными, в вашем случае ...

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

Вы можете предоставить список списков, наборов наборов или простой список, простой установить unique_together вариант class meta:.

Да, но Django сказал, что ...

UniqueConstraint предоставляет больше функциональности, чем unique_together.

unique_together может быть устаревшим в будущем.

Вы можете добавить UniqueConstraint вместо unique_together в том же class meta:, в вашем случае вы можете написать, как показано ниже ...

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

Таким образом, рекомендуется использовать параметр constraints вместо unique_together из class meta:.

1 голос
/ 04 февраля 2020

Имеете / Можете ли вы установить атрибут unique в поле object_id?

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

Если это не работает, я бы изменил тип поля на поле uuid:

class CommonModel(models.Model):
    object_type = models.IntegerField()
    object_uuid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...