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月6日

Django の機能をコマンドラインやcronから実行する手順

概要

Django」の機能はWebアプリとして利用したことしかないので、スクリプトとして利用する方法を調査してみた。

ドキュメント

公式ドキュメント(英語)の「Writing custom django-admin commands」に手順が書いてある。

「django-admin.py」がそもそも Django で作成されているスクリプトで、ソースコードは「django/core/management/commands」に存在している。

手順

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

django-admin.py startproject mysite

ディレクトリ構成は以下のようになる。

mysite
├── manage.py
└── mysite
    ├── __init__.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py

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

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',
)

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

# -*- coding: utf-8 -*-
from django.core.management.base import BaseCommand


class Command(BaseCommand):

    def handle(self, *args, **options):
        print 'さんぷるそーす'

ディレクトリ構成は以下のようになる。

mysite
├── example
│   ├── __init__.py
│   ├── management
│   │   ├── __init__.py
│   │   └── commands
│   │       ├── __init__.py
│   │       ├── sample.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── manage.py
└── mysite
    ├── __init__.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py

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

python manage.py sample

「さんぷるそーす」と表示されれば成功。

まとめ

これで Django の機能をコマンドラインで利用できるようになった。結構便利。

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を調査した記事を書いた。

2012年10月2日

Python の MySQL ドライバはどれを利用すれば良いのか

概要

Python から MySQL に接続するためのドライバは複数存在している。どれを使うのが一番良いのか確認してみる。

Python の MySQLドライバ

Python の MySQLドライバの主な物は「MySQL - PythonInfo Wiki」に記載されている。

以下のような物がある。記事を書いた時点でのpypiでの最終リリース日の新しい順。

リンク Python3対応 DB API v2.0対応 ライセンス メンテナー 最終リリース日 概要
MySQL-python × GPL or CNRI Python License Andy Dustman 2012-09-27 比較的初期から存在したため有名なドライバ
mysql-connector-python GNU GPLv2 (with FOSS License Exception) Oracle社および協力者 2012-09-07 MySQL公式配布物
oursql BSD Aaron Gallagher 2012-06-05 MYSQL_STMT API のラッピングにフォーカスしたドライバ
umysql 不明 × BSD License Jonas Tarnstrom 2012-04-20 C/C++のみで実装されている。gevent対応と記載されている。
mysql-connector-repackaged GNU GPLv2 (with FOSS License Exception) Sun Microsystems社 2012-03-11 mysql-connector-python」の古いバージョン
PyMySQL3 MIT Pete Hunt 2011-11-08 Pythonのみで実装されている。Python2 対応版はPyMySQLで別配布。
PyMySQL × MIT Pete Hunt 2011-11-08 Pythonのみで実装されている。Python3 対応版はPyMySQL3で別配布。

とりあえず、Python 2.7.3 で動作を確認する。また DB API v2.0 対応は必須としたい。
この条件に合致する 「MySQL-python」、 「mysql-connector-python」、 「oursql」、 「PyMySQL」、 を比較してみる。

準備

利用した環境のバージョンは以下とした。今回の MySQL は localhost の物を利用する。
比較観点は、「実行速度」、「メモリ効率」、「使い勝手」。「使い勝手」に関しては「戻り値」、「with」、「例外」あたりを見ることにする。

  • Mac OS X 10.8.3 Mountain Lion
  • Python 2.7.3
  • MySQL Community Server 5.5.27
    • ホスト:localhost
    • データベース名:example
    • ユーザ名:username
    • パスワード:password
  • 利用ドライバ
    • MySQL-python 1.2.4b2
    • mysql-connector-python 1.0.6b2
    • oursql 0.9.3.1
    • PyMySQL 0.5

Python に今回利用するライブラリをインストール。

# ドライバ類のインストール
pip install MySQL-python
pip install mysql-connector-python
pip install oursql
pip install PyMySQL
# 計測ツール
easy_install Benchmarker
pip install psutil
pip install memory_profiler

データベースとテーブルを作成。

> 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 -ppassword 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;

ソースコード

単純な INSERT、SELECT、DELETE のコードを以下のように書いてみた。

MySQL-pythonのサンプル

