3. Pythonとデータベース

Python Database APIとアダプター


Pythonから利用できるデータベースはSQLiteのように簡易的なものから、オープンソースのMySQL、PostgreSQL、商用のOracleなど多岐にわたります。また,これ以外の多くのデータベースに対応しています。

PythonではデータベースのAPIが定義されています。そのAPIを実装する形で各データベース用のアダプターが提供されています。Python 2.5からはSQLite3とそのアダプターが標準ライブラリとして提供されています。Python 2.5さえインストールすれば、すぐにSQLite3を使ったデータベースプログラミングを始められます。また、別途アダプターをインストールすれば本格的なデータベースが使用できます。

本稿では、SQLite3を例に説明します。

Python O/Rマッパー

データベースAPIを使ったプログラミングでは、プログラマは直接SQLを扱います。高速に動作し、パフォーマンスチューニングが行いやすいというメリットがあります。その一方で、SQLインジェクションなどのセキュリティを意識する必要があり、また、SQL文を覚えなければならないなど、敷居が高いです。

Pythonでは古くからObject/Relationalマッパー(O/Rマッパー)が提供されていました。O/Rマッパーを使うことで、プログラマはSQLをほとんど意識することなく,Pythonの"普通"のプログラミングのようにプログラムを書くことができます。また、SQLインジェクションなどのセキュリティの脅威もO/Rマッパーが処理してくれるので、より安全にプログラミングできます。一方で、SQLを直接書く場合に比べて動作速度が遅くなったり、パフォーマンスチューニングが難しかったりします。

最近のPython WebフレームワークのほとんどがO/Rマッパーをサポートしています。以前はSQLObjectが多く使われていましたが、最近はSQLAlchemyが急速に勢力を伸ばしています。SQLAlchemyはSQLObjectに比べてサポートしているデータベースが豊富で、任意のSQL文の実行結果をオブジェクトにマッピングできるなど、自由度が高いという特徴があります。特にSQLAlchemyでOracleなどの商用データベースを扱えることが大きなアドバンテージになっています。

ただし、SQLObjectは豊富な稼働実績があり、APIも安定しています。SQLAlchemyはまだバージョンが0.4で,今後
仕様が変更される可能性もあるため、注意が必要です。

SQLAlchemy

ここでは、SQLAlechemyをとりあげます。まずは次のコマンドでインストールしましょう。

 > easy_install SQLAlchemy

SQLAlchemyは通常のデータベースAPIを利用して動作します。SQLite3以外のデータベースを使いたい場合は、対応するアダプターをインストールします。本稿ではSQLite3を利用するので、別途アダプターをインストールする必要はありません。

データベースと接続

SQLAlchemyがインストールできたら、Pythonを対話モードで起動してください。
次のコマンドを入力して、SQLAlchemyのエンジンを作成します。configは辞書です。通常はWebフレームワークが設定ファイルからconfigオブジェクトを作成しますが、ここでは簡易的に直接辞書を作成しています。辞書にはURLとしてデータベースの接続情報を記述します。それ以外にも様々なオプションが指定できます。

  >>> import sqlalchemy
  >>> config = {"sqlalchemy.url": "sqlite://memory"}
  >>> engine = sqlalchemy.engine_from_config(config)
  >>> 

この例ではURLとしてsqlite://memoryを指定します。これは、データベースとしてsqliteを使い,メモリ上にデータベースを作成するという意味になります。メモリ上にデータベースを作成すると、ちょっとした実験が後片付けの必要なく利用できて便利です。

URLは次のような形式になります。

scheme://[user[:password]@]host[:port]/database[?parameters]

[]の中はオプションになります。sqliteでc:¥tmp¥ディレクトリにtest.dbを作成する場合は、次のようになります。

sqlite://c:/tmp/test.db

sqlalchemy.engine_from_configで、SQLAlchemyのエンジンを初期化します。エンジンはコネクションやコネクションプールを扱います。

次に、下のコマンドを実行してセッションを初期化します。セッションとはデータベースのセッションのことです。プログラムがマルチスレッドで動作するときに、セッションのオブジェクトが実際のコネクションやトランザクションを管理します。metadataは、ここではおまじないだと思ってください。複数のデータベースにまたがって接続する場合に使います。

  >>> from sqlalchemy.orm import scoped_session, sessionmaker
  >>> from sqlalchemy import MetaData
  >>> db_session = scoped_session(sessionmaker(autoflush=True, transactional=True,
  ...                                       bind=engine))
  >>> metadata = MetaData()
  >>> 


