つーことで、PyCon APAC2013のCFP没ネタの2つ目。
インストール
特にめんどうなことなくpipでインストールできます。
$ pip install sqlalchemy
モデル定義
テーブル定義とクラス定義をして、それらをマッピングするのが、データマッパーの本来の方法ですが、sqlalchemy.ext.declarative を使うのが圧倒的に楽です。 これを使うとテーブル定義をクラス内で行い、自動でマッピングまで行ってくれます。 declarative_baseでベースクラスを定義して、それを継承してモデルを定義します。:
from sqlalchemy import (
Column,
Integer,
ForeignKey,
)
from sqlalchemy.orm import (
relationship,
scoped_session,
sessionmaker,
)
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
DBSession = scoped_session(sessionmaker())
class BankAccount(Base):
__tablename__ = 'bankaccount'
query = DBSession.query_property()
id = Column(Integer, primary_key=True)
balance = Column(Integer, default=0)
bank_id = Column(Integer, ForeignKey('bank.id'))
bank = relationship('Bank', backref='accounts')
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
self.balance -= amount
class Bank(Base):
__tablename__ = 'bankaccount'
query = DBSession.query_property()
id = Column(Integer, primary_key=True)
データ作成とかクエリとか
新規作成は作ったオブジェクトを sessionにaddします。:
bank = Bank() DBSession.add(bank) DBSession.commit()
DBSessionはaddされたオブジェクトを flush メソッドが呼ばれたときに INSERT します。 また、commitメソッドでDBにcommitします。このときflushが必要だったときに自動でflushを呼びます。
既にDBSession管理下のオブジェクトに新規オブジェクトを関連づけても、INSERTされます。:
bank.accounts.append(BankAccount()) DBSession.commit()
モデルを取得する場合は、DBSessionのqueryメソッドを使用します。 クエリには、filterメソッドで条件を追加できます。:
DBSession.query(BankAccount) DBSession.query(BankAccount).filter(BankAccount.balance>=100)
queryメソッドはクエリを作成するだけで、実際に取得させるには、allやone, firstなどのメソッドを呼ぶか、 for文などでイテレータとして評価させます。:
DBSession.query(BankAccount).all()
DBSession.query(BankAccount).first()
DBSession.query(BankAccount).one()
for account in DBSession.query(BankAccount):
account.withdraw(199)
また、DBSession.query_propertyで、そのクラスのクエリを生成するプロパティを作成できます。(上記の例でも定義してあります。):
BankAccount.query.filter(BankAccount.balance>=50).all()
プラクティス
テストとかの注意
view内でcommitすると、テスト中でもデータベースが必要になります。 また、テスト後にrollbackしてもテストで作成したデータが消えないため他のテストに影響がでます。
SQLAlchemyは、SessionがUnitOfWorkパターンを用いて更新情報を管理しているので、モデルの状態変更だけを気にするようにしましょう。
commit,rolbackは、wsgiミドルウェアやpyramidのtweenなどの仕組みを活用します。 zope.sqlalchemyのZopeTransactionExtensionを使うと、repoze.tm2やpyramid_tmでトランザクション管理ができます。 SQLAlchemyだけでよい場合は以下のようなwsgiミドルウェアが利用できます。:
class SQLATransactionMiddleware(object):
def __init__(self, app, dbsession):
self.app = app
self.dbsession = dbsession
@wsgify
def __call__(self, request):
try:
request.get_response(self.app)
self.dbsession.commit()
except Exception as e:
self.dbsession.abort()
six.reraise()
finally:
self.dbsession.remove()
primaryjoinやsecondaryjoinの条件を活用する
relationshipではForeignKeyを自動で結合条件として認識しますが、primaryjoinやsecondaryjoinを使ってさらに条件を追加できます。 たとえば、BlogとBlogEntryのような関係があった場合に、publicフラグがTrueのものだけを必要とする場合を考えてみましょう。:
class Blog(Base):
__tablename__ = 'blogs'
id = Column(Integer, primary_key=True)
title = Column(UnicodeText)
entries = relationship('BlogEntry', backref="blog")
public_entries = relationship('BlogEntry', primaryjoin='and_(Blog.id==BlogEntry.blog_id, BlogEntry.published!=None)')
class BlogEntry(Base):
__tablename__ = 'blog_entries'
id = Column(Integer, primary_key=True)
title = Column(UnicodeText)
published = Column(DateTime)
blog_id = Column(Integer, ForeignKey('blogs.id'))
entriesにはprimaryjoinがないので、 BlogEntryのblog_idの外部キーを自動で条件に採用します。 public_entriesでは、primaryjoinで条件を指定しています。blog_idの条件およびpublishedが設定されているものが、public_entriesで取得されるようになります。
1テーブル 1クラス という妄想を捨てる
SQLAlchemyはデータマッパーなので、複数テーブル複数クラスなどのマッピングもできます。(Djang○ ORM とは違うのだよ) declarative_baseを使っているとクラス内でテーブル構成を定義するのでついつい1テーブル1クラスという考えになってしまいがちです。 複数テーブルを結合した集計サマリーなどのクエリをviewに書くような原始的な開発はもうやめましょう。
class Customer(Base):
__tablename__ = 'customers'
id = Column(Integer, primary_key=True)
name = Column(UnicodeText)
class Order(Base):
__tablename__ = 'orders'
id = Column(Integer, primary_key=True)
amount = Column(Integer)
customer_id = Column(Integer, ForeignKey('customers.id'))
customer = relationship('Customer', backref="orders")
order_summary = select([func.sum(Order.__table__.c.amount).label('amount'),
Order.__table__.c.customer_id]
).group_by(
Order.__table__.c.customer_id
).alias()
class CustomerOrder(Base):
__table__ = Customer.__table__.join(order_summary)
CustomerOrder は __tablename__ ではなく __table__を使っています。 このクラスには、customersと、orderを集計した結果を結合したものをマッピングしています。 喜んでください!みなさんが大好きな宣言的な書き方ですよ!
Comments !