def sample_mysql_python():
    import MySQLdb
    cur = MySQLdb.connect(
        host='localhost',
        db='example',
        user='username',
        passwd='password').cursor()

    print(cur.execute('SELECT * FROM tweets'))

    # 日本語でも u つけない
    cur.execute(
        'INSERT INTO tweets '
        '(status_id, from_user_id, text, created_at, datetime) '
        'VALUES (%s, %s, %s, %s, current_timestamp)',
        ('251298602096xxxxx1', '43172xxx6',
         'さんぷるでーた', 'Mon, 1 Oct 2012 12:33:50 +0000'))

    print(cur.execute('SELECT * FROM tweets'))

    res = cur.fetchall()

    for row in res:
        print(row[3])

    cur.execute(
        'DELETE FROM tweets WHERE status_id = %s',
        ('251298602096xxxxx1',))

    cur.close()

oursqlのサンプル

def sample_oursql():
    import oursql
    with oursql.connect(
            host='localhost',
            db='example',
            user='username',
            passwd='password').cursor() as cur:

        print(cur.execute('SELECT * FROM tweets'))

        # 日本語は u つける
        cur.execute(
            'INSERT INTO tweets '
            '(status_id, from_user_id, text, created_at, datetime) '
            'VALUES (?, ?, ?, ?, current_timestamp)',
            ('251298602096xxxxx1', '43172xxx6',
             u'さんぷるでーた', 'Mon, 1 Oct 2012 12:33:50 +0000'))

        print(cur.execute('SELECT * FROM tweets'))

        res = cur.fetchall()

        for row in res:
            print(row[3])

        cur.execute(
            'DELETE FROM tweets WHERE status_id = ?',
            ('251298602096xxxxx1',))

PyMySQLのサンプル。自分の環境では日本語が INSERT できなかった。

def sample_pymysql():
    import pymysql
    cur = pymysql.connect(
        host='localhost',
        db='example',
        user='username',
        passwd='password').cursor()

    print(cur.execute('SELECT * FROM tweets'))

    # TODO 日本語がうまく入らない?
    cur.execute(
        'INSERT INTO tweets '
        '(status_id, from_user_id, text, created_at, datetime) '
        'VALUES (%s, %s, %s, %s, current_timestamp)',
        ('251298602096xxxxx1', '43172xxx6',
         'さんぷるでーた', 'Mon, 1 Oct 2012 12:33:50 +0000'))

    print(cur.execute('SELECT * FROM tweets'))

    res = cur.fetchall()

    for row in res:
        print(row[3])

    cur.execute(
        'DELETE FROM tweets WHERE status_id = %s',
        ('251298602096xxxxx1',))

    cur.close()

mysql-connector-pythonのサンプル。自分の環境では INSERT 文が実行できなかった。

def sample_mysql_connector():
    import mysql.connector
    con = mysql.connector.connect(
        host='localhost',
        db='example',
        user='username',
        passwd='password',
        buffered=True)

    # 書き方が他と違うが、connect().cursor() 形式だとエラーになる模様
    cur = con.cursor()

    cur.execute('SELECT * FROM tweets')

    # TODO 動作しない?「Unread result found」になる「buffered=True」で動作する
    cur.execute(
        'INSERT INTO tweets '
        '(status_id, from_user_id, text, created_at, datetime) '
        'VALUES (%s, %s, %s, %s, current_timestamp)',
        ('251298602096xxxxx1', '43172xxx6',
         'さんぷるでーた', 'Mon, 1 Oct 2012 12:33:50 +0000'))

    cur.execute('SELECT * FROM tweets')

    res = cur.fetchall()

    for row in res:
        print(row[3])

    cur.execute(
        'DELETE FROM tweets WHERE status_id = %s',
        ('251298602096xxxxx1',))

    cur.close()
    con.close()

比較結果

「mysql-connector-python」は実行できなかったので、比較できないという結果になったが、どうやるのが正しいのだろう?このサンプルでは「buffered=True」が必要。

様々な条件で試した結果、速度は「MySQL-python」、「oursql」、「PyMySQL」の順。数値は載せないので、自分で試してほしい。
メモリ効率はこのサンプルだとそれほど違いがないようだ。

使い易さは「oursql」が圧倒的で、「with」が利用できるのはこのドライバだけ。いろいろ頑張っている分遅いと思われる。また 「oursql」は「libmysql」に依存しているのは欠点。

結論

速度を求める場合や、ORMを利用する場合は、「MySQL-python」を使うのが良い。Python 3 への対応は徐々に行なわれている模様。
ドライバだけで利用したい場合や Python 3 に対応したい場合は、「oursql」は良い選択肢になる。

「PyMySQL」と「mysql-connector-python」に関しては、今回上手く実行できていない部分があるので判断を保留とする。

