Створення моделі з двома необов’язковими, але одним обов’язковим зовнішнім ключем


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та InspectionID, тоді я можу мати кілька "перевірок" у вигляді цих InspectionReportвідносин. Це було зроблено для того, щоб я міг легше сортувати Dateвсі записи, пов’язані з одним Groupабо Site. Сподіваюся, що має сенс
CalMac

@ Cm0295 Боюся, що не бачу сенсу цього рівня непрямості - введення FK / сайту 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(або спеціальне, або за замовчуванням згенероване адміністратором) зробить поле форми необхідним . На рівні схеми db ви можете заповнити обидва, або один, або жоден із цих FK, це було б нормально, оскільки ви зробили ці nb-поля нульовими (з null=Trueаргументом).

Крім того, (див. Інші мої коментарі), ви можете перевірити, чи дійсно ви хочете, щоб FKS були унікальними. Це технічно перетворює відносини один до багатьох на відносини один на один - вам дозволяється лише один єдиний запис "перевірки" для даного GroupID або SiteId (у вас не може бути двох і більше "перевірок" для одного GroupId або SiteId) . Якщо це дійсно те, що ви хочете, ви можете скористатися явним OneToOneField замість цього (схема db буде такою ж, але модель буде більш чіткою і пов'язаний дескриптор набагато більш придатний для цього випадку використання).

Як бічна примітка: у моделі 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. BTW, в Python, умовна назва називає "all_lower_with_underscores" для будь-чого іншого, крім імен класів та псевдоконстант.

Тепер для вашого основного питання: немає специфічного стандартного способу SQL примусового застосування "одного чи іншого" обмеження на рівні бази даних, тому зазвичай це робиться за допомогою обмеження CHECK , яке робиться в моделі Django з мета "обмеженнями" моделі. варіант .

Це говорить про те, як фактично підтримуються та застосовуються обмеження на рівні db, залежить від вашого постачальника баз даних (MySQL <8.0.16, просто проігноруйте їх, наприклад), і вид обмеження, яке вам знадобиться тут , не буде застосовано до форми або валідація рівня моделі , лише при спробі збереження моделі, тому ви також хочете додати валідацію або на рівні моделі (бажано), або перевірку рівня форми, в обох випадках у clean()методі (відповідно) моделі чи формі .

Отже, щоб коротко розповісти:

  • спочатку перевірте, чи дійсно ви хочете цього unique=Trueобмеження, і якщо так, то замініть поле FK на OneToOneField.

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

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

і вам повинно бути все в порядку, якщо припустити, що ваш RDBMS поважає обмеження перевірки.

Зауважте лише, що при такому дизайні ваша Inspectionмодель є абсолютно марною (але дорогою!) Непрямою діяльністю - ви отримаєте такі самі функції за меншу ціну, перемістивши FK (та обмеження, валідацію тощо) безпосередньо в InspectionReport.

Тепер може бути інше рішення - збережіть модель перевірки, але поставте FK як OneToOneField на інший кінець взаємозв'язку (у Сайті та Групі):

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.

Примітка щодо загальних відносин: вони можуть бути зручними, коли у вас дійсно багато можливих пов'язаних моделей та / або не знаєте заздалегідь, які моделі ви хочете пов’язати з вашими. Це особливо корисно для багаторазових програм (думайте, що "коментарі" або "теги" тощо) або розширюваних (рамки управління вмістом тощо). Мінусом є те, що він робить запити набагато важчими (і досить непрактично, коли ви хочете робити запити вручну на своєму db). З досвіду, вони можуть швидко стати ботом wrt / код та перфоманти PITA, тому краще тримати їх, коли немає кращого рішення (та / або коли накладні витрати на технічне обслуговування та час роботи не є проблемою).

Мої 2 копійки.


2

Django має новий (з 2.2) інтерфейс для створення обмежень БД: https://docs.djangoproject.com/en/3.0/ref/models/constraints/

Ви можете використовувати a, 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

Я думаю, ви говорите про загальні стосунки , док . Ваша відповідь схожа на цю .

Десь тому мені потрібно було використовувати загальні стосунки, але я прочитав у книзі та десь ще про те, що цього слід уникати, я думаю, що це дві совки Джанго.

Я в кінцевому підсумку створив таку модель:

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

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

Я не впевнений, що це гарне рішення, і як ви вже згадували, ви краще не будете використовувати його, але в моєму випадку це працює.


«Я читаю в книзі та десь ще» - це про гіршу можливу причину щось робити (або уникати).
bruno desthuilliers

@brunodesthuilliers Я думав, що дві совки Джанго - хороша книга.
Луїс Сільва

Не можу сказати, я не читав. Але це не пов'язано: моя думка полягає в тому, що якщо ти не розумієш, чому книга так говорить, то це не знання, ані досвід, це релігійна віра. Я не проти релігійної віри, коли мова йде про релігію, але вони не мають місця в КС. Або ви розумієте, які плюси і мінуси якоїсь функції, і тоді ви можете судити, чи підходить вона в даному контексті , або ви цього не зробите, і тоді ви не повинні бездумно папугувати тим, що прочитали. Існують дуже вагомі випадки використання для родових відносин, справа в тому, щоб їх взагалі не уникати, а знати, коли їх уникати.
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.