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