2つのオプションで1つの必須の外部キーを持つモデルの作成


9

私の問題は、2つの外部キーのいずれかを使用して、どのようなモデルであるかを示すことができるモデルがあることです。両方ではなく、少なくとも1つは取得したい。これを1つのモデルのままにすることはできますか、それとも2つのタイプに分割する必要がありますか?これがコードです:

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モデルです。これはグループまたはサイトのいずれかにリンクする必要がありますが、両方にリンクすることはできません。現在、この設定では両方が必要です。

私はむしろ2つの、ほぼ同一のモデルにこれを分割する必要はないと思いますGroupInspectionし、SiteInspection一つのモデルとして、それを続けて任意のソリューションが理想的であるので、。


おそらく、サブクラスを使用する方が良いでしょう。Inspectionクラスを作成しSiteInspectionGroupInspectionから、共通部分にサブクラスを作成できます。
Willem Van Onsem

おそらく無関係ですが、unique=TrueFKフィールドの部分は、1つのInspectionインスタンスGroupIDまたはSiteIDインスタンスに対して存在できるインスタンスが1つだけであることを意味します。つまり、1対1の関係であり、1対多の関係ではありません。これは本当にあなたが欲しいものですか?
ブルーノdesthuilliers

「現在、このセットアップでは両方が必要です。」=>技術的にはそうではありません-データベースレベルでは、両方のキーを設定することも、どちらのキーも設定しないこともできます(上記の警告を参照)。(直接またはdjango管理を介して)ModelFormを使用する場合にのみ、これらのフィールドが必須としてマークされます。これは、 'blank = True'引数を渡していないためです。
ブルーノdesthuilliers

@brunodesthuilliersはい、アイデアはor とのInspection間にリンクを持つことであり、その1つの関係の形で複数の「検査」を行うことができます。これは、1つまたはに関連するすべてのレコードをより簡単にソートできるようにするために行われました。それが理にかなっているとGroupSiteInspectionIDInspectionReportDateGroupSite
期待

@ Cm0295私はこの間接参照レベルのポイントがわからないようです-グループ/サイトのFKを直接InspectionReportに入れると、まったく同じサービスAFAICTが生成されます-適切なキーで検査レポートをフィルタリングします(またはサイトまたはグループ)、日付順に並べ替えれば完了です。
ブルーノdesthuilliers

回答:


5

Djangoの方法でそのような検証を行うことをお勧めします

cleanDjangoモデルのメソッドをオーバーライドする

class Inspection(models.Model):
    ...

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

ただし、Model.full_clean()と同様に、モデルのsave()メソッドを呼び出しても、モデルのclean()メソッドは呼び出されないことに注意してください。モデルのデータを検証するには手動で呼び出す必要があります。またはモデルのsaveメソッドをオーバーライドして、Modelクラスのsaveメソッドをトリガーする前に常にclean()メソッドを呼び出すようにすることもできます。


複数のテーブルに関連するポリモーフィックフィールドを提供するためにGenericRelationsを使用するのが役立つ可能性のある別のソリューションですが、これらのテーブル/オブジェクトを最初からシステム設計で交換可能に使用できる場合があります。


2

コメントで言及されているように、「この設定では両方が必要です」という理由はblank=True、FKフィールドにを追加するのを忘れたためです。そのため、ModelFormカスタム(または管理者が生成したデフォルト)によってフォームフィールドが必須になります。dbスキーマレベルでは、これらのFKのいずれか1つを入力することも、まったく入力しないこともできます。これらのdbフィールドを(null=True引数を使用して)null可能にしたので問題ありません。

また、(他のコメントを参照)、FKが本当に一意であることを確認したい場合があります。これにより、技術的には1対多の関係が1対1の関係に変わります-特定のGroupIDまたはSiteIdに対して単一の「検査」レコードのみが許可されます(1つのGroupIdまたはSiteIdに対して2つ以上の「検査」を持つことはできません) 。それが本当に必要な場合は、代わりに明示的なOneToOneFieldを使用することをお勧めします(dbスキーマは同じですが、モデルはより明示的で、関連する記述子はこのユースケースではるかに使いやすくなります)。