2012/10/10追記:

「PyMySQL」と「mysql-connector-python」のエラーに関して補足しておく。

「PyMySQL」のエラー

「PyMySQL」は「"さんぷるでーた"」として日本語を INSERT して、SELECT しようとすると以下のようなエラーになる。

Traceback (most recent call last):
  File "sample.py", line xxx, in <module>
    main()
  File "sample.py", line xxx, in main
    sample_pymysql()
  File "sample.py", line xxx, in sample_pymysql
    print(cur.execute('SELECT * FROM tweets'))
  File "/path/to/lib/python2.7/site-packages/pymysql/cursors.py", line 117, in execute
    self.errorhandler(self, exc, value)
  File "/path/to/lib/python2.7/site-packages/pymysql/connections.py", line 187, in defaulterrorhandler
    raise Error(errorclass, errorvalue)
pymysql.err.Error: (<type 'exceptions.UnicodeEncodeError'>, UnicodeEncodeError('latin-1', u'\u3055\u3093\u3077\u308b\u3067\u30fc\u305f', 0, 7, 'ordinal not in range(256)'))

「u"さんぷるでーた"」として日本語を INSERT しようとすると以下のようなエラーになる。

Traceback (most recent call last):
  File "sample.py", line xxx, in <module>
    main()
  File "sample.py", line xxx, in main
    sample_pymysql()
  File "sample.py", line xx, in sample_pymysql
    u'さんぷるでーた', 'Mon, 1 Oct 2012 12:33:50 +0000'))
  File "/path/to/lib/python2.7/site-packages/pymysql/cursors.py", line 97, in execute
    escaped_args = tuple(conn.escape(arg) for arg in args)
  File "/path/to/lib/python2.7/site-packages/pymysql/cursors.py", line 97, in <genexpr>
    escaped_args = tuple(conn.escape(arg) for arg in args)
  File "/path/to/lib/python2.7/site-packages/pymysql/connections.py", line 579, in escape
    return escape_item(obj, self.charset)
  File "/path/to/lib/python2.7/site-packages/pymysql/converters.py", line 35, in escape_item
    val = val.encode(charset)
UnicodeEncodeError: 'latin-1' codec can't encode characters in position 1-7: ordinal not in range(256)

つまり、日本語の INSERT は可能な模様。しかし、「SELECT」ができない。
このサンプルのままだと、エラーが解消されなかったのでそれ以上の原因の調査はしていない。

「mysql-connector-python」のエラー

「mysql-connector-python」は、INSERTで以下のようなエラーになる。

Traceback (most recent call last):
  File "sample.py", line xxx, in <module>
    main()
  File "sample.py", line xxx, in main
    sample_mysql_connector()
  File "sample.py", line xxx, in sample_mysql_connector
    'さんぷるでーた', 'Mon, 1 Oct 2012 12:33:50 +0000'))
  File "/path/to/lib/python2.7/site-packages/mysql/connector/cursor.py", line 365, in execute
    raise errors.InternalError("Unread result found.")
mysql.connector.errors.InternalError: Unread result found.

その後 ORM の調査をしているが、「MySQL-python」以外のドライバを利用しようとすると、結構面倒なので、「MySQL-python」以外のドライバに関して興味を喪失したため、その後調査していない。

2012/10/11追記:

「PyMySQL」charsetの設定をしたら通った。「PyMySQL」では明確に指定する必要があるのですね。

con = pymysql.connect(
    host='localhost',
    db='example',
    user='username',
    passwd='password',
    charset='utf8')

「mysql-connector-python」の方はcharsetやportを指定してもエラーになるので別の原因らしい。

2013/05/29追記:

「mysql-connector-python」はこのサンプルでは「buffered=True」が必要。

2012年10月1日

Mac OS X で Python のバージョンを切り替えるための VERSIONER_PYTHON_VERSION 環境変数

概要

osx - How can I make python 2.6 my default in Mac OS X Lion? - Stack Overflow」という質問に「VERSIONER_PYTHON_VERSION」と「com.apple.versioner.python」が書いてあった。
知らなかったのでメモしておく。

使い方

「man python」すると書いてある。
Mac OS X では 「VERSIONER_PYTHON_VERSION」環境変数に利用したい Python のバージョンを設定すると、それが起動されるようになる。

export VERSIONER_PYTHON_VERSION=2.6
python -V
Python 2.6.6

疑問

追加で Python を自分でインストールしている環境では上手く動作しないが、動作させる方法はあるのだろうか?