【问题标题】:Unique together involving multiple foreign keys & a many to many field唯一一起涉及多个外键和多对多字段
【发布时间】:2019-11-27 07:51:24
【问题描述】:

我们的业务定价取决于多个参数,现在我们想在 Django 中的现有设置中引入另一个可能的 M2M 参数。

为此,我们有一个现有的定价表,它对除price_field 之外的所有字段都有unique_together 约束。对示例中的通用/基于字母的命名表示歉意。

class PricingTable(models.Model):
    a = models.ForeignKey(A, on_delete=models.CASCADE)
    price = MoneyField()
    b = ArrayField(models.CharField(choices=CHOICES))
    c = models.ForeignKey(C, on_delete=models.CASCADE)

    class Meta:
        ordering = ("a",)
        unique_together = ("a", "b", "c")

    def validate_b(self):
        # b can't be empty
        if not len(self.b) >= 1:
            raise ValueError
        # each element in b needs to be unique
        if not len(self.b) == len(set(self.b)):
            raise ValueError
        # each element in b needs to be unique together with a & c
        for i in self.b:
            query = PricingTable.objects.filter(
                a=self.a, c=self.c, b__contains=[i]
            ).exclude(pk=self.pk)
            if query.count() > 0:
                raise ValueError

    def save(self, *args, **kwargs):
        self.validate_b()
        return super().save(*args, **kwargs)

我想在此表中引入另一个参数,该参数必须与非价格参数(abc)一起唯一。

d = models.ManyToManyField("x.D", related_name="+")

列表b 中的每个元素与ac 一起必须是唯一的。

上述问题是validate_b 函数必须升级为可能具有繁重数据库查询的复杂函数。除此之外,Django 并没有提供一种直接的方式来确保多对多领域的独特团结。

那么,有没有我应该尝试的不同方法? through table 也许?但是,我应该在直通表中包含哪些所有字段?所有非价格字段?或者我是否应该停止梦想为d 提供多对多字段,并继续使用简单的外键方法,并在所有那些直截了当的字段上使用unique_together

版本:

  • Django 2.2
  • postgres 11

如果需要,我可以将现有的 ArrayField 转换为一个简单的 CharField,这意味着更多的数据库行,只要我将所有独特的约束都放入数据库而不是每次都验证保存,这在某种程度上是可以的。

【问题讨论】:

    标签: python django postgresql django-models database-design


    【解决方案1】:

    场所

    在 Sql 和 Django ORM 中,您不能在多对多字段上设置唯一约束,因为它涉及两个不同的表。

    SQL 解决方案:

    您可以尝试在 django 上重现this 解决方案。

    但为了做到这一点,您必须手动创建 tab_constr 并在 save 方法或 signals 中插入触发逻辑

    Django 解决方案

    我不建议您遵循该解决方案,因为它很难在 django 中重现,实际上您必须使用两个外部密钥和一个额外的表手动重现 m2m 引用。

    只需检查on_save 方法,没有其他方法。

    附言

    不要使用 save 方法的覆盖来检查您的对象,因为如果您更改对象的 QuerySet,则不会调用此方法。 而是使用这样的信号:

    @receiver(post_save, sender=Program)
    def on_save_pricing_table(sender, instance, created, **kwargs):
        if not instance.value = 10:
            raise ValueError
    

    【讨论】:

    【解决方案2】:

    你应该尝试overlap来替换

    # each element in b needs to be unique together with a & c
    for i in self.b:
        query = PricingTable.objects.filter(
            a=self.a, c=self.c, b__contains=[i]
        ).exclude(pk=self.pk)
        if query.count() > 0:
            raise ValueError
    

    query = PricingTable.objects.filter(
        a=self.a, c=self.c, b__overlap=self.b
    ).exclude(pk=self.pk)
    if query.count() > 0:
                raise ValueError
    

    注意:我没有验证生成的查询和性能

    【讨论】:

      【解决方案3】:

      由于只有少数d 实体必须有相应的价格(其余的将继续有一个通用的通用价格),我最终得到了以下结构。

      class PricingTable(models.Model):
          a = models.ForeignKey(A, on_delete=models.CASCADE)
          price = MoneyField()
          b = ArrayField(models.CharField(choices=CHOICES))
          c = models.ForeignKey(C, on_delete=models.CASCADE)
          d = models.ForeignKey("x.D", on_delete=models.CASCADE, blank=True, null=True)
      
          class Meta:
              ordering = ("a",)
              unique_together = ("a", "b", "c", "d")
      
          def validate_b(self):
              # b can't be empty
              if not len(self.b) >= 1:
                  raise ValueError
              # each element in b needs to be unique
              if not len(self.b) == len(set(self.b)):
                  raise ValueError
              # each element in b needs to be unique together with a, c & d
              query = PricingTable.objects.filter(
                  a=self.a, c=self.c, d=self.d, b__overlap=self.b
              ).exclude(pk=self.pk)
              if query.count() > 0:
                  raise ValueError
      
          def save(self, *args, **kwargs):
              self.validate_b()
              return super().save(*args, **kwargs)
      
      
      class DBasedPricing(models.Model):
          """
          Lookup table that tells (row exists) if we have D based pricing coupled with param A
          If we do, query PricingTable.d=d, else PricingTable.d=None for correct pricing
          """
          d = models.ForeignKey("x.D", on_delete=models.CASCADE)
          a = models.ForeignKey(A, on_delete=models.CASCADE)
      
          class Meta:
              unique_together = ("d", "a")
      

      这迫使我首先根据d 参数进行查找,以检查定价是否基于D

      d_id = None
      if DBasedPricing.objects.filter(d_id=input_param.d, a_id=a.id).exists():
          d_id = input_param.d
      

      然后将另一个参数添加到我的常用查询中

      price_obj = PricingTable.objects.filter(...usual query..., d_id=d_id)
      

      总体而言,以单个简单的索引查找为代价,我节省了行数,当然还有复杂的数据库结构。此外,我最终不必重新输入所有现有定价!

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2015-02-09
        • 1970-01-01
        • 2014-11-21
        • 1970-01-01
        • 2016-03-13
        • 2016-02-21
        • 1970-01-01
        • 2015-09-25
        相关资源
        最近更新 更多