ラベル database の投稿を表示しています。 すべての投稿を表示
ラベル database の投稿を表示しています。 すべての投稿を表示

2012年10月10日

Python の ORM 調査:結論

概要

ここ数日Python の ORM を調査した結論。

結論

peewee」お勧め。よほど複雑なことをしないならこれで十分。
peewee」で機能が不足するレベルだと、「SQLAlchemy」以外の選択肢は無い。

「SQLObject」と「storm」は「peewee」の出きが良いので、調査する気力がなくなった。

2012/10/16追記:

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

2012年10月9日

Python の ORM 調査:SQLAlchemy Core編

概要

参考に「SQLAlchemy Core」のサンプルを書いてみる。
ORM に関しては昨日のエントリを参照。

サンプル

良い書き方でない部分もあるが、昨日の ORM とほぼ同じことをやっているサンプルが以下。SQLを細かく制御したい場合用。

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

from datetime import datetime
import sqlalchemy as sa
import sqlalchemy.dialects.mysql as samy


class TestSqlAlchemy(unittest.TestCase):

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

        metadata = sa.MetaData()
        self.tweets = sa.Table(
            'tweets', metadata,
            sa.Column('id', samy.BIGINT(unsinged=True), primary_key=True),
            sa.Column('status_id', sa.VARCHAR(length=255)),
            sa.Column('from_user_id', sa.VARCHAR(length=255)),
            sa.Column('text', sa.VARCHAR(length=140)),
            sa.Column('created_at', sa.VARCHAR(length=50)),
            sa.Column('datetime', sa.DATETIME)
        )

        self.conn = engine.connect()

    def test_main(self):
        ### COUNT
        # Core 版
        count_sql = sa.select(
            [sa.func.count(self.tweets.c.id).label('count')])
        self.assertEqual(0, self.conn.execute(count_sql).fetchone()['count'])

        ### INSERT
        sql = self.tweets.insert().values(
            status_id='251298602096xxxxx1',
            from_user_id='43172xxx1',
            text='さんぷるでーた',
            created_at='Mon, 1 Oct 2012 12:33:50 +0000',
            datetime=datetime.utcnow(),
        )
        # トランザクション制御をしないと COMMIT する
        self.conn.execute(sql)

        self.assertEqual(1, self.conn.execute(count_sql).fetchone()['count'])

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

        # トランザクション制御
        trans = self.conn.begin()

        self.conn.execute(sql)
        self.assertEqual(2, self.conn.execute(count_sql).fetchone()['count'])
        # ROLLBACK
        trans.rollback()
        # COMMIT は trans.commit()
        self.assertEqual(1, self.conn.execute(count_sql).fetchone()['count'])

        # commit か rolleback を発行するとトランザクション制御が終了する
        # 制御終了後 SQL を発行すると COMMIT する
        self.conn.execute(sql)

        ### UPDATE

        # UPDATE 前件数確認
        sql = self.tweets.select().where(
            self.tweets.c.from_user_id == '43172xxx2')
        ret = self.conn.execute(sql).fetchall()
        self.assertEqual(1, len(ret))
        sql = self.tweets.select().where(
            self.tweets.c.from_user_id == '43172xxx3')
        ret = self.conn.execute(sql).fetchall()
        self.assertEqual(0, len(ret))

        # UPDATE
        sql = self.tweets.update().where(
            self.tweets.c.from_user_id == '43172xxx2').values(
                from_user_id='43172xxx3')
        self.conn.execute(sql)

        # UPDATE 後件数確認
        sql = self.tweets.select().where(
            self.tweets.c.from_user_id == '43172xxx2')
        ret = self.conn.execute(sql).fetchall()
        self.assertEqual(0, len(ret))
        sql = self.tweets.select().where(
            self.tweets.c.from_user_id == '43172xxx3')
        ret = self.conn.execute(sql).fetchall()
        self.assertEqual(1, len(ret))

        # DELETE
        self.conn.execute(self.tweets.delete())

        # 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(),
            },
        ]

        self.conn.execute(self.tweets.insert(), insert_lst)

        # SELECT
        sql = sa.select([self.tweets])
        ret = self.conn.execute(sql)
        for row in ret:
            print row
        ret.close()

        # DELETE
        self.conn.execute(self.tweets.delete())


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

