SQLAlchemyでリレーション

以下のサンプルでは、

各クラスについてidやテーブル名を定義するのが面倒なのでベースクラスを用意する。

from sqlalchemy.ext.declarative import declarative_base, declared_attr

Base = declarative_base()

class BaseModel(Base):
    # 継承してね
    __abstract__ = True

    # idはすべてのクラスで使うので
    id = Column(Integer, primary_key=True)

    # 例えば、__tablename__ = "user" と同じ
    @declared_attr
    def __tablename__(cls):
        return cls.__name__.lower()

多対一

ユーザーが複数の日記をもっているリレーションの例。

class User(BaseModel):
    name = Column(String)


class Dialy(BaseModel):

    # ForeignKeyはデータベースレベルでのみリレーションを定義する。
    user_id = Column(Integer, ForeignKey("user.id"), nullable=False)
    title = Column(String)

    # オブジェクトレベルでのリレーションを張る
    # order_by はクラス名から書かないと組み込み関数idとかいわれる。
    user = relationship(User, backref=backref("dialies"), order_by='User.id')

def test_many2one():
    engine = create_engine("sqlite:///:memory:", echo=True)
    BaseModel.metadata.create_all(engine)

    Session = sessionmaker(bind=engine)
    sess = Session()
    user = User(name="hoge")
    user2 = User(name="foo")
    sess.add(user)
    sess.add(user2)
    sess.commit()
    d = Dialy(user_id=user.id, title="first")
    sess.add(d)
    user.name = "fuga"
    sess.commit()

    for instance  in sess.query(Dialy).all():
        print(instance.title, instance.user.name)

一対一

ユーザーが専用のプロフィールをもっている例。

class Profile(BaseModel):
    user_id = Column(Integer, ForeignKey("user.id"), nullable=False)
    age = Column(Integer)

    # ProfileからもUserからもuselist=Falseを使うことがポイント
    user = relationship(User, uselist=False, backref=backref("profile", uselist=False))


def test_one2one():
    engine = create_engine("sqlite:///:memory:", echo=True)
    BaseModel.metadata.create_all(engine)
    Session = sessionmaker(bind=engine)
    sess = Session()

    user = User(name="hoge")
    sess.add(user)
    sess.commit()  ## コミットするとidプロパティが暗黙的に定義される

    p = Profile(age=50, user_id=user.id)
    sess.add(p)
    sess.commit()

    for u, p in sess.query(User, Profile).join(Profile, User.id == Profile.user_id).all():
        print(p.user.name, p.user.profile.age)

多対多

ユーザーと行ったことがある国の関係。

user_country_table = Table("user_country", Base.metadata,
    Column("user_id", Integer, ForeignKey("user.id")),
    Column("country_id", Integer, ForeignKey("country.id")),
)

class Country(BaseModel):
    name = Column(String)

    users = relationship("User", secondary=user_country_table, backref="countries")


def test_many2many():
    engine = create_engine("sqlite:///:memory:", echo=True)
    BaseModel.metadata.create_all(engine)
    Session = sessionmaker(bind=engine)
    sess = Session()

    user = User(name="hoge")
    user2 = User(name="foo")

    c1 = Country(name="Japan")
    c2 = Country(name="Russia")

    user.countries.extend([c1, c2])
    user2.countries.extend([c1, c2])

    sess.add(user)
    sess.add(user2)
    sess.commit()

    for u, c in sess.query(User, Country).join("countries").all():
        print(u.name, c.name)

その他Tips

relationは内部でrelationshipを呼んでいるので、

relationshipを直接呼ぶべし。

orm/init.py:112

 def relation(*arg, **kw):
    """A synonym for :func:`relationship`."""

    return relationship(*arg, **kw)