テーブル定義

さて、データベースの準備が整いました。次にテーブル構造を定義して、Pythonのクラスとマッピングしましょう。SQLAlchemyは既存のデータベースから自動でテーブル構造を解析することもできます。このため、稼働中のプロジェクトをデータベースの変更なくSQLAlchemyに移行することができます。新規のプロジェクトの場合では、ほとんどSQL(やデータベース)を意識することなく、Pythonだけでプログラミングできます。

では、テーブル定義をしてみましょう。ここでは、アドレス帳のようなものを作ってみます。必要となるのは、人の名前を格納するテーブルです。次のように作成します。Tableクラスでpersonオブジェクトを作成します。最初の引数は、テーブル名です。第2引数のmetadataはおまじないです。第3引数以降にテーブルのフィールド定義を行います。

>>> from sqlalchemy import Column, Table, types
>>> from sqlalchemy.orm import mapper, relation
>>> person = Table("person", metadata,
...     Column("id", types.Integer, primary_key=True),
...     Column("first_name", types.Unicode, nullable=False),
...     Column("last_name", types.Unicode, nullable=False))
>>> 

テーブルのフィールド定義はColumnクラスで行います。最初の引数にフィールド名、第2引数にフィールドの型を定義します。第3引数以降はオプションです。

次に、テーブルとPythonのオブジェクトをマッピングするおまじないを行います。

  >>> class Person(object):
  ...     pass
  ... 
  >>> mapper(Person, person)
  <sqlalchemy.orm.mapper.Mapper object at 0x81810>
  >>> 


決まり事がいくつかありますが、これだけでテーブル定義ができました。それでは、次のコードでデータベース上にテーブルを作成します。

  >>> metadata.create_all(bind=engine)
  >>> 

テーブルの基本操作

テーブルができたので、テーブルに対して基本的な操作(INSERT/UPDATE/SELECT/DELETE)を行います。

まずはテーブルにINSERTを行ってみます。最初にPersonオブジェクトpを作成して、last_name、first_nameを設定します。次にdb_sessionに対してsaveを行います。現在のデータベースのセッションはトランザクションを有効にしているので、db_sessionに対してcommitを行うことで実際のデータベースに変更が反映されます。

  >>> p = Person()
  >>> p.last_name = u"Ohtani"
  >>> p.first_name = u"Hiroki"
  >>> db_session.save(p)
  >>> db_session.commit()
  >>> 

さて、今後の実験のために、あと、二人データベースに登録しておきます。

  >>> p = Person()
  >>> p.last_name = u"Foo"
  >>> p.first_name = u"Bar"
  >>> db_session.save(p)
  >>> p = Person()
  >>> p.last_name = u"Hoge"
  >>> p.first_name = u"Fuga"
  >>> db_session.save(p)
  >>> db_session.commit()
  >>> 

次に、データベースに対して条件に応じた検索を行ないます。query関数を実行してQueryオブジェクトを作成します。

  >>> query = db_session.query(Person)
  >>> type(query)
  <class 'sqlalchemy.orm.query.Query'>
  >>> 

では、テーブルの中にあるレコード数をカウントしてみます。先ほど3件のレコードを登録したので、結果が3になります。

  >>> query.count()
  3
  >>> 

では、各レコードを順番にイテレーションします。

  >>> for p in query:
  ...     print "-" * 10
  ...     print p.first_name
  ...     print p.last_name
  ... 
  ----------
  Hiroki
  Ohtani
  ----------
  Bar
  Foo
  ----------
  Fuga
  Hoge
  >>> 
  
 もう少し簡易的に次のように配列でアクセスすることもできます。
 
  >>> p = query[0]
  >>> p.first_name
  u'Hiroki'
  >>> p.last_name
  u'Ohtani'
  >>>


では条件によるフィルタをかけてみましょう。SQL文でWHERE句を書くことに相当します。ここでは、first_nameがHogeのものだけをフィルタします。qeuryのfilterメソッドを呼び出すことでフィルタできます。fitlerの戻り値も同じQueryオブジェクトになりますので、countメソッドやallメソッドなどが使えます。ここでは、検索結果から最初のものを返すoneメソッドを使っています。

  >>> hoge_query = query.filter(Person.last_name==u"Hoge")
  >>> type(hoge_query)
  <class 'sqlalchemy.orm.query.Query'>
  >>> hoge_query.count()
  1
  >>> p = hoge_query.one()
  >>> p.first_name
  u'Fuga'
  >>> p.last_name
  u'Hoge'
  >>> 