まとめ

Core を直接利用するのはそれなりの理由がないとやらないと思われる。通常は ORM の機能を利用すれば良い。

2012/10/16追記:

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

2012年10月8日

Python の ORM 調査:SQLAlchemy編

概要

Python の ORM を調査中。Python の ORM としてほとんど標準の的扱いの「SQLAlchemy」の最新版を調査してみる。

SQLAlchemyに関して

SQLAlchemy」は Python の ORM ではおそらく最も高機能。
ドキュメントの充実度はかなりの物。
対応しているデータベースの数が多いのも特徴で、オープンソースのRDBだけでなく、商用データベースに関してもサポートしている。
また、各データーベースに関してデータベースドライバを数種類サポートしているのも特徴。
かなり高機能だったり複雑なアプリケーションでも、これを覚えておくと問題なく対応できる。

ドキュメント

SQLAlchemy 0.7 Documentation」から対象のドキュメントを見ることができる。
SQLAlchemy は Core と ORM が存在し、Core は SQL をラッピングした文法、ORM が データベースをオブジェクトして利用するための部分になる。
ドキュメントの量が多いが、とりあえず普通に使うだけなら「Object Relational Tutorial」を見るのが良い。

インストール

pipでインストールする。

pip install SQLAlchemy

サンプルソース

自分は 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 sqlalchemy import (
    create_engine,
    Column,
    VARCHAR,
    DATETIME,
)
from sqlalchemy.dialects.mysql import BIGINT
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()


class Tweets(Base):
    __tablename__ = 'tweets'

    # unsinged=True を利用する場合
    # sqlalchemy.dialects.mysql.BIGINT を利用する
    id = Column(BIGINT(unsinged=True), primary_key=True)
    # length は必須ではない
    status_id = Column(VARCHAR(length=255))
    from_user_id = Column(VARCHAR(length=255))
    text = Column(VARCHAR(length=140))
    created_at = Column(VARCHAR(length=50))
    datetime = Column(DATETIME)

    # __init__ や __repr__ を定義すると便利だが、省略


class TestSqlAlchemy(unittest.TestCase):

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

        # session を作成する
        # session は面倒なようで、用途によっては非常にありがたかったりする
        Session = sessionmaker(bind=engine)
        self.session = Session()

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

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

        # インスタンスを生成しただけでは SQL 文は発行されない
        self.assertEqual(0, self.session.query(Tweets).count())

        # session に追加
        self.session.add(tweet)

        # add で SQL 文は発行されるが COMMIT はされない
        # デフォルトは autocommit=False
        self.assertEqual(1, self.session.query(Tweets).count())

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

        self.assertEqual(2, self.session.query(Tweets).count())

        # COMMIT を発行して確定
        self.session.commit()

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

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

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

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

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

        # COMMIT を発行して確定
        self.session.commit()

        # 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(),
            },
        ]

        # 実行(ORMの場合は以下の書き方をする。Core だともっと綺麗に書ける)
        self.session.execute(Tweets().__table__.insert(), insert_lst)

        self.assertEqual(2, self.session.query(Tweets).count())

        # COMMIT しないと ROLLBACK

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

まとめ

決して使い易いとは言えないが、用途によっては本当に便利。
また、Oracleなど商用データベースをPythonから利用する場合は、実質選択肢がこれしかない。
目的をちゃんと考えて使うと良い。

Core と ORM が存在するので、上手く使い分ける必要がある。
今回のサンプルだと bulk insert の部分を ORM だけで書いているため、かなり無理矢理な感じになっているが、Core を利用すると bulk insert はもうすこし綺麗に書ける。

速度は用途によって多少異なるが、ORM だけの利用でも結構速い。ただ本気で速度を求める場合は Core の機能を利用した方が良いかもしれない。

