web-dev-qa-db-ja.com

Djangoのカスケード削除動作をオーバーライドするためのオプションは何ですか?

Djangoモデルは通常、ON DELETE CASCADEの動作を非常に適切に処理します(ネイティブでサポートしていないデータベースで機能する方法で)。

ただし、たとえば次のようなシナリオで、適切でない場合にこの動作をオーバーライドする最善の方法を見つけるのに苦労しています。

  • ON DELETE RESTRICT(つまり、オブジェクトに子レコードがある場合、オブジェクトの削除を禁止します)

  • ON DELETE SET NULL(つまり、子レコードを削除せず、親キーをNULLに設定して関係を解除します)

  • レコードが削除されたときに他の関連データを更新する(アップロードされた画像ファイルを削除するなど)

以下は、私が知っているこれらを達成する潜在的な方法です。

  • モデルのdelete()メソッドをオーバーライドします。この種の機能は動作しますが、QuerySetを介してレコードが削除されると、回避されます。また、すべてのモデルのdelete()をオーバーライドして、Djangoのコードが呼び出されず、QuerySetを使用して子オブジェクトを削除する可能性があるため、super()を呼び出せないようにする必要があります。

  • 信号を使用します。これは、モデルを直接削除するとき、またはQuerySetを介して削除するときに呼び出されるため、理想的なようです。ただし、子オブジェクトが削除されるのを防ぐことはできないため、ON CASCADE RESTRICTまたはSET NULLを実装することはできません。

  • これを適切に処理するデータベースエンジンを使用します(この場合、Djangoはどうしますか?)

  • Django=がサポートするまで待ちます(そしてそれまでバグと一緒に生きます...)

最初のオプションが唯一の実行可能なオプションのように見えますが、それは見苦しく、お風呂の水で赤ちゃんを追い出し、新しいモデル/関係が追加されたときに何かを見逃す危険があります。

何か不足していますか?推奨事項はありますか?

57
Tom

この問題に出くわした人たちへの注意点として、Django 1.3。

ドキュメントの詳細を参照してください Django.db.models.ForeignKey.on_delete Fragments of Codeサイトの編集者に指摘していただきありがとうございます。

最も単純なシナリオでは、モデルのFKフィールド定義を追加するだけです。

on_delete=models.SET_NULL
79
blasio

DjangoはCASCADEの動作のみをエミュレートします。

discussion in Django Users Groupによると、最も適切なソリューションは次のとおりです。

  • ON DELETE SET NULLシナリオを繰り返すには-obj.delete()の前に(関連するすべてのモデルに対して)obj.rel_set.clear()を手動で実行します。
  • ON DELETE RESTRICTシナリオを繰り返すには、obj.delete()の前にobj.rel_setが空であることを手動で確認します。
6

OK、次は私が解決した解決策ですが、満足のいくものではありません。

すべてのモデルに抽象基本クラスを追加しました。

class MyModel(models.Model):
    class Meta:
        abstract = True

    def pre_delete_handler(self):
        pass

シグナルハンドラは、このモデルのサブクラスのpre_deleteイベントをキャッチします。

def pre_delete_handler(sender, instance, **kwargs):
    if isinstance(instance, MyModel):
        instance.pre_delete_handler()
models.signals.pre_delete.connect(pre_delete_handler)

各モデルで、子レコードが存在する場合にON DELETE RESTRICTメソッドから例外をスローすることにより、「pre_delete_handler」関係をシミュレートします。

class RelatedRecordsExist(Exception): pass

class SomeModel(MyModel):
    ...
    def pre_delete_handler(self):
        if children.count(): 
            raise RelatedRecordsExist("SomeModel has child records!")

これにより、データが変更される前に削除が中止されます。

残念ながら、削除するオブジェクトのリストは、シグナルが生成される前にDjango=によって既に生成されているため、pre_deleteシグナルのデータを更新することはできません(ON DELETE SET NULLをエミュレートする) Djangoは、循環参照でスタックするのを防ぎ、オブジェクトに不必要に複数回シグナルを送ることを防ぐためにこれを行います。

削除を確実に実行できるようにするのは、呼び出し元コードの責任です。これを支援するために、各モデルにはprepare_delete()または同様の方法でNULLにキーを設定するself.related_set.clear()メソッドがあります。

class MyModel(models.Model):
    ...
    def prepare_delete(self):
        pass

views.pyおよびmodels.pyのコードを変更しすぎることを避けるため、delete()を呼び出すためにprepare_delete()メソッドがMyModelでオーバーライドされます。

class MyModel(models.Model):
    ...
    def delete(self):
        self.prepare_delete()
        super(MyModel, self).delete()

つまり、obj.delete()を介して明示的に呼び出された削除は期待どおりに動作しますが、削除が関連オブジェクトからカスケードされた場合、またはqueryset.delete()を介して行われ、呼び出しコードがすべてを保証していない場合必要に応じてリンクが壊れると、pre_delete_handlerが例外をスローします。

最後に、同様のpost_delete_handlerメソッドをモデルに追加し、post_delete信号で呼び出され、モデルが他のデータをクリアできるようにします(たとえば、ImageFieldsのファイルを削除します) )

class MyModel(models.Model):
     ...

    def post_delete_handler(self):
        pass

def post_delete_handler(sender, instance, **kwargs):
    if isinstance(instance, MyModel):
        instance.post_delete_handler()
models.signals.post_delete.connect(post_delete_handler)

それが誰かの助けになり、コードをあまり手間をかけずに使いやすいものに再スレッド化できることを願っています。

これを改善する方法についての提案は大歓迎です。

4
Tom