補足:Djangoモデルでは、ForeignKeyフィールドは、未加工のIDではなく、関連するモデルインスタンスとして実体化します。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解決さfoofoo.idます。したがって、あなたのInspectionIDand SiteIDフィールドの名前を適切なinspectionand に変更したいのは確かです site。ところで、Pythonでは、クラス名と疑似定数以外の命名規則は「all_lower_with_underscores」です。

核心的な質問です。データベースレベルで「いずれか」の制約を適用する特定の標準SQLの方法はないため、通常は、モデルのメタ「制約」を持つDjangoモデルで行われるCHECK制約を使用して行われます。オプション

つまり、DBレベルで実際に制約がサポートおよび適用される方法は、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()

これにより、特定の制約や検証を追加する必要がなくなりますが、間接参照レベル(SQL join句など)が追加されます。

これらのソリューションのどれが「最適」であるかは、実際にはコンテキストに依存するため、両方の影響を理解し、モデルを通常どのように使用して自分のニーズにより適しているかを確認する必要があります。私に関する限り、より多くのコンテキストがない(または疑わしい)場合は、より間接的なレベルではなくYMMVのソリューションを使用します。

一般的な関係についての注意:これらは、実際に関連するモデルがたくさんある場合や、自分のモデルにどのモデルを関連付けたいか事前にわからない場合に便利です。これは、再利用可能なアプリ(「コメント」や「タグ」などの機能を考える)または拡張可能なアプリ(コンテンツ管理フレームワークなど)で特に役立ちます。欠点は、クエリがはるかに重くなる(そして、dbで手動クエリを実行する場合はむしろ非現実的です)ことです。経験から、それらはすぐにPITAボットのwrt /コードとperfsになる可能性があるため、より良いソリューションがない場合(および/またはメンテナンスとランタイムのオーバーヘッドが問題ではない場合)に保持する方が良いでしょう。

私の2セント。


2

Djangoには、DB制約を作成するための新しい(2.2以降)インターフェースがあります:https : //docs.djangoproject.com/en/3.0/ref/models/constraints/

を使用して、CheckConstraint1つだけをnull以外に強制できます。わかりやすくするために2つ使用します。

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'),
            ),
        ]

これにより、DBレベルでのみ制約が適用されます。フォームまたはシリアライザーの入力を手動で検証する必要があります。

補足として、おそらくのOneToOneField代わりに使用する必要がありForeignKey(unique=True)ます。あなたも欲しいでしょうblank=True


0

私はあなたが一般的な関係ドキュメントについて話していると思います。あなたの答えは次のように見えます、この1

あるとき、私はGeneric Relationsを使用する必要がありましたが、本を読んだり、他の場所では使用を避けるべきだと思っていました。それは、Djangoの2つのスクープだったと思います。

私はこのようなモデルを作成することになりました:

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

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

それが良い解決策であるかどうかはわかりませんし、あなたが言ったようにあなたはそれを使いたくないのですが、これは私の場合うまくいきます。


「私は本や他の場所で読んだ」とは、何かを行う(または行うのを避ける)ために考えられるより悪い理由についてです。
bruno desthuilliers

@brunodesthuilliers Djangoの2つのスクープは良い本だと思いました。
Luis Silva

わかりません、まだ読んでいません。しかし、それは無関係です:私の要点は、あなたが本がそう言う理由を理解しなければ、それは知識でも経験でもない、それは宗教的信念であるということです。私は宗教に関しては信仰を気にしませんが、彼らはCSでの立場はありません。いくつかの機能の長所と短所を理解し、特定のコンテキストでそれが適切かどうかを判断できます。そうでない場合は、読んだ内容を無意識にオウムしないでください。一般的な関係には非常に有効なユースケースがあり、それらをまったく回避するのではなく、いつ回避するかを知ることがポイントです。
ブルーノdesthuilliers

注意:私はCSについてすべてを知ることはできないことを完全に理解しています。ある本を信頼する以外に選択肢がないドメインがあります。しかし、私はおそらくそのトピックに関する質問には答えません;-)
ブルーノ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)

次にForeignKeyInspectionモデルにシングルを作成する必要があります。

あなたではview機能、あなたが作成する必要があるDependencyオブジェクトをしてからに割り当てInspectionたレコード。あなたが使用していることを確認してくださいcreate_dependency_object、あなたにview機能しています。

これにより、コードが明確になり、バグの証拠になります。強制は非常に簡単に回避できます。しかし、要点は、この正確な制限を回避するには事前の知識が必要であるということです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.