今回のサンプルを書くために、多少ネットの情報を検索したが、古い情報が多い。
しかも、Core と ORM の違いを理解していない解説が多い印象。
ちゃんと使いこなすには、本家のドキュメントを読む必要がある。

2012/10/16追記:

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

2012年10月7日

Python の ORM 調査:Django編

概要

Python の ORM を調査中。今回はWebアプリケーション作成で比較的利用されている「Django」内蔵のORMを調査してみる。

Djangoに関して

Django」はPythonで代表的なフルスタックフレームワーク。
結構なんでも揃い、かつプラグイン機構が充実しているので、拡張もしやすい。
ライセンスは BSD。Python 3 には今の所対応していない。
対応RDBは MySQL、Oracle、PostgreSQL、SQLite。

本来は ORM だけ独立で使うための物ではないかもしれないが、今回は ORM だけを調査したい。

ドキュメント

Django ドキュメント」から日本語のドキュメントが読める。
今回のようにORMだけ独立で利用したい場合等は公式ドキュメント(英語)の「Writing custom django-admin commands」に書いてある。使い方は昨日書いたので参照。

インストール

pip でインストールする。

pip install django

サンプルソース

自分は 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;

次に付属のツール「django-admin.py」を利用してプロジェクトの雛形を作成する。
「django-admin.py」は pip 等でインストールすると、適切な場所にインストールされるのでパスを通しておくと良い。 以下のように利用する。

django-admin.py startproject mysite

データベースの設定は「settings.py」で行なう。HOST、PORT は初期設定のままなので、今回は空にしておく。

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql.',
        'NAME': 'example',
        'USER': 'username',
        'PASSWORD': 'password',
        'HOST': '',
        'PORT': '',
    }
}

「settings.py」には他にも以下のような設定をした。

TIME_ZONE = 'Asia/Tokyo'
LANGUAGE_CODE = 'ja-JP'
USE_TZ = False

次にアプリケーションを作成する。

python manage.py startapp example

「management/commands」ディレクトリを作成する。

mkdir -p example/management/commands
touch example/management/__init__.py
touch example/management/commands/__init__.py

「settings.py」の「INSTALLED_APPS」にアプリケーションを有効化するために追加する。

INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # 以下を追加
    'example',
)

「models.py」を以下のように記述。(実際には __unicode__ 等を書くが省略)

from django.db import models


class Tweets(models.Model):
    status_id = models.CharField(max_length=255)
    from_user_id = models.CharField(max_length=255)
    text = models.CharField(max_length=140)
    created_at = models.CharField(max_length=50)
    datetime = models.DateTimeField()

    class Meta:
        db_table = 'tweets'

「example/management/commands」以下にスクリプトを作成。sample.py」の名前で以下のように作成。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from django.core.management.base import BaseCommand

from datetime import datetime
from example.models import Tweets


class Command(BaseCommand):

    def handle(self, *args, **options):
        # 基本的に autocommit=True

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

        #  インスタンスの生成では INSERT しない
        self.assertEqual(0, Tweets.objects.count())

        # save メソッドで INSERT を発行
        tweet.save()

        self.assertEqual(1, Tweets.objects.count())

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

        self.assertEqual(2, Tweets.objects.count())


        # UPDATE 前の確認
        self.assertEqual(1, Tweets.objects.filter(
            from_user_id='43172xxx1').count())
        self.assertEqual(1, Tweets.objects.filter(
            from_user_id='43172xxx2').count())
        self.assertEqual(0, Tweets.objects.filter(
            from_user_id='43172xxx3').count())

        # UPDATE
        tweet = Tweets.objects.get(from_user_id='43172xxx2')
        tweet.from_user_id = '43172xxx3'

        # COMMIT
        tweet.save()

        # UPDSTE 後の確認
        self.assertEqual(0, Tweets.objects.filter(
            from_user_id='43172xxx2').count())
        self.assertEqual(1, Tweets.objects.filter(
            from_user_id='43172xxx3').count())

        # bulk INSERT
        insert_lst = [
            Tweets(
                status_id='251298602096xxxxx3',
                from_user_id='43172xxx3',
                text=u'さんぷるでーた その 3',
                created_at='Mon, 1 Oct 2012 12:33:50 +0000',
                datetime=datetime.utcnow(),
            ),
            Tweets(
                status_id='251298602096xxxxx4',
                from_user_id='43172xxx4',
                text=u'さんぷるでーた その 4',
                created_at='Mon, 1 Oct 2012 12:33:50 +0000',
                datetime=datetime.utcnow(),
            ),
        ]
        Tweets.objects.bulk_create(insert_lst)

        self.assertEqual(4, Tweets.objects.count())

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

        # DELETE 確認
        self.assertEqual(0, Tweets.objects.count())

    def assertEqual(self, first, second):
        """
        テスト用の簡易関数
        """
        if first == second:
            print '・',
        else:
            print 'NG',

