Уникальное сочетание нескольких внешних ключей и поля "многие ко многим"
Цены нашего бизнеса зависят от множества параметров, и теперь мы хотим ввести еще один возможный параметр 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
всех тех, которые были бы просты?
Версии:
При необходимости я могу преобразовать существующий 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
Примечание. Я не проверял сгенерированный запрос и результаты