LoginSignup
5
4

More than 5 years have passed since last update.

SQLAlchemyで多態的継承と多対多を組み合わせる

Posted at

背景

似た構成のテーブルがあり、多態性=ポリモーフィズムを活用して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)
5
4
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4