読者です 読者をやめる 読者になる 読者になる

ManyToManyのフィールドを持つモデルの初回save時に、参照先が取得できない問題に対処する。

Python Django


モデル保存時に何かしたい場合は、saveメソッドをオーバーライドすれば大抵のことはできる。
なんだけど、ManyToManyのフィールドを持つ場合はうまくいかない。
それを何とかしようといろいろやってたら、いい方法が見つかったのでメモ。
Python 2.5, Django 0.96, WindowsVista,XP で確認)
※この方法はManipulatorを使っているので、0.96より後のリリースでは動かない可能性が高いです。


これを書いてる人は、「みんなのPython」+「Python 2.4 クイックリファレンス」斜め読み程度のPython力なので、おかしいところがあると思います(実際よく分からないけど、動いてるからいいか的な所があります)。
なのでツッコミお待ちしております。

参考URL

何故うまくいかないか?

上記参考URLでも触れられているが、ManyToManyのフィールドはManipulator.saveメソッドで保存される。
参照元モデルのsaveが行われた後で、参照先との関連がsaveされる。
なので、参照元モデルのsaveが行われる時点では参照先との関連が存在しない。


save2回目以降は参照先との関連も取得できる。上記参考URLの2つ目はそれを利用しているようだ。
ただ、この方法だとビューを書かないと実現できないし、結果として管理サイトが使えなくなる。

じゃあどうするのか?

models.Modelには(Add|Change)Manipulatorというアトリビュートは存在せず、django.db.models.signals.class_preparedイベント発生時に追加している。

#django.db.models.manipulators.py 12-17行目
def add_manipulators(sender):
    cls = sender
    cls.add_to_class('AddManipulator', AutomaticAddManipulator)
    cls.add_to_class('ChangeManipulator', AutomaticChangeManipulator)

dispatcher.connect(add_manipulators, signal=signals.class_prepared)

このイベント発生後、自作Manipulatorに差し替えようとしたけどうまくいかなかった。


このイベントはdjango.db.models.Model._prepareメソッド(192行目)で発生している。
_prepareメソッドは、ModelBaseの__new__メソッド(65行目)で呼ばれている。


ModelBaseはModelのメタクラス?というやつで、クラスの初期化処理を行うものらしい。
ということで、このメタクラスをオーバーライドしてManipulatorのsaveメソッドをラップすることにした。

前提とする仕様

モデルはこんなかんじ。
書籍追加時にタグ付けするイメージで、その時にタギング数を更新する。
タギングの変更は今回考えない。

class Tag(models.Model):
    name = models.CharField(maxlength=200)
    count = models.PositiveIntegerField(default=0, editable=False)
    class Admin:pass
    def __str__(self):
        return "%s:%d" % (self.name, self.count)

class Book(models.Model):
    name = models.CharField(maxlength=200)
    tags = models.ManyToManyField(Tag)
    class Admin:pass
    def __str__(self):
        return self.name

解決策その1

  1. Manipulator.saveのデコレータ?を作る
  2. ModelBaseを継承して、__new__メソッドをオーバーライド。そこでManipulator.saveをデコレータでラップする。
  3. モデルのメタクラスを差し替える。

コードはこんなかんじ。

# -*- encoding: utf-8 -*-
from django.db import models
from django.db.models.base import ModelBase
from django.db.models.manipulators import *
from django.db.models import *

#デコレータ:モデル毎に定義する
def save_decorator(func):
    def save_decorator_main(self, new_data):
        saved = func(self, new_data)
        for tag in saved.tags.all():#ここが実際やりたい処理
            tag.count = tag.book_set.all().count()
            tag.save()
        return saved
    return save_decorator_main

#メタクラス:モデル毎に定義する
class CustomModelBase(ModelBase):
    def __new__(cls, name, bases, attrs):
        meta = super(CustomModelBase, cls).__new__(cls, name, bases, attrs)
        manipulator = getattr(meta, 'AddManipulator')
        if not "save_decorator_main" in str(manipulator.save):#この判定をもうちょっとスマートにしたい・・・
            manipulator.save = save_decorator(manipulator.save)
        return meta

class Book(models.Model):
    __metaclass__ = CustomModelBase #メタクラスを差し替える
    name = models.CharField(maxlength=200)
    tags = models.ManyToManyField(Tag)
    class Admin:pass
    def __str__(self):
        return self.name

解決策その2

解決策その1はかなりアドホックなやり方で、モデル毎にデコレータとメタクラスを作るのでかなり面倒。
何よりも、実行したい処理とそれを実行するための工夫がごっちゃになってて分かり難い。
django.dispatch.dispatcherを使うと、そこが分離できてなかなかいい感じになった。

# -*- encoding: utf-8 -*-
from django.db import models
from django.db.models.base import ModelBase
from django.db.models.manipulators import *
from django.db.models import *
from django.dispatch import dispatcher

#デコレータ:1回定義すればOK
def save_decorator(func):
   def save_decorator_main(self, new_data):
       saved = func(self, new_data)
       #モデル固有の処理用のイベントを発生させる
       dispatcher.send(signal=post_manipulator_save, sender=saved.__class__, instance=saved)
       return saved
   return save_decorator_main

#メタクラス:1回定義すればOK
class CustomModelBase(ModelBase):
   def __new__(cls, name, bases, attrs):
       meta = super(CustomModelBase, cls).__new__(cls, name, bases, attrs)
       manipulator = getattr(meta, 'AddManipulator')
       if not "save_decorator_main" in str(manipulator.save):
           manipulator.save = save_decorator(manipulator.save)
       return meta

post_manipulator_save = object() #シグナル:1回定義すればOK

#モデル固有の処理:モデル毎に定義
def post_book_saved(signal, sender, instance, **kwds):
   for tag in instance.tags.all():
       tag.count = tag.book_set.all().count()
       tag.save()

#モデル固有の処理を登録:モデル毎に定義
dispatcher.connect(
   receiver = post_book_saved,
   signal = post_manipulator_save,
   sender = Book)