2012年10月4日

Python の ORM 調査:Elixir編

概要

Python の ORM を調査中。最初に自分が良く利用している「Elixir」の使い方から。

Elixirに関して

Elixir」は「SQLAlchemy」のラッパ。気軽にSQLAlchemyの機能を使いたい場合に便利。
SQLAlchemyのラッパなので、オープンソースのデータベースは無論、商用データベースにも多数に対応しているのが特徴。

残念な事にpypi 上の最終リリースが2009-11-16で、最新の SQLAlchemy を利用すると 多少の不具合が存在しているので、手元では改造バージョンを利用したりしている。

ドキュメント

公式サイト」にそれなりにドキュメントが存在する。
部分的に「SQLAlchemy」ドキュメント参照になっている部分があるが、SQLAlchemyのドキュメントは多すぎる。
入門であれば「Elixirのチュートリアル」を見ると大体の機能はわかるはず。

インストール

pipでインストールする。

pip install Elixir

サンプルソース

自分は ORM を利用する場合あまり DDL を発行しないので、DML を中心としたサンプルとする。

今回は MySQL でデータベースとテーブル用意する。

> mysql -u root
mysql> CREATE DATABASE example DEFAULT CHARACTER SET utf8;

mysql> GRANT ALL PRIVILEGES ON example.*
    -> TO username@localhost
    -> IDENTIFIED BY 'password';

mysql> exit;

> mysql -u username -p example

mysql> CREATE TABLE tweets (
    ->  id serial PRIMARY KEY,
    ->  status_id VARCHAR(255) UNIQUE NOT NULL,
    ->  from_user_id VARCHAR(255) NOT NULL,
    ->  text VARCHAR(140) NOT NULL,
    ->  created_at VARCHAR(50) NOT NULL,
    ->  datetime DATETIME NOT NULL
    ->) engine=innodb default charset=utf8;

DML を発行するサンプルは以下。unittest で書いてある。MySQL ドライバはMySQL-pythonを利用する。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import unittest

from datetime import datetime
from elixir import (
    Entity,
    using_options,
    setup_all,
    metadata,
    session,
)


class Tweets(Entity):
    # autoload を True にすると DB から定義を自動読み込み
    using_options(tablename='tweets', autoload=True)


class TestElixir(unittest.TestCase):

    def setUp(self):
        # DB 接続情報 MySQL の場合デフォルトでは MySQL-python ドライバを利用する
        metadata.bind = 'mysql://username:password@localhost/example'
        # 詳細表示
        metadata.bind.echo = True

        setup_all()

    def test_main(self):
        # SELECT
        self.assertEqual(0, Tweets.query.count())

        # INSERT
        # autocommit=False になる
        Tweets(
            status_id='251298602096xxxxx1',
            from_user_id='43172xxx1',
            text='さんぷるでーた',
            created_at='Mon, 1 Oct 2012 12:33:50 +0000',
            datetime=datetime.utcnow(),
        )

        # この場合インスタンスが生成される度に INSERT が発行されるが、COMMITはされない。
        self.assertEqual(1, Tweets.query.count())

        Tweets(
            status_id='251298602096xxxxx2',
            from_user_id='43172xxx2',
            text='さんぷるでーた その 2',
            created_at='Mon, 1 Oct 2012 12:33:50 +0000',
            datetime=datetime.utcnow(),
        )

        # コミットすると確定
        # session.commit()

        # commit しなくても件数は取れる
        self.assertEqual(2, Tweets.query.count())

        self.assertEqual(1, Tweets.query.filter_by(from_user_id='43172xxx1').count())
        self.assertEqual(1, Tweets.query.filter_by(from_user_id='43172xxx2').count())
        self.assertEqual(0, Tweets.query.filter_by(from_user_id='43172xxx3').count())

        # UPDATE
        for tweet in Tweets.query.filter_by(from_user_id='43172xxx2'):
            tweet.from_user_id = '43172xxx3'

        self.assertEqual(0, Tweets.query.filter_by(from_user_id='43172xxx2').count())
        self.assertEqual(1, Tweets.query.filter_by(from_user_id='43172xxx3').count())


        # DELETE
        for tweet in Tweets.query.all():
             tweet.delete()

        self.assertEqual(0, Tweets.query.count())

        # bulk INSERT
        insert_lst = [
            {
                'status_id': '251298602096xxxxx3',
                'from_user_id': '43172xxx3',
                'text': 'さんぷるでーた その 3',
                'created_at': 'Mon, 1 Oct 2012 12:33:50 +0000',
                'datetime': datetime.utcnow(),
            },
            {
                'status_id': '251298602096xxxxx4',
                'from_user_id': '43172xxx4',
                'text': 'さんぷるでーた その 4',
                'created_at': 'Mon, 1 Oct 2012 12:33:50 +0000',
                'datetime': datetime.utcnow(),
            },
        ]
        # Elixir 0.7.1 は以下のロジックだと autocommit=True なので注意
        Tweets.table.insert().execute(insert_lst)


if __name__ == '__main__':
    unittest.main()

まとめ

速度に関しては、「metadata.bind.echo」を「False」にして簡易に計測してみたが、ドライバ直接利用するのに比較すると、やはり遅くなる。また、メモリもそれなりに利用する。
本気で速度を求める場合は、ORMの利用を考慮しない方が良いだろう、

使いごこちとしては、「autoload=True」等の機能が非常に便利。
また、いざとなったら、SQLAlchemy の機能が利用できるので、ほとんどの場合、機能が不足することはない。
SQLAlchemy の機能を利用しているため、UPDATE、DELETEの発行が、SELECT してからでないとできないのは、やや不便かもしれないがORMだと比較的一般的な動作と思われる。
最近メンテナンスされていないは、懸念事項になる。

2012/10/16追記:

この記事は Python のORM 調査の記事の一部となる。以下が関連記事。

blog comments powered by Disqus