箱が…

Amazon箱ストラクチャーが崩れてきそうです。ダンボー作ろうかな。

SQLAlchemyのdeclarativeなモデルの継承

SQLAlchemy というPythonのORMライブラリがあります。

こいつでテーブルを定義するには二通りの方法があって、1つは従来の「テーブル定義とそれを表現するオブジェクト(のクラス)を定義した後、2つをマッピングする」もの、もう1つは「declarativeという機能を使って、1つのクラス定義だけでテーブル定義とマッピングができる」ものがあります。

後者(declarative)の方が楽なので好きなんですが、あんまり日本語情報がないのでメモも兼ねてdeclarativeのモデルの継承についてちょっと書いてみたいと思います。

今回使ったSQLAlchemyのバージョンは0.7.5です

declarative_base() で生成したクラスオブジェクトについて

結論から言うと、モデルを書くときはテーブル名・主キーを書かないとSQLAlchemyに怒られます。

以下のコードは必要なものが足りないので怒られます

# -*- coding: utf-8 -*-

from sqlalchemy import Column, String, Integer
from sqlalchemy.ext import declarative

Base = declarative.declarative_base()

class User(Base):
    # __tablename__ = 'users'  # 必要なもの
    id = Column(Integer, primary_key=True) # 主キーもないと怒られる
    name = Column(String(64), nullable=False)

エラーメッセージはこんな感じ

sqlalchemy.exc.InvalidRequestError: Class <class '__main__.User'> does not have a __table__ or __tablename__ specified and does not inherit from an existing table-mapped class.

これはどうやらPythonのabcで提供されているメタクラス、ABCMetaと同じような抽象メンバの再定義を強制する機能を実装しているようです

ただしabcは使っておらず、また __metaclass__ も使ってはいないようです

from sqlalchemy.ext import declarative
Base = declarative.declarative_base()
import inspect
print inspect.getmembers(Base, lambda m: inspect.isclass(m) and issubclass(m, type))
[('__class__', <class 'sqlalchemy.ext.declarative.DeclarativeMeta'>)]

なので、テーブル名・主キーを定義した上でなら、ABCMetaを使った抽象クラスが書けるみたいです(試してない)

モデルの継承

クラスを継承してモリモーフィックにDBアクセスしたいときは、モデルを少し弄ってやらないといけません

サンプルとして以下にコードと出力結果を貼ります

# -*- coding: utf-8 -*-

from sqlalchemy import Column, String, Integer, ForeignKey, create_engine
from sqlalchemy.orm import sessionmaker, relation
from sqlalchemy.ext import declarative

engine = create_engine('sqlite:///:memory:')
Session = sessionmaker(bind=engine)
session = Session()

Base = declarative.declarative_base()

class MailAddress(Base):
    __tablename__ = 'mail_addresses'
    id = Column(Integer, primary_key=True)
    addr = Column(String(256), nullable=False)
    user_id = Column(Integer, ForeignKey('users.id'))

    def __init__(self, addr):
        self.addr = addr

class Person(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(String(64), nullable=False)

    _discriminator = Column(String(32))
    __mapper_args__ = {'polymorphic_on': _discriminator,
                       'polymorphic_identity': 'user'}

    def __init__(self, name):
        self.name = name

class Engineer(Person):
    __mapper_args__ = {'polymorphic_identity': 'engineer'}
    mail_addresses = relation('MailAddress', backref='engineer')

    def __init__(self, name, addr):
        super(Engineer, self).__init__(name)
        self.mail_addresses.append(MailAddress(addr))

Base.metadata.create_all(engine)

john = Person('John Doe')
jane = Engineer('Jane Doe', 'jane@example.com')
session.add_all([john, jane])
session.commit()

def get_mail_addresses(person):
    if hasattr(person, 'mail_addresses'):
        return [a.addr for a in person.mail_addresses]
for p in session.query(Person).all():
    print '%s\'s mail addresses: %s' % \
            (p.name, get_mail_addresses(p))
John Doe's mail addresses: None
Jane Doe's mail addresses: [u'jane@example.com']

クラス Person のインスタンスを取得しようとクエリを投げているにも関わらず、Personのサブクラスである Engineer のインスタンスも同時に取得できています

ここで属性 __mapper_args__ が出てきましたが、これは従来のテーブル・クラスマッピング定義におけるmapper()関数の引数に相当します

Person のpolymorphic_onにはpolymorphic_identityを入れる属性を指定し、
polymorphic_identityには型を区別するための識別子の文字列を指定します

このへんの(declarativeでない方法による)詳しい説明はドキュメントの和訳か、本家のdeclarativeのドキュメントに書かれているので参考にしてください