Проверка внешнего ключа родительской модели по дочернему классу в Django
Допустим, в моем приложении Django есть следующие родительские модели:
class Location(models.Model):
name = models.CharField(max_length=100)
class Exit(models.Model):
location = models.ForeignKey(Location, on_delete=models.CASCADE, related_name="exits")
closed = models.BooleanField()
И две пары соответствующих дочерних моделей:
class Submarine(Location):
size = models.FloatField()
class Hatch(Exit):
diameter = models.FloatField()
class House(Location):
height = models.FloatField()
class Door(Exit):
width = models.FloatField()
height = models.FloatField()
При такой установке возможно, чтобы House имел Hatch в качестве одного из своих Exit, а также чтобы Submarine имел Door. Есть ли способ явным образом предотвратить это? В идеале, я бы хотел, чтобы при попытке установить недопустимый внешний ключ возникало исключение.
Перемещение поля location из Exit в Hatch и Door не является вариантом, потому что я хочу иметь возможность использовать конструкции, подобные следующим:
open_locations = Location.objects.filter(exits__closed=False)
и избегать дублирования (т.е. написания отдельных функций для Houses и Submarines).
Возможно, ограничение limit_choices_to может быть полезным, но я не смог понять, как его применить здесь.
Вы можете использовать CheckConstraint:
# models.py
from django.db import models
class Location(models.Model):
name = models.CharField(max_length=100)
class Exit(models.Model):
location = models.ForeignKey(Location, on_delete=models.CASCADE, related_name="exits")
closed = models.BooleanField()
class Submarine(Location):
size = models.FloatField()
class Hatch(Exit):
diameter = models.FloatField()
class Meta:
constraints = [
CheckConstraint(
check=Q(location__submarine__is_null=False),
name='hatches_must_be_submarine_exits'
),
]
class House(Location):
height = models.FloatField()
class Door(Exit):
width = models.FloatField()
height = models.FloatField()
class Meta:
constraints = [
CheckConstraint(
check=Q(location__house__is_null=False),
name='doors_must_be_house_exits'
),
]
Django, к сожалению, не очень хорошо работает с наследованием, потому что каждому ребенку все равно нужна своя таблица базы данных. Так что даже если вы сможете заставить это работать, это не будет выглядеть красиво и не поможет вам в дальнейшем. Самым простым и наиболее Djangesque способом будет
class Location(models.Model):
name = models.CharField(max_length=100)
class Meta:
abstract = True
class Exit(models.Model):
closed = models.BooleanField()
class Meta:
abstract = True
class Submarine(Location):
size = models.FloatField()
class Hatch(Exit):
diameter = models.FloatField()
location = models.ForeignKey(Submarine, on_delete=models.CASCADE, related_name="exits")
class House(Location):
height = models.FloatField()
class Door(Exit):
width = models.FloatField()
height = models.FloatField()
location = models.ForeignKey(House, on_delete=models.CASCADE, related_name="exits")
Я добавил Meta с abstract = True, потому что моя интуиция подсказывает, что вы не захотите иметь в базе данных простые Location и Exit объекты, но я могу ошибаться; Meta.abstract говорит Django, что вам не нужны таблицы DB для абстрактных родительских моделей. Повторяющаяся строка Location неудачна, но если таких моделей много, то лучше использовать фабрику, чем наследование. Это будет что-то вроде:
def location_factory(exit_type):
assert isinstance(exit_type, Exit)
return models.ForeignKey(exit_type, on_delete=models.CASCADE, related_name="exits")
class Barrel(Location):
diameter = models.FloatField()
height = models.FloatField()
class Lid(Exit):
diameter = models.FloatField()
location = location_factory(Barrel)