deprecatedなrelationshipを作成する
モデルの持つあるrelationshipをdeprecatedにして、これにアクセスしたら警告を出すようにしたい
relationship?
sqlalchemyのormでforeign keyでつながったテーブルにマッピングされたオブジェクトへの参照を保持する属性のこと。
代表例
MemberGroupがMemberを持つような関係のものなど。 Groupのオブジェクトgが保持するMemberへの参照をmembersとして定義などできる。
import sqlalchemy as sa import sqlalchemy.orm as orm import sqlahelper import transaction engine = sa.create_engine("sqlite://") sqlahelper.add_engine(engine) Base = sqlahelper.get_base() Session = sqlahelper.get_session() class MemberGroup(Base): query = Session.query_property() __tablename__ = "membergroups" id = sa.Column(sa.Integer, primary_key=True) name = sa.Column(sa.Unicode(255)) class Member(Base): query = Session.query_property() __tablename__ = "members" id = sa.Column(sa.Integer, primary_key=True) name = sa.Column(sa.Unicode(255)) group_id = sa.Column(sa.ForeignKey("membergroups.id")) group = orm.relationship("MemberGroup", backref="members") # <- これ def __repr__(self): return u"<%s name=%s>" % (self.__class__.__name__, self.name) Base.metadata.create_all() g = MemberGroup(name="g") g.members.append(Member(name="x")) g.members.append(Member(name="y")) g.members.append(Member(name="z")) Session.add(g) transaction.commit() g = MemberGroup.query.first() print g.members # [<Member name=x>, <Member name=y>, <Member name=z>]
修正
MemberからGroupへの参照を"member_group"で行いたい。一方、既存のコードには"group"で参照を行っている箇所もあるため、 とりあえずは"group"でも動作するようにしたい。ただしgroupをdeprecatedな属性としたい。
期待する振る舞い
groupでアクセスしたとき警告を表示してほしい。
g = Group.query.filter_by(name="group_name").first() members = Member.query.filter(Member.group==g)first() #warning: Member.group is deprecated print members
方法
いくつかの失敗例の後、正しく動作するコードを示す。
失敗例1 deprecated property
relationshipの実体はRelationshipProperty.pythonのpropertyをdeprecatedにしてみる。 propertyはディスクリプタなのでpropertyを持つディスクリプタを定義してあげれば良さそう
import sys import functools class DeprecatedProperty(object): def __init__(self, name, prop): self.name = name self.prop = prop def __get__(self, obj, type_=None): sys.stderr.write("Warning: %s is deprecated attribute\n" % self.name) return self.prop.__get__(obj, type_) class A(object): def __init__(self, x): self._x = x @functools.partial(DeprecatedProperty, "x") @property def x(self): return abs(self._x) print A(-10).x ## Warning: x is deprecated attribute ## 10
sqlalchemyのdeclarativeはclass実行時にRelationshipPropertyからInstrumentAttributeを生成する。 class文が実行された後、Member.groupはInstrumentedAttributeに変わる
print type(Member.group) ## <class 'sqlalchemy.orm.attributes.InstrumentedAttribute'>
なので
class Member(Base): query = Session.query_property() __tablename__ = "members" id = sa.Column(sa.Integer, primary_key=True) name = sa.Column(sa.Unicode(255)) group_id = sa.Column(sa.ForeignKey("membergroups.id")) group = DeprecatedProperty("group", orm.relationship("MemberGroup", backref="members")) ## Member.group # Warning: group is deprecated attribute # AttributeError: 'RelationshipProperty' object has no attribute '__get__' ## またbackrefをたどることができない MemberGroup().members # AttributeError: 'MemberGroup' object has no attribute 'members'
失敗
失敗例2 deprecated decorator(class decorator)
class変更後、InstrumentedAttributeに変わるならクラスデコレータでdeprecationを指定すれば良いのではないかと思う。 以下のようなイメージ
def deprecated_property(propname): def _deprecated_property(cls): setattr(cls, propname, DeprecatedProperty(propname, getattr(cls, propname))) return cls return _deprecated_property @deprecated_property("x") class A(object): def __init__(self, x): self._x = x @property def x(self): return abs(self._x) print A(-10).x # Warning: x is deprecated attribute # 10
警告が表示されるのは片側だけ。
@deprecated_property("group") class Member(Base): query = Session.query_property() __tablename__ = "members" id = sa.Column(sa.Integer, primary_key=True) name = sa.Column(sa.Unicode(255)) group_id = sa.Column(sa.ForeignKey("membergroups.id")) group = orm.relationship("MemberGroup", backref="members") Member().group # Warning: group is deprecated attribute Member.group # Warning: group is deprecated attribute # MemberGroup().members
失敗
失敗例3 sqlalchemyのeventを使う
sqlalchemyにはorm,sessionなどいくつかの段階でeventが発火し結びつけている関数を呼ぶという機能がある。 例えばorm eventのloadなどを使ってみる。
def duplicated_group(target, context): sys.stderr.write("warning: deprecated group") sa.event.listen(MemberGroup, "load", duplicated_group) MemberGroup() # warning: deprecated group
調べてみたところ、属性アクセスに対するイベントはなかったようだった。
成功
考え方を変える。元のpropertyを_xにし、warning付きのpropertyをxで公開する(xは属性名)。 こちらはMemberGroup,Memberの両方のモデルにhybrid_propertyを付けてあげれば、backref側にも警告を表示させることができる
from sqlalchemy.ext.hybrid import hybrid_property class MemberGroup(Base): query = Session.query_property() __tablename__ = "membergroups" id = sa.Column(sa.Integer, primary_key=True) name = sa.Column(sa.Unicode(255)) @hybrid_property def members(self): sys.stderr.write("Warning: members is deprecated property\n") return self._members class Member(Base): query = Session.query_property() __tablename__ = "members" id = sa.Column(sa.Integer, primary_key=True) name = sa.Column(sa.Unicode(255)) group_id = sa.Column(sa.ForeignKey("membergroups.id")) _group = orm.relationship("MemberGroup", backref="_members") @hybrid_property def group(self): sys.stderr.write("Warning: group is deprecated property\n") return self._group MemberGroup().members MemberGroup.members Member().group Member.group # Warning: members is deprecated property # Warning: members is deprecated property # Warning: group is deprecated property # Warning: group is deprecated property