データの更新は、検索したオブジェクトを変更してコミットするだけです。上の例でfirst_nameのFugaをMogeに変えてみましょう。

  >>> p.first_name = u"Moge"
  >>> db_session.save_or_update(p)
  >>> db_session.commit()
  >>> 

さて、最後にレコードを削除します。レコードの削除は次のようになります。

  >>> db_session.delete(p)
  >>> db_session.commit()
  >>>

テーブルに対して基本的な操作を見てきました。ここまでのコードでSQL文を一行も書かずに操作できることがわかると思います。また、簡単な操作であれば、ほとんどデータベース自体を意識することなくコードが書けることがわかると思います。

また、ここでは説明しませんが、必要であればSQL文を直接書けたり、SQL文でselectした結果を特定のオブジェクトにマッピングすることもできます。

リレーション

多くのデータベースを利用したアプリケーションでは、リレーションを使う機会が多いと思います。SQLAlchemyではリレーションもスマートに扱え、SQL文を意識する必要はありません。リレーションは、1対多、多対多両方が扱えますが、紙面の都合上、ここでは、多対多のリレーションを説明します。

ここでは、personテーブルとaddressテーブルにリレーションをはります。personには名前を格納してaddressには住所を格納するようにします。まずは、テーブル定義です。多対多のテーブル定義は少々長くなりますが、ほとんどが決まり事です。

>>> person = Table("person", metadata,
...     Column("id", types.Integer, primary_key=True),
...     Column("first_name", types.Unicode, nullable=False),
...     Column("last_name", types.Unicode, nullable=False))
>>> class Person(object):
...     pass
... 
>>> address = Table('address', metadata,
...     Column('id', types.Integer, primary_key=True),
...     Column('address', types.Unicode(100))
... )
>>> class Address(object): 
...     pass
... 
>>> mapper(Address, address)
<sqlalchemy.orm.mapper.Mapper object at 0xe7e6f0>
>>> addresses_people = Table('address_person', metadata,
...     Column('id', types.Integer, primary_key=True),
...     Column('person_id', types.Integer, ForeignKey('person.id')),
...     Column('address_id', types.Integer, ForeignKey('address.id'))
... )
>>> mapper(Person, person, properties = {
...     'addresses' : relation(Address, secondary = addresses_people),
...     })
<sqlalchemy.orm.mapper.Mapper object at 0xe7eb70>
>>> 
>>> metadata.create_all(bind=engine)
>>> 

personのテーブル定義とaddressのテーブル定義はフィールドの数や名前が違うだけでほとんど同じです。リレーションの設定はperson_address中間テーブルを介して設定します。そして、mapper関数で、PersonクラスとAddressクラスを関連づけています。

それでは、レコード間でリレーションを設定してみましょう。まずは、PersonとAddressのオブジェクトを設定します。次に、PersonオブジェクトpのaddressesフィールドにAddressオブジェクトaを追加しています。

>>> p = Person()
>>> p.first_name = "Foo"
>>> p.last_name = u"Bar"
>>> p.first_name = u"Foo"
>>> db_session.save(p)
>>> a = Address()
>>> a.address = u"somewhere in Japan"
>>> p.addresses.append(a)
>>> db_session.save(a)
>>> db_session.commit()

次にコードを実行してみてください。addressesフィールドはsqlalchemy.orm.collections.InstrumentedListクラスのオブジェクトですが、リストとして振る舞うことがわかります。

>>> type(p.addresses)
<class 'sqlalchemy.orm.collections.InstrumentedList'>
>>> isinstance(p.addresses, list)
True
>>> 

それでは、Personに対してqueryを発行してみます。query[0]のaddressesフィールドに登録したアドレスが設定されていることがわかると思います。

>>> query = db_session.query(Person)
>>> query.count()
1
>>> query[0].first_name
u'Foo'
>>> query[0].addresses
[<__main__.Address object at 0xe7eb10>]
>>> query[0].addresses[0].address
u'somewhere in Japan'
>>> 

このように、多対多のリレーションがある場合でも、ほとんどPythonの知識だけでコードが書けることがわかると思います。
Comments