Создание модели с двумя необязательными, но одним обязательным внешним ключом


9

Моя проблема в том, что у меня есть модель, которая может взять один из двух внешних ключей, чтобы сказать, что это за модель. Я хочу, чтобы это заняло хотя бы один, но не оба. Могу ли я иметь это по-прежнему одной модели или я должен разделить его на два типа. Вот код:

class Inspection(models.Model):
    InspectionID = models.AutoField(primary_key=True, unique=True)
    GroupID = models.ForeignKey('PartGroup', on_delete=models.CASCADE, null=True, unique=True)
    SiteID = models.ForeignKey('Site', on_delete=models.CASCADE, null=True, unique=True)

    @classmethod
    def create(cls, groupid, siteid):
        inspection = cls(GroupID = groupid, SiteID = siteid)
        return inspection

    def __str__(self):
        return str(self.InspectionID)

class InspectionReport(models.Model):
    ReportID = models.AutoField(primary_key=True, unique=True)
    InspectionID = models.ForeignKey('Inspection', on_delete=models.CASCADE, null=True)
    Date = models.DateField(auto_now=False, auto_now_add=False, null=True)
    Comment = models.CharField(max_length=255, blank=True)
    Signature = models.CharField(max_length=255, blank=True)

Проблема в Inspectionмодели. Это должно быть связано либо с группой, либо с сайтом, но не с обоими. В настоящее время с этой настройкой нужно и то и другое.

Я бы предпочел не разбивать это на две почти идентичные модели, GroupInspectionи SiteInspectionпоэтому любое решение, которое сохраняет его как одну модель, было бы идеальным.


Возможно, использование подклассов лучше здесь. Вы можете сделать Inspectionкласс, а затем подкласс в SiteInspectionи GroupInspectionдля не являющихся -Общих частей.
Виллем Ван Онсем

Возможно, не связаны, но unique=Trueчасть в ваших полях FK означает, что только один Inspectionэкземпляр может существовать для одного данного GroupIDили SiteIDэкземпляра - IOW, это отношение один к одному, а не один ко многим. Это действительно то, что вы хотите?
Bruno Desthuilliers

«В настоящее время с этой настройкой нужны оба». => Технически это не так - на уровне базы данных вы можете установить оба, либо ни один из этих ключей (с оговоркой, упомянутой выше). Только при использовании ModelForm (напрямую или через администратора django) эти поля будут помечены как обязательные, и это потому, что вы не передали аргумент «blank = True».
Bruno Desthuilliers

@brunodesthuilliers Да, идея заключается в том, чтобы Inspectionбыть связующим звеном между Groupили Siteи an InspectionID, тогда я могу провести несколько «проверок» в форме InspectionReportэтих отношений. Это было сделано для того, чтобы мне было легче сортировать Dateвсе записи, относящиеся к одному Groupили Site. Надеюсь, что это имеет смысл
CalMac

@ Cm0295 Боюсь, не вижу смысла в этом уровне косвенности - помещение FK группы / сайта непосредственно в InspectionReport дает тот же сервис AFAICT - отфильтруйте ваши InspectionReports по соответствующему ключу (или просто следуйте обратному дескриптору с сайта или Группа), отсортировать их по дате, и все готово.
Bruno Desthuilliers

Ответы:


5

Я бы посоветовал вам сделать такую ​​проверку способом Джанго

переопределив cleanметод модели Джанго

class Inspection(models.Model):
    ...

    def clean(self):
        if <<<your condition>>>:
            raise ValidationError({
                    '<<<field_name>>>': _('Reason for validation error...etc'),
                })
        ...
    ...

Обратите внимание, что, как и Model.full_clean (), метод clean () модели не вызывается при вызове метода save () вашей модели. его нужно вызывать вручную для проверки данных модели, или вы можете переопределить метод сохранения модели, чтобы он всегда вызывал метод clean () перед запуском Modelметода сохранения класса


Другое решение, которое могло бы помочь, - это использование GenericRelations для обеспечения полиморфного поля, связанного с более чем одной таблицей, но это может быть в том случае, если эти таблицы / объекты могут использоваться взаимозаменяемо в проекте системы с самого начала.


2

Как упомянуто в комментариях, причина того, что «с этой настройкой нужны оба», заключается в том, что вы забыли добавить blank=Trueполя FK, так что ваше ModelForm(либо пользовательское, либо сгенерированное по умолчанию администратором) сделает поле формы обязательным. , На уровне схемы БД вы можете заполнить оба, либо один, либо ни один из этих FK, это будет нормально, поскольку вы сделали эти поля БД обнуляемыми (с null=Trueаргументом).

