背景
似た構成のテーブルがあり、多態性=ポリモーフィズムを活用してDRYな構成にした。その各テーブルから同一の別テーブルに多対多のリレーションシップを持つ必要が出てきた。
おさらい1
SQLAlchemyではテーブル構成により三種類のポリモーフィズムを利用できる。一つの基底クラスと二つの派生クラスがある場合を想定したテーブル数も記す。
- Single Table Inheritance(単一テーブル継承) 派生するテーブルを全て一枚のテーブルに収める。テーブル数は1。
- Joint Table Inheritance(結合テーブル継承と訳す?) 基底テーブルと派生テーブルが別になる。テーブル数は3。
- Concrete Table Inheritance(個別テーブル継承と訳す?)基底クラスをテーブルに持たず、派生クラスを各テーブルに持つ。テーブル数は2。
外部のデータベース等の関係で、今回はConcrete Table Inheritanceを採用した。
おさらい2
SQLAlchemyでは多対多を中間テーブルを意識せずに扱える。
公式マニュアルにあるサンプルを引用。ブログ記事が複数のキーワードを持つパターン。
post.keywords.append(Keyword('wendy'))
で、どうやる
似た構成のフォームがそれぞれ複数のタグを持つ場合を想定。テーブルとしてはタグ・各フォーム×2、中間テーブルの4つとなる。
# -*- coding: utf-8 -*-
from sqlalchemy import *
from sqlalchemy.ext.declarative import *
from sqlalchemy.orm import *
from sqlalchemy_utils import *
base = declarative_base()
# 中間テーブルはクラスにしなくて良い。
assoc = Table('assoc', base.metadata,
Column('aform_id', Integer, ForeignKey('AForm.id')),
Column('bform_id', Integer, ForeignKey('BForm.id')),
Column('tag_id', Integer, ForeignKey('Tag.id'))
)
class Tag(base):
__tablename__ = 'Tag'
id = Column(Integer, primary_key = True)
name = Column(String)
#直接使わなくともこの定義がないとエラーになる。
#backrefを指定する事で対向の[AB]Formのrelationship定義を省略出来る。
aform = relationship('AForm', secondary = assoc, backref = 'atag')
bform = relationship('BForm', secondary = assoc, backref = 'btag')
class Form(AbstractConcreteBase, base):
id = Column(Integer, primary_key = True)
amount = Column(Integer)
@declared_attr
def __tablename__(cls):
return cls.__name__
@declared_attr
def __mapper_args__(cls):
return {
'polymorphic_identity': cls.__name__,
'concrete':True
}
class AForm(Form, base):
pass
class BForm(Form, base):
b_only = Column(String(10))
db_uri = 'sqlite:////tmp/m2m.sqlite'
engine = create_engine(db_uri, echo =True)
if database_exists(engine.url):
drop_database(db_uri)
create_database(engine.url)
base.metadata.create_all(bind = engine)
Session = sessionmaker(bind = engine)
session = Session()
# AFormにはatag、BFormにはbtagを定義しているので、呼び分ける必要がある。
a = AForm(amount = 100)
atag = Tag(name = 'booked')
a.atag.append(atag)
session.add(a)
f = BForm(amount = 200)
tag = Tag(name = 'canceled')
f.btag.append(tag)
session.add(f)
session.commit()
# 以下で動的に呼び分けることも可能。
getattr(btag, f.__class__.__name__.lower()).append(f)
forms=session.query(AForm).all()
for f in forms:
print(f)
print(f.atag[0].name)
確かに中間テーブルを意識せずに操作はできているが、AFormにはatag、BFormにはbtagを定義しているので、呼び分ける必要がある。但しgetattrを利用して、オブジェクトのクラス名からメソッドの参照を生成し、メソッドチェーンに参加させる事は可能。
getattr(tag, b.__class__.__name__.lower()).append(b)
分かりにくい!こう書けばええやん、という意見もあるだろう。コーディングの流儀と、派生クラスの数に依るだろう。
if isinstance(f, AForm):
tag.aform.append(f)