実行は以下のようにする。

python manage.py sample

まとめ

DjangoのORMは確かに良くできている。Djangoを普段利用している人には便利だろう。
ただ、速度はあまり早くないので、速度をそれほど必要としないような管理ツール等に向いていると思われる。

2012/10/16追記:

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

2012年10月5日

Python の ORM 調査:peewee編

概要

Python の ORM を調査中。今回は小さいサイズで使いやすそうな「peewee」を調査してみる。

peeweeに関して

peewee」は「a little orm」と記述されているように、小さいサイズのORM。
ライセンスはMIT。Python 3 には今の所対応していない。
対応RDBは、PostgreSQL、MySQL、SQLite3で、商用データベースには対応していない。

ドキュメント

peewee documentation」にドキュメントがある。機能は網羅されたドキュメントになっている。
とりあず入門するなら「Peewee Cookbook」を読むと良い。

インストール

pipでインストールする。

pip install peewee

サンプルソース

自分は 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;

次に付属のツール「pwiz.py」を利用してテーブルと関連したクラスを生成する。「pwiz.py」は pip 等でインストールすると、適切な場所にインストールされるのでパスを通しておくと良い。
以下のように利用する。

pwiz.py -H localhost -u username -P password -e mysql example > example_model.py

以下のようなソースが生成されるが、気に入らない点がある。

  • 一部の列名を勝手に省略する
  • 自動生成ソースが pep8 に準拠してない

特に列名の変更は、結構面倒。抑止するオプションは無いみたい。

from peewee import *

database = MySQLDatabase('example', **{'passwd': 'password', 'host': 'localhost', 'user': 'username'})

class UnknownFieldType(object):
    pass

class BaseModel(Model):
    class Meta:
        database = database

class Tweets(BaseModel):
    created_at = CharField()
    datetime = DateTimeField()
    from_user = CharField(db_column='from_user_id')
    id = BigIntegerField()
    status = CharField(db_column='status_id')
    text = CharField()

    class Meta:
        db_table = 'tweets'

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

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

from datetime import datetime
from example_model import (
    database,
    Tweets,
)