Кроме того, (см. Мои другие комментарии), вы можете проверить, действительно ли вы хотите, чтобы FK были уникальными. Это технически превращает ваши отношения один-ко-многим в отношения один-к-одному - вам разрешена только одна запись «осмотра» для данного GroupID или SiteId (у вас не может быть двух или более «проверок» для одного GroupId или SiteId) , Если это действительно то, что вы хотите, вы можете вместо этого использовать явный OneToOneField (схема БД будет такой же, но модель будет более явной, а связанный дескриптор гораздо более пригодным для этого варианта использования).

В качестве примечания: в модели Django поле ForeignKey материализуется как экземпляр связанной модели, а не как необработанный идентификатор. IOW, учитывая это:

class Foo(models.Model):
    name = models.TextField()

class Bar(models.Model):
    foo = models.ForeignKey(Foo)


foo = Foo.objects.create(name="foo")
bar = Bar.objects.create(foo=foo)

тогда bar.fooразрешу foo, а не foo.id. Таким образом , вы , конечно , хотите переименовать InspectionIDи SiteIDполе собственно inspectionи site. Кстати, в Python соглашение об именах называется all_lower_with_underscores для всего, кроме имен классов и псевдоконстант.

Теперь о вашем основном вопросе: не существует конкретного стандартного способа SQL для принудительного применения «одного или другого» ограничения на уровне базы данных, поэтому обычно это делается с использованием ограничения CHECK , что делается в модели Django с мета-«ограничениями» модели. вариант .

При этом, как фактически поддерживаются и применяются ограничения на уровне базы данных, зависит от вашего поставщика БД (MySQL <8.0.16 просто игнорирует их, например), и вид ограничения, который вам здесь понадобится , не будет применяться в форме или проверка на уровне модели , только при попытке сохранить модель, поэтому вы также хотите добавить проверку либо на уровне модели (предпочтительно), либо на уровне формы, в обоих случаях в (соответственно) модели или clean()методе формы .

Короче говоря:

  • сначала проверьте, действительно ли вы хотите это unique=Trueограничение, и если да, то замените поле FK на OneToOneField.

  • добавьте blank=Trueаргумент в оба поля FK (или OneToOne)

  • добавьте правильное проверочное ограничение в метаданные вашей модели - документ является кратким, но все еще достаточно явным, если вы знаете, что делать сложные запросы с помощью ORM (а если нет, пора учиться ;-))
  • добавьте clean()метод в вашу модель, который проверяет, есть ли у вас одно или другое поле, и выдает ошибку проверки еще

и вы должны быть в порядке, если, конечно, ваша СУБД соблюдает контрольные ограничения.

Просто отметьте, что с этим дизайном ваша Inspectionмодель является абсолютно бесполезной (но дорогостоящей!) Ссылкой - вы получите те же функции с меньшими затратами, переместив FK (и ограничения, валидацию и т. Д.) Непосредственно в InspectionReport.

Теперь может быть другое решение - сохранить модель Inspection, но поместить FK как OneToOneField на другом конце отношения (в Site и Group):

class Inspection(models.Model):
    id = models.AutoField(primary_key=True) # a pk is always unique !

class InspectionReport(models.Model):
    # you actually don't need to manually specify a PK field,
    # Django will provide one for you if you don't
    # id = models.AutoField(primary_key=True)

    inspection = ForeignKey(Inspection, ...)
    date = models.DateField(null=True) # you should have a default then
    comment = models.CharField(max_length=255, blank=True default="")
    signature = models.CharField(max_length=255, blank=True, default="")


class Group(models.Model):
    inspection = models.OneToOneField(Inspection, null=True, blank=True)

class Site(models.Model):
    inspection = models.OneToOneField(Inspection, null=True, blank=True)

И тогда вы можете получить все отчеты для данного сайта или группы с yoursite.inspection.inspectionreport_set.all().

Это избавляет от необходимости добавлять какие-либо конкретные ограничения или проверки, но за счет дополнительного уровня косвенности ( joinпункт SQL и т. Д.).

То, какое из этих решений будет «лучшим», действительно зависит от контекста, поэтому вы должны понять значение обоих и проверить, как вы обычно используете свои модели, чтобы выяснить, что больше подходит для ваших собственных нужд. Насколько мне известно, без какого-либо контекста (или сомнений) я бы предпочел использовать решение с меньшим уровнем косвенности, но с YMMV.

Обратите внимание на родовые отношения: они могут быть полезны, когда у вас действительно много возможных связанных моделей и / или вы заранее не знаете, какие модели вы хотите связать с вашими собственными. Это особенно полезно для многократно используемых приложений (например, «комментарии» или «теги» и т. Д.) Или расширяемых (структуры управления контентом и т. Д.). Недостатком является то, что это делает запросы намного более тяжелыми (и довольно непрактичными, когда вы хотите выполнять ручные запросы к вашей базе данных). Исходя из опыта, они могут быстро стать ботом PITA по отношению к коду и программному обеспечению, поэтому лучше их хранить, когда нет лучшего решения (и / или когда нет проблем с обслуживанием и временем выполнения).

