Django - 使用 属性 作为外键

Django - Use a property as a foreign key

我的应用程序的数据库已填充并与外部数据源保持同步。我有一个抽象模型,我的 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.

来追踪对象的来源

不幸的是,Django 的 ORM 不支持多列外键。

问题

同时将自动生成的主键保存在数据库中 (id),我想在 object_id 和 [=] 上创建外键和多对多关系15=] 字段而不是主键 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'.

听起来不错,Django 将 to_field 的字段排除在数据库字段之外。但是不需要向我的 CommonModel 添加新字段,因为 composed_object_type_id 是使用两个不可空字段构建的...

有/可以在 object_id 字段上设置 unique 属性吗?

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)

您在另一个答案的评论中提到 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"),
        )

您在问题中被提及为“不幸的是,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']]

你可以提供list of list, sets of set or simple list, simple set to unique_together option of class meta:.

是的,但 Django 说...

UniqueConstraint provides more functionality than unique_together.

unique_together may be deprecated in the future.

您可以在相同的 class meta: 中添加 UniqueConstraint 而不是 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:
        constraints = [ models.UniqueConstraint(fields=['object_origin', 'object_id'], name='unique_object')]

因此,最佳做法是使用 constraints 选项而不是 class meta:unique_together

您可以将组合对象源 ID 设为一个字段 (composed_object_origin_id),该字段 (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
    )