class TestPeewee(unittest.TestCase):

    def setUp(self):
        database.connect()
        # デフォルトは autocommit=True
        database.set_autocommit(False)

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

        # INSERT
        # 日本語に u 付ける
        tweet = Tweets(
            status='251298602096xxxxx1',
            from_user='43172xxx1',
            text=u'さんぷるでーた',
            created_at='Mon, 1 Oct 2012 12:33:50 +0000',
            datetime=datetime.utcnow(),
        )

        #  インスタンスの生成では INSERT しない
        self.assertEqual(0, Tweets.select().count())

        # save メソッドで INSERT を発行
        # autocommit=True の場合は COMMIT 発行
        tweet.save()

        self.assertEqual(1, Tweets.select().count())

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

        self.assertEqual(2, Tweets.select().count())

        # COMMIT すると確定
        database.commit()

        self.assertEqual(1, Tweets.select().where(
            from_user='43172xxx1').count())
        self.assertEqual(1, Tweets.select().where(
            from_user='43172xxx2').count())
        self.assertEqual(0, Tweets.select().where(
            from_user='43172xxx3').count())

        # UPDATE
        query = Tweets.update(from_user='43172xxx3').where(
            from_user='43172xxx2')

        # sql メソッドで発行される SQL 文を確認可能
        self.assertEqual(('UPDATE `tweets` SET `from_user_id`=%s '
                          'WHERE `from_user_id` = %s',
                          [u'43172xxx3', u'43172xxx2']),
                         query.sql())

        # SQL文発行
        query.execute()

        self.assertEqual(0, Tweets.select().where(
            from_user='43172xxx2').count())
        self.assertEqual(1, Tweets.select().where(
            from_user='43172xxx3').count())

        # COMMIT すると確定
        database.commit()

        # DELETE
        for tweet in Tweets.select():
            tweet.delete_instance()

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

        # COMMIT すると確定
        database.commit()

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

疑問点

今回調査した時間は長くないので以下が疑問として残っている。

  • bulk insert はどうやるのか。自分でSQL文を作成しないと不可能なのか

まとめ

簡単で、使い易いと感じた。
Model の自動生成ツールが付いているので、データベースの変更にも追随が容易だろう。
簡易にベンチを取ったが、速度は比較的速い。
簡単な物だったり、商用データベースを利用するのでなければ、これで十分な感じ。

2012/10/16追記:

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

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 調査の記事の一部となる。以下が関連記事。

2012年10月3日

Python の ORM いろいろ

概要

Python にはいくつか RDB のための ORM があるので現在時点で見つけた物の一覧表を作成してみた。

一覧

今回見つけた物で、ちゃんと利用されていそうな物は以下。

リンク ライセンス PofEAA 対応RDB バージョン Python3対応 概要
peewee MIT ActiveRecord PostgreSQL,MySQL,SQLite3 1.0.0(2012-08-26) × 比較的ライトなORM。
Django BSD ActiveRecord MySQL,Oracle,PostgreSQL,SQLite3 1.4.1(2012-07-30) × ORMを含むWebアプリケーションフレームワーク。ORMの出来が良く、単体でも利用できる。
SQLAlchemy MIT DataMapper DB2,ODBC,Oracle,PostgreSQL,MS SQL Server,MySQL,SQLite3,Sybase等(Supported Databases参照) 0.7.8(2012-06-17) 多くのRDBに対応。非常に高機能。
SQLObject LGPL ActiveRecord PostgreSQL,MS SQL Server,MySQL,SQLite3,Sybase 1.3.1(2012-05-25) × SQLAlchemyと比較されるORM。昔は良く利用されていたが、最近はSQLAlchemyの方が利用されている印象。
storm LGPL ActiveRecord PostgreSQL,MySQL,SQLite3 0.19(2011-10-05) × Ubuntuの開発支援をしているCanonical社が作成している比較的ライトなORM。
Elixir MIT ActiveRecord SQLAlchemyと同じ 0.7.1(2009-11-16) SQLAlchemyをActiveRecordパターンで使うためのラッパ。結構便利だが、最近はメンテナンスされていない。

感想

自分は普段はSQLAlchemyを主に利用していて、目的によってはElixirを改造した物を利用している。それ以外のORMをほとんど利用した事がないので、今回探したORMに関して機能を調査してみようと思う。

それぞれのソースとドキュメントを簡単に読んだが、stormとpeeweeは比較的利用される場面が似ている印象。stormはそれほど使いやすそうではない感じ。peeweeの方が簡単そう。
SQLObjectは、けっこう使い易そうだが、機能が多いのにドキュメントがいまいちな印象。
DjangoのORMはDjangoのバックエンドとして利用することが想定されているが、出来は良さげなので、すべてを極力一つのフレームワークの枠組みでやりたい人には向いているかもしれない。

2012/10/15追記:

この記事の後で各ORMを調査した記事を書いた。