Мои 2 цента.


2

В Django появился новый (начиная с 2.2) интерфейс для создания ограничений БД: https://docs.djangoproject.com/en/3.0/ref/models/constraints/

Вы можете использовать CheckConstraintдля принудительного применения один-единственный-ненулевой. Я использую два для ясности:

class Inspection(models.Model):
    InspectionID = models.AutoField(primary_key=True, unique=True)
    GroupID = models.OneToOneField('PartGroup', on_delete=models.CASCADE, blank=True, null=True)
    SiteID = models.OneToOneField('Site', on_delete=models.CASCADE, blank=True, null=True)

    class Meta:
        constraints = [
            models.CheckConstraint(
                check=~Q(SiteID=None) | ~Q(GroupId=None),
                name='at_least_1_non_null'),
            ),
            models.CheckConstraint(
                check=Q(SiteID=None) | Q(GroupId=None),
                name='at_least_1_null'),
            ),
        ]

Это будет только применять ограничение на уровне БД. Вам нужно будет проверить входные данные в ваших формах или сериализаторах вручную.

Как примечание, вы, вероятно, должны использовать OneToOneFieldвместо ForeignKey(unique=True). Тебе тоже захочется blank=True.


0

Я думаю, что вы говорите об общих отношениях , документы . Ваш ответ похож на этот .

Некоторое время назад мне нужно было использовать родовые отношения, но я читал в книге, а где-то еще, что использования следует избегать, я думаю, что это были Two Scoops of Django.

В итоге я создал такую ​​модель:

class GroupInspection(models.Model):
    InspectionID = models.ForeignKey..
    GroupID = models.ForeignKey..

class SiteInspection(models.Model):
    InspectionID = models.ForeignKey..
    SiteID = models.ForeignKey..

Я не уверен, что это хорошее решение, и, как вы упомянули, вы бы не хотели его использовать, но в моем случае это работает.


«Я читаю в книге и где-то еще» о худшей возможной причине что-то делать (или избегать).
Bruno Desthuilliers

@brunodesthuilliers Я думал, что Two Scoops of Django - хорошая книга.
Луис Сильва

Не могу сказать, я не читал это. Но это не имеет отношения: моя точка зрения такова, что если вы не понимаете, почему в книге так говорится, то это не знание или опыт, это религиозная вера. Я не против религиозных убеждений, когда речь идет о религии, но им нет места в CS. Либо вы понимаете, в чем заключаются плюсы и минусы какой-либо функции, и затем вы можете судить, подходит ли она в данном контексте , либо нет, и тогда вы не должны бездумно повторять то, что прочитали. Существуют очень обоснованные варианты использования родовых отношений, суть не в том, чтобы их вообще избежать, а в том, чтобы знать, когда их следует избегать.
Bruno Desthuilliers

NB Я прекрасно понимаю, что никто не может знать все о CS - есть области, где у меня нет других вариантов, кроме как доверять какой-то книге. Но тогда я, вероятно, не буду отвечать на вопросы по этой теме ;-)
bruno desthuilliers

0

Возможно, уже поздно ответить на ваш вопрос, но я подумал, что моё решение может подойти к делу другого человека.

Я хотел бы создать новую модель, назовем ее Dependencyи применить логику в этой модели.

class Dependency(models.Model):
    Group = models.ForeignKey('PartGroup', on_delete=models.CASCADE, null=True, unique=True)
    Site = models.ForeignKey('Site', on_delete=models.CASCADE, null=True, unique=True)

Тогда я написал бы логику, чтобы быть применимой очень явно.

class Dependency(models.Model):
    group = models.ForeignKey('PartGroup', on_delete=models.CASCADE, null=True, unique=True)
    site = models.ForeignKey('Site', on_delete=models.CASCADE, null=True, unique=True)

    _is_from_custom_logic = False

    @classmethod
    def create_dependency_object(cls, group=None, site=None):
        # you can apply any conditions here and prioritize the provided args
        cls._is_from_custom_logic = True
        if group:
            _new = cls.objects.create(group=group)
        elif site:
            _new = cls.objects.create(site=site)
        else:
            raise ValueError('')
        return _new

    def save(self, *args, **kwargs):
        if not self._is_from_custom_logic:
            raise Exception('')
        return super().save(*args, **kwargs)

Теперь вам просто нужно создать сингл ForeignKeyдля вашей Inspectionмодели.

В ваших viewфункциях вам нужно создать Dependencyобъект, а затем назначить его вашей Inspectionзаписи. Убедитесь, что вы используете create_dependency_objectв своих viewфункциях.

Это в значительной степени делает ваш код явным и защищенным от ошибок. Исполнение может быть обойдено слишком легко. Но дело в том, что для того, чтобы обойти это точное ограничение, необходимо предварительное знание.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.