読者です 読者をやめる 読者になる 読者になる

dynamic_loader

SQLAlchemy

relationやbackrefで関係を定義するのではなく、dynamic_loaderで定義すると、シーケンス(InstrumentedList)ではなく、Queryオブジェクトで関連オブジェクトを参照できるようになる。

以下は、よくある「著者-著作物」のような多対多の例。

import datetime
from sqlalchemy import *
from sqlalchemy.orm import mapper, relation, backref, create_session

engine = create_engine('mysql://scott:tiger@localhost/test?charset=utf8&use_unicode=0',
                       echo=False,
                       convert_unicode=True)

metadata = MetaData(engine)

author = Table('author', metadata,
               Column('id', Integer, primary_key=True),
               Column('name', Unicode(50), nullable=False),
               mysql_engine='InnoDB',
               mysql_charset='utf8')

book = Table('book', metadata,
             Column('id', Integer, primary_key=True),
             Column('name', Unicode(50), nullable=False),
       Column('publish_date', DateTime, nullable=False,
                    default=datetime.date(1860, 1, 1)),
             mysql_engine='InnoDB',
             mysql_charset='utf8')

author_book_rel = Table('author_book_rel', metadata,
                        Column('author_id', Integer, ForeignKey('author.id'), nullable=False),
                        Column('book_id', Integer, ForeignKey('book.id'), nullable=False),
                        PrimaryKeyConstraint('author_id', 'book_id'),
                        mysql_engine='InnoDB',
                        mysql_charset='utf8')
metadata.create_all()

class Author(object):
    def __init__(self, name):
        self.name = name

class Book(object):
    def __init__(self, name):
        self.name = name

mapper(Author, author,
       properties={'books': relation(Book,
                                     secondary=author_book_rel
                                     ),
                   })

mapper(Book, book,
       properties={'authors': relation(Author,
                                       secondary=author_book_rel
                                       ),
                   })

a = Author(u"Dostoevsky")
session.add(a)

for name in (u"The Possessed", u"Crime And Panishment", u"The Brother Karamazov"):
    a.books.append(Book(name))

session.commit()

通常のrelationを使うと、

a = session.query(Author).filter_by(name="Dostoevsky").first()
books = a.books

とした時に、結果として得られるbooksはPythonシーケンス(DBへのクエリ発行後の結果として得られるオブジェクト群)だけれども、以下のようにdynamic_loaderを使うと、

mapper(Author, author,
       properties={'books': dynamic_loader(Book,
                                     secondary=author_book_rel
                                     ),
                   })

関連オブジェクトをQueryクラスの(サブクラスの)インスタンスとして得られる。すなわち、実際にDBへクエリが発行される前の段階の抽象オブジェクトを取得できる。なので、

# 著者を取得し・・・
author = session.query(Author).filter_by(name="Dostoevsky").first()

# 著者がDostoevskyであるような著作の数を取得
book_count = author.books.count()

# 発表年が1860年以降であるような著作をすべて取得
books_before_1860 = author.books.filter(Book.publish_date<=date(1860, 1, 1)).all()

これは非常に便利。だいぶ前からある機能らしいが、恥ずかしながらZineのコードを読んでから初めて知った。

他にもdynamic_loaderは、SQLAlchemy>=5.0ならば、dynamic_loader関数のquery_class引数でカスタムQueryクラスを指定することができるという特徴もある。これもまた、使えそうな機能なのだが、これに関してはまた別途。