Уникальное сочетание нескольких внешних ключей и поля "многие ко многим"

Цены нашего бизнеса зависят от множества параметров, и теперь мы хотим ввести еще один возможный параметр M2M в существующую настройку в Django.

Для этого у нас есть существующая таблица для расчета цены, которая имеет ограничение unique_together для всех полей, кроме price_field. Извинения за общее/буквенное именование в примере.

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)

Я хочу ввести в эту таблицу еще один параметр, который должен быть уникальным - вместе с неценовыми параметрами (a, b & c).

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

Каждый элемент в списке b должен быть уникальным вместе с a & c.

Проблема с вышесказанным заключается в том, что функцию validate_b необходимо обновить до возможно сложной функции с тяжелыми запросами к БД. Наряду с этим, Django не предоставляет прямой путь для обеспечения уникального единения многих со многими полями.

Итак, есть ли другой подход, который я, вероятно, должен попробовать? А through столик возможно? Но тогда, что все поля я должен включить в сквозную таблицу? Все неценовые поля? Или я должен перестать мечтать о том, чтобы иметь поле "многие ко многим" для " d и перейти к простому подходу с иностранными ключами и иметь unique_together всех тех, которые были бы просты?

Версии:

  • Джанго 2.2
  • Postgres 11

При необходимости я могу преобразовать существующий ArrayField в простой CharField который будет означать больше строк БД, что несколько хорошо, если я CharField все уникальные ограничения в базу данных, а не проверю каждый раз при сохранении.

Ответ 1

Поскольку только у нескольких организаций 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)

В целом, за счет одного простого индексированного поиска я экономлю на строках & конечно сложное структурирование БД. Кроме того, мне не пришлось повторно вводить все существующие цены!

Ответ 2

Предпосылки

В Sql и т.д. В Django ORM вы не можете установить уникальные ограничения на множество полей, потому что в нем задействованы две разные таблицы.

Решение SQL:

Вы можете попытаться воспроизвести это решение на Django.

Но чтобы сделать это, вам нужно вручную создать tab_constr и вставить логику триггера в метод save или с сигналами

Джанго решение

Я не рекомендую вам следовать этому решению, потому что это трудно воспроизвести в django, на самом деле вы должны вручную воспроизвести ссылку m2m с двумя внешними ключами и одной дополнительной таблицей.

Просто поставьте галочку на методе on_save другого пути нет.

PS

Не используйте переопределение метода save для добавления проверки вашего объекта, потому что этот метод не вызывается, если вы изменяете QuerySet объектов. Вместо этого используйте сигнал как это:

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

Ответ 3

Вы должны попробовать перекрытие, чтобы заменить

# 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

Примечание. Я не проверял сгенерированный запрос и результаты