SQLAlchemy
 BPStyle #4
小田切 aodag 篤
 

アクティブレコードとデータマッパー
SQLAlchemy
データマッピング
宣言的な方法
関連
継承
Session
アクティブレコードとデータマッパー

アクティブレコード
クラスとテーブルを1対1に割り当てるマッピング
(オブジェクトと行が対応する)
簡単
融通は利かない
多対多関連ではマッピングされないテーブルが存在する

データマッパー
クラスに対するテーブルマッピングを指定する
柔軟性が高い
ちょっと面倒
多対多関連で関連属性もマッピング可能
SQLAlchemy




 データマッパータイプのORマッパー
 SQLをPythonオブジェクトで構築
 Unit of workパターン
対応データベース

  SQLite
  MySQL
  PostgreSQL
  Firebird
  Oracle
  SQL Server
  DB2
など
データマッピング(1) スキーマ定義

person_table = Table("person", meta,
  Column("person_id", Integer, primary_key),
  Column("first_name", Unicode(20)),
  Column("last_name", Unicode(20)),
  Column("birthday", Date))


要するにテーブル
データマッピング(2) マッピング先

class Person(object):
   """ A person
   """

   def __init__(self, first_name, last_name, birthday):
      self.first_name, self.last_name, self.birthday = first_name,
last_name, birthday


普通のクラス
データマッピング(3) マッピング

person_mapper = mapper(Person, person_table)

Columnがそのままアトリビュートになる
 




    ぶっちゃけめんどくさい
宣言的マッピング

Base = declarative_base()

class Person(Base):
   __tablename__ = 'person'
   person_id = Column(Integer, primary_key=True)
   first_name = Column(Unicode(20)),
   last_name = Column(Unicode(20)),
   birthday = Column(Date))

クラスとスキーマを同時に定義
関連マッピング

class Employee(Base):
   __tablename__ = 'employee'
   id = Column(Integer, primary_key=True)
   .....
   company_id = Column(Integer,
                    ForeignKey('company.id'))

class Company(Base):
   __tablename__ = 'company'
   id = Column(Integer, primary_key=True)
   employees = relation(Employee, backref="company")
多対多

user_keyword_table = Table('user_keyword', meta,
  Column('user_id', ForeignKey('user.id')),
  Column('keyword_id', ForeignKey('keyword.id')))

class User(Base):
   id = Column(Integer, primary_key=True)

class Keyword(Base):
   id = Column(Integer, primary_key=True)
   users = relation(User, backref='keywords',
               secondary=user_keyword_table)
関連属性(1)

class User(Base):
   id = Column(Integer, primary_key=True)
   name = Column(String(255), unique=True)

class Keyword(Base):
   id = Column(Integer, primary_key=True)
   word = Column(String(255), unique=True)
関連属性(2)

class UserKeywords(Base):
   user_id = Column(Integer, ForeignKey('user.id'))
   keyword_id = Column(Integer, ForeignKey('keyword.id'))
   registered = Column(DateTime)
   kw= relation(Keyword, backref="users")
   us = relation(User, backref="keywords")
 

user = User()
user.name = 'aodag'
keyword = Keyword()
keyword.word = 'python'

user_keyword = UserKeyword()
user_keyword.registered = datetime.now()
user_keyword.us = user
user_keyword.kw= keyword
 

user.kw[0].registered
user.kw[0].kw.word
word.us[0].user.name

2HOPするのがうざい
関連属性 AssociationProxy

class User(Base):
   id = Column(Integer, primary_key=True)
   name = Column(String(255), unique=True)
   keywords = association_proxy('kw', 'keyword')

class Keyword(Base):
   id = Column(Integer, primary_key=True)
   word = Column(String(255), unique=True)
   users= association_proxy('us', 'user')
 

user.kw[0].registered
user.keywords[0].word
keyword.users[0].name
 

user.kw[0].keyword.word
-> user.keyword[0].word

keyword.us[0].user.name
-> keyword.users[0].name
継承

RDBの継承実装方法

結合テーブル
スーパータイプのテーブルとサブタイプ固有のデータを持つテーブ
ル

単一テーブル
すべてのサブタイプのデータを含む1テーブル

完全テーブル
サブタイプごとにすべてのデータを持つテーブル
継承(1) スーパークラスの設定

class Person(Base):
   ...
   typename = Column(String(20))
   __mapper_args__ = {'polymorphic_on':'typename'}

タイプフラグのカラムを追加
オプションで、カラムを指定
継承(2) 結合テーブルの場合

class Employee(Person):
   __tablename__ = 'employee'
   employee_id = Column(Integer,
                      ForeignKey('person.person_id'))
   __mapper_args__ = {'polymorphic_identity':'employee'}

サブタイプはテーブルを持つ
スーパータイプのテーブルを参照する外部参照制約
タイプフラグの値を指定
継承(2) 単一テーブル継承

class Employee(Person):
   __mapper_args__ = {'polymorphic_identity':'employee'}

サブタイプはテーブルを持たない
継承(3) 完全テーブル

class Employee(Person):
   __tablename__ = 'employee'
   person_id = Column(Integer, primary_key=True)
   first_name = Column(Unicode(20)),
   last_name = Column(Unicode(20)),
   birthday = Column(Date))
   __mapper_args__ = {'concrete':True}

サブタイプはテーブルを持つ
オプションで完全に別テーブルとする指定
スーパータイプにはタイプフラグが必要ない
カラムを再度定義
Unit of work

データ処理をマーキングして管理
一度にデータベースに反映

メモリ上でのトランザクション処理
SessionとDB接続

DB接続情報
engine = create_engine('sqlite:///')

クラスファクトリ
Session = sessionmaker(bind=engine)

セッションオブジェクト
session = Session()
Session

p = Person(u'篤', u'小田切', datetime(1979, 8, 2))
session.add(p)
p = Person(u'John', u'Doe', datetime(1970, 1, 1))
session.add(p)

session.commit()


新規オブジェクトは、セッションに追加する
Session

p = session.query(Person).filter_by(id=1)
p.birthday = date.today()

session.commit()

p = session.query(Person).filter_by(id=1)
p.delete()
session.commit()

sessionから取り出したオブジェクトはそのまま。
scoped_session

スレッドローカルなモノステートオブジェクト

Session = scoped_session(sessionmaker(bind=engine))
Session.query(...)

直接グローバルオブジェクトでメソッド呼び出しできる。
同一スレッド内であれば、同一セッション。

内部状態のリセットはremoveメソッドで。
Session.remove()
マルチDB

MasterSession = session_maker(bind=create_engine('mysql:
//db1/db')

SlaveSession =
session_maker(bind=create_engine('sqlite://db2/db')

p = Person(...)
s1 = MasterSession()
s1.add()
s1.commit()

s2 = SlaveSession()
s2.query(Person).filter_by(id=1)
まとめ

恐ろしく柔軟
今回紹介したのはSqlAlchemyができることの1/10くらい
その他、
コネクションプロキシ
2フェーズコミット
複数テーブルから1クラスにマッピング
1テーブルから複数クラスにマッピング
クエリからクラスにマッピング
関連の実装クラスをdictやsetに変更
アトリビュート単位での遅延ローディング
垂直分割、水平分割(sharding)の対応
などなど
 


足りないのはadminだけ!
多分FormAlchemyで作れば、それほど問題ないかと。
マイグレーションは、sqlalchemy-migrate

Sql alchemy bpstyle_4