そろそろpythonでもSQLを直に書くのが面倒になってきたので、O/Rマッパーを探してみたところ、幾つか種類があったので有名どころを使ってみることにしました。今回試したのは以下の4つです。
まず用途についてですが、僕はテーブルスキーマはSQLで直に書きますので、ORMでDDLを扱うつもりはありません。DMLを簡単に扱いたいというのが一番の目標です。そこで予め作成して置いたテーブルに対してCRUD操作のし易さを比べてみました。比較に使用したのは以下のテーブルです。
CREATE TABLE `books` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `title` VARCHAR(100) DEFAULT NULL, `price` INT(11) DEFAULT NULL, `isbn` INT(11) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `isbn` (`isbn`) )
まず最初に試したのは、SQLAlchemyです。ちょっと検索した感じでは一番使われてそうな雰囲気です。早速使ってみたところ以下のようになりました。一連の初期化処理でセッションを作成し、セッションを通してオブジェクトの操作を行います。ソースを見ての通りCRUD操作自体は直感的に行えるのですが、マッピングを明示的に行う事や、セッションの管理やDBハンドルの管理などやや煩雑です。ただドキュメントを読む限りでは、非常に高機能で細やかな制御が行えるようです。感想としてはもう少しデータベースを隠蔽して欲しいと思いました。create_engineやget_session等は本来書きたい処理ではありません。
#!/usr/bin/python # -*- coding: utf-8 -*- import sys from sqlalchemy import * from sqlalchemy.orm import * # booksテーブルとマッピングするクラス class Book(object): def __init__(self, title, isbn=None, author=None, price=None): self.title = title self.price = price self.isbn = isbn # セッション(トランザクション)を取得する def get_session(engine): Session = sessionmaker(bind=engine) return Session() def main(): engine = create_engine('mysql://root@localhost/sample', echo=True) # echoでデバッグ出力 meta = MetaData(engine) books = Table('books', meta, autoload=True) # autoloadでテーブル情報を自動ロードする mapper(Book, books) session = get_session(engine) # insert try: book1 = Book('1Q84', price=2000, isbn=1234567) book2 = Book('リアル9', price=630, isbn=9876543) session.add(book1) session.add(book2) session.commit() except: print sys.exc_info() # duplicate error # select session = get_session(engine) for b in session.query(Book).all(): print b # update for b in session.query(Book).all(): b.price *= 1.05 session.commit() # just modify object's value and commit # delete session = get_session(engine) for b in session.query(Book).all(): if b.price > 2000: session.delete(b) session.commit() if __name__ == '__main__': main()
次に試してみたのがSQLObjectです。同じ処理を書いたコードが以下になります。SQLAlchemyと比べてとてもシンプルにまとまっています。セッション周りの煩雑なコードも不要ですし、またテーブル名を指定するだけで、自動的にカラムをロードしてくれるのも便利です。イメージしていたORMとかなり近く、もうこれで良いかなと思ったのですが、1つ気になる情報がありました。TurboGearsというWebフレームワークがバージョン1.xの際に採用していたSQLObjectを、2.xからSQLAlchemyに差し替えた事です。どうも複雑なデータベースを操作する際のパフォーマンスに問題があるというのが理由だそうです。
#!/usr/bin/python # -*- coding: utf-8 -*- import sys from sqlobject import * # 他の処理に先立ってconnectionを作っておく必要がある sqlhub.processConnection = connectionForURI('mysql://root@localhost/sample?debug=1') # booksテーブルにマッピングするオブジェクト class Book(SQLObject): class sqlmeta: table = "books" fromDatabase = True def main(): # insert try: book1 = Book(title='1Q84', price=2000, isbn=1234567) book2 = Book(title='リアル9', price=630, isbn=9876543) except: print sys.exc_info() # duplicate # select for b in Book.select(): print b.title # update for b in Book.select(): b.price = int(b.price * 1.05) # delete for b in Book.select(): if b.price > 2000: Book.delete(b.id) if __name__ == '__main__': main()
正直DBのパフォーマンスが問題になるようなアプリケーションは、まだ作れないのでSQLObjectでも良かったのですが、念のため他のORMを見ていると今度はElixirというモジュールが見つかりました。これはSQLAlchemyのラッパーで、乱暴な言い方をすればSQLAlchemyをSQLObjectの様に扱えるようになります。実際のコードは以下になります。マッピングが自動化されて、セッション管理なども簡潔に記述できるようになっています。
#!/usr/bin/python # -*- coding: utf-8 -*- import sys from elixir import * metadata.bind = 'mysql://root@localhost/sample' metadata.bind.echo = True # booksテーブルにマッピングするオブジェクト class Book(Entity): using_options(tablename='books', autoload=True) def main(): setup_all() # insert try: book1 = Book(title='1Q84', price=2000, isbn=1234567) book2 = Book(title='リアル9', price=600, isbn=9876543) session.commit() except: print sys.exc_info() session.close() # select for b in Book.query.all(): print b # update for b in Book.query.all(): b.price *= 1.05 session.flush() # delete for b in Book.query.all(): if b.price > 2000: b.delete() session.flush() if __name__ == '__main__': main()
これはもうElixirで決まりと思ったのですが、ウノウラボのブログに「PythonのORMを研究してみる(1) 」という記事があり、そこでSQLAlchemyやSQLObjectが選択肢にある中でStormを紹介しますと言っているのが気になって、Stormでも書いてみました。以下がソースになります。オブジェクトのコンストラクタを自分で書かないと駄目なのが気になります。また僕の使い方が良くないのか、データベースに登録された日本語が化けてしまいました。
#!/usr/bin/python # -*- coding: utf-8 -*- import sys from storm.locals import * from storm.tracer import debug class Book(object): __storm_table__ = 'books' id = Int(primary=True) title = Unicode() price = Int() isbn = Int() def __init__(self, title, price, isbn): self.title = title self.price = price self.isbn = isbn def main(): debug(True, stream=sys.stdout) db = create_database('mysql://root@localhost/sample') store = Store(db) book1 = Book(u'1Q84', 2000, 123456) book2 = Book(u'リアル', 600, 987654) # insert try: store.add(book1) store.add(book2) store.commit() except: print sys.exc_info() store.rollback() # select for b in store.find(Book): print b # update for b in store.find(Book): b.price *= 1.05 store.flush() # delete for b in store.find(Book): if b.price >= 2000: store.remove(b) store.flush() if __name__ == '__main__': main()
まとめ
というわけでまとめですが、上の4つでいうとElixirかSQLObjectが使い易いと思いました。オブジェクトとテーブルの自動マッピングが出来るのでコード量がかなり減ります。どちらを使うか迷うのですが、総合判断でElixirを使う事にしました。いざという時にSQLAlchemyの機能にアクセスできるのが、何となく安心だというのが理由です。
| ORM | サンプルコードの行数 | 使い心地 | ドキュメント |
|---|---|---|---|
| SQLAlchemy | 56 | △ | ◎ |
| SQLObject | 38 | ◎ | ◯ |
| Elixir | 42 | ◯ | ◯ |
| Storm | 53 | △ | △ |
本来ならベンチマークを入れるべきところですが、それは又の機会という事で。

