※この文書は、 [SQLAlchemy 0.4 for people in a hurry] の v.29 を翻訳したものです。
手っ取り早く知りたい人のためのSQLAlchemy 0.4
これは、 Pylons >= 0.9.6 と SQLAlchemy >= 0.4 のための SQLAlchemy 0.4 バージョンの記事です。もしまだ SQLAlchemy 0.3 を使用しているなら、[SQLAlchemy 0.3 for people in a hurry] を見てください。このバージョンは、 SAContext や pylons.database を使用しません。その両方とも廃止されました。
これは、他のコンポーネントを使い始めるための Pylons のクイックガイドのひとつです。それは Pylons 0.9.6rc2 (2007年8月) 現在、 PylonsとSQLAlchemy を使用するお勧めの方法を述べます。これは Pylons と SQLAlchemy を使用する 唯一の 方法ではありませんが、始めるのが簡単で、柔軟なアプローチです。リレーショナルデータベースや、ジョインテーブル、多対一関連、多対多関連などの概念になじみがないなら、これを始める前に、Relational databases for people in a hurry を試してください。
SQLAlchemy マニュアル は、ここでカバーされなかった質問のためにあなたが次に立ち寄るべき場所でしょう。 SQLAlchemy 0.3 になじみ深いなら、 What's new in 0.4 を見てください。 Elixirと Tesla のようなサードパーティのフロントエンドはこの記事の最後に説明されます。
この記事を通して、 "myapp" はあなたの Pylons アプリケーションのパッケージディレクトリ (例えば、 MyApp-1.0.1.egg/myapp) を参照します。
Introduction
SQLAlchemy は、いくつかの SQL データベースへのフロントエンドです。あなたは SQLAlchemy を 3 つの異なったレベルで使うことができ、それらを同じアプリケーションの中で混在することもできます。
- オブジェクト-関連マッパー (ORM) は、 あなた自身のオブジェクトクラスを使用することによって、 SQL コードを書かずにデータベースとやり取りすることができます。
- SQL 式言語には、カスタマイズされた SQL 文を作成するための多くのメソッドがあり、結果カーソルは DBAPI より扱いやすいです。
- SQL ビルダーではできないこと (既存のテーブルにカラムを加えたり、カラムの型を変更したり) をしようとする場合、低レベルの execute メソッドはリテラルの SQL 文字列を受け付けます。それらが結果を返すなら、依然として SQLAlchemy の結果カーソルの恩恵を受けることができます。
最初の 2 つのレベルが データベース中立 です。その意味は、それらは MySQL, PostgreSQL, MSSQL, SQLite などの違いを隠蔽します。 異なるデータベースに変更するには、単に新しい接続 URL を指定するだけで済みます。 もちろん限界もありますが、 SQLAlchemy はあなたのすべての SQL クエリを書き直すより 90% 簡単です。
SQLAlchemy のインストール
Pylons が既にインストールされていることを前提とすると、コマンドラインで次をタイプしてください
easy_install SQLAlchemy easy_install pysqlite # SQLite を使う場合 easy_install MySQL-python # MySQL を使う場合 easy_install psycopg2 # PostgreSQL を使う場合
他のデータベースドライバーを使う場合は Python Package Index (以前の Cheeseshop) を見てください。
バージョンチェック
SQLAlchemy が最新であることを確かめるには、 Python シェルに行って sqlalchemy._version_ を見ます:
1 2 3 | >>> import sqlalchemy >>> sqlalchemy.__version__ 0.4.0 |
設定ファイル
使用するデータベースに依存して、次のような記述を development.ini の [app:main] セクションに追加してください。
SQLite の場合
1 | sqlalchemy.url = sqlite:///%(here)s/mydatabasefilename.db |
ここで mydatabasefilename.db は SQLite データベースファイルへのパスです。 "%(here)s" は development.ini ファイルを含むディレクトリを表します。
MySQL の場合
1 2 3 | sqlalchemy.url = mysql://username:password@host:port/database sqlalchemy.echo = True sqlalchemy.pool_recycle = 3600 |
ユーザ名、パスワード、ホスト (同一マシン上にあるなら localhost)、ポート番号 (通常 3306)、およびデータベース名を入力してください。 2行目と3行目は省略可能です; それらは エンジンオプション の設定例です。
"pool_recycle" という行は MySQL を使用する場合に重要です; それは、SQLAlchemy が使用されてから 1時間 (3600秒) 後に、データベース接続を閉じて再接続するようにします; これは MySQL が数時間のアイドル時間の後に接続を一方的に閉じるのを防ぎ、 "MySQL server has gone away" エラーを避けることができます。
PostgreSQL の場合
1 | sqlalchemy.url = postgres://username:password@host:port/database |
ユーザ名、パスワード、ホスト (同一マシン上にあるなら localhost)、ポート番号 (通常 5432)、およびデータベース名を入力してください。
注意: 後から 2 つ目のデータベースを加える場合 (後述の "複数エンジン" セクションで説明される) に物事が簡単になるように、 "sqlalchemy.url" の代わりに "sqlalchemy.default.url" を好む人もいます。
エンジン
myapp/config/environment.py の先頭にこれを追加してください:
1 | from sqlalchemy import engine_from_config |
そしてこれを load_environment 関数の中に入れてください:
1 | config['pylons.g'].sa_engine = engine_from_config(config, 'sqlalchemy.') |
2番目の引数は、探すべきプリフィックスです。 あなたがキーを "sqlalchemy.default.url" と命名するなら、ここは "sqlalchemy.default" となるでしょう。プリフィックスは、設定ファイルとこの関数呼び出しの間で一貫している限り何でも構いません.
モデル
アプリケーションの myapp/model/__init__.py の中身を、このように修正してください:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | from pylons import config from sqlalchemy import Column, MetaData, Table, types from sqlalchemy.orm import mapper, relation from sqlalchemy.orm import scoped_session, sessionmaker # Global session manager. Session() returns the session object # appropriate for the current web request. Session = scoped_session(sessionmaker(autoflush=True, transactional=True, bind=config['pylons.g'].sa_engine)) # Global metadata. If you have multiple databases with overlapping table # names, you'll need a metadata for each database. metadata = MetaData() # Define a table. table1 = Table("table1", metadata, Column("id", types.Integer, primary_key=True), Column("name", types.String, nullable=False), ) # Define another table, reading its structure from an existing database # table. You must provide an engine in this case. table2 = Table("table2", metadata, autoload=True, autoload_with=config['pylons.g'].sa_engine) # Define ORM classes (often called "mapped classes"). # attributes will be added by the mapper below class MyClass(object): pass # Map each class to its corresponding table. mapper(MyClass, table1) |
これは 1 つのエンジンへのアクセスを提供します。 複数のエンジンを使用する方法は以下で説明されます。
関連の例
これは、 Person と Address クラスが people.my_address で多対多関連をしている例です。詳細については Relational databases for people in a hurry と SQLAlchemy マニュアルを見てください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | from sqlalchemy import ForeignKey people_table = Table('people', metadata, Column('id', types.Integer, primary_key=True), Column('name', types.String(100)), Column('email', types.String(100)) ) addresses_people_table = Table('addresses_people', metadata, Column('id', types.Integer, primary_key=True), Column('person_id', types.Integer, ForeignKey('people.id')), Column('address_id', types.Integer, ForeignKey('addresses.id')) ) addresses_table = Table('addresses', metadata, Column('id', types.Integer, primary_key=True), Column('address', types.String(100)) ) mapper(Address, addresses_table) mapper(Person, people_table, properties = { 'my_addresses' : relation(Address, secondary = addresses_people_table), }) |
コントローラ
以下のコードを myapp/lib/base.py のベースコントローラの _call_ メソッドに追加してください:
1 2 3 4 5 | def __call__(self, environ, start_response): try: return WSGIController.__call__(self, environ, start_response) finally: model.Session.remove() |
.remove()メソッドは非常に重要です! それは現在のウェブリクエストにおける ORM データの残り物を捨てます。 さもなければ、はぐれたデータがこのスレッドによって扱われる次のリクエストに漏れてしまい、潜在的にエラーかデータの汚染を引き起こすでしょう。
この段階では、リクエスト毎の振舞いを何でも構成することができます。例えば、 1 リクエストに 1 つだけの単独のデータベース接続を使用する (それはすべての接続プールのチェックイン/チェックアウトオーバーヘッドを取り除く) ために、リクエスト毎の Session を Connection と共に構成することができます:
1 2 3 4 5 6 7 8 | def __call__(self, environ, start_response): conn = config['pylons.g'].sa_engine.connect() model.Session(bind=conn) try: return WSGIController.__call__(self, environ, start_response) finally: model.Session.remove() conn.close() |
transactional=True なセッションを使用するとき、セッションがそれぞれのトランザクションの寿命を通して単一の接続を持つので、上の最適化が重要でないことに注意してください。
データベースの作成
データベースの中に実際にテーブルを作成するために、 myapp/websetup.py をカスタマイズしてください。 load_environment() 呼び出しの後に次のようなコードを追加します。
1 2 3 4 5 | ... from myapp import model log.info("Creating tables") model.metadata.create_all(bind=config['pylons.g'].sa_engine) log.info("Successfully setup") |
そして、コマンドラインで以下のコマンドを実行します:
paster setup-app development.ini
追加された行は、データベースに接続して、定義したすべてのテーブルを作成します。
データのクエリと修正
重要: このセクションでは、あなたがコードを高度なモデル関数に置いていると仮定しています。 コードを直接コントローラメソッドに入れている場合、モデルで定義されたあらゆるオブジェクトの前に model. プリフィックスを置くか、または個別にオブジェクトをインポートしなければならないでしょう。 また、ここでの Session オブジェクト (大文字の s) が、コントローラの Beaker session オブジェクト (小文字の s) とは異なることに注意してください。
これは、データベースに新しいデータを追加する方法です:
1 2 3 4 | mr_jones = Person() mr_jones.name = 'Mr Jones' Session.save(mr_jones) Session.commit() |
ここで mr_jones は、 Person のインスタンスです。 そのプロパティは、people_table のカラム名に対応していて、選択された行からのデータを含んでいます。より精巧なアプリケーションには、引数に基づいて自動的に属性を設定する Person.__init__ メソッドがあるでしょう。
コントローラメソッドでデータベースエントリをロードして、性別を変更して、それを保存する例です:
1 2 3 4 5 | person_q = Session.query(Person) # Person テーブルにアクセスするための ORM Query オブジェクト mr_jones = person_q.filter(Person.name=='Mr Jones').one() print mr_jones.name # 'Mr Jones' と表示される mr_jones.name = 'Mrs Jones' # ここではオブジェクトインスタンスだけが変更される ... Session.commit() # ... ここではじめてデータベースが更新される |
エントリのリストを返すためには:
1 | all_mr_joneses = person_q.filter(Person.name=='Mr Jones').all() |
テーブルに含まれるすべての人のリストを取得するには:
1 | everyone = person_q.all() |
id で検索するには:
1 | someuser = person_q.get(5) |
より簡単に、すべての人に対して反復を行うことができます:
1 2 3 4 5 6 7 | print "All people" for p in person_q: print p.name print print "All Mr Joneses:" for p in person_q.filter(Person.name=='Mr Jones'): print p.name |
エントリを削除するには次のようにします:
1 2 3 | mr_jones = person_q.filter(Person.name=='Mr Jones').one() Session.delete(mr_jones) Session.commit() |
ジョインされたオブジェクトを扱う
my_address のプロパティが Address オブジェクトのリストであることを思い出してください。
1 | print mr_jones.my_addresses[0].address # 最初のアドレスを表示する |
'Mr Jones' に既存のアドレスを加えるためには、次のようにします:
1 2 3 4 | address_q = Session.query(Address) address = address_q.filter(Address.address=='33 Pine Marten Lane, Pleasantville').one() # 既存のアドレスを検索する mr_jones.my_addresses.append(new_address) # リストに追加する Session.commit() # ジョインテーブルに更新を発行する |
'Mr Jones' に完全に新しいアドレスを加えるためには、次のようにします:
1 2 3 4 | new_address = Address() # 空のアドレスオブジェクトを構築する new_address.address = '33 Pine Marten Lane, Pleasantville' mr_jones.my_addresses.append(new_address) # リストに追加する Session.commit() # データベースに変更をコミットする |
変更を行った後でデータベースにそれらを永続的に格納するために、 Session.commit() を呼ばなければなりません; さもなければ、それらはウェブリクエストの終わりに捨てられるでしょう。 また、いつでも Session.rollback() を呼ぶことで、まだコミットされていないあらゆる変更を元に戻すことができます。
ジョインされたオブジェクトの中から検索するために、オブジェクトそのものをクエリとして渡すことができます:
1 2 3 | search_address = Address() search_address.address = '33 Pine Marten Lane, Pleasantville' residents_at_33_pine_marten_lane = person_q.filter(Person.my_addresses.contains(search_address)).all() |
- クエリオブジェクトのすべての属性が一致しなければなりません。
または、ジョインされたオブジェクトのプロパティで検索することができます:
1 | residents_at_33_pine_marten_lane = person_q.join('my_addresses').filter(Address.address=='33 Pine Marten Lane, Pleasantville').all() |
上記のショートカットは、 any() を使用することです:
1 | residents_at_33_pine_marten_lane = person_q.filter(Person.my_addresses.any(Address.address=='33 Pine Marten Lane, Pleasantville')).all() |
Mr Jones からアドレスへの関連をなくすには、次をします:
1 2 | del mr_jones.my_addresses[0] # アドレスへの参照を削除する Session.commit() |
address テーブルのアドレス自体を削除するためには、通常は別の delete() を Address オブジェクト自体に発行しなければなりません:
1 2 3 | Session.delete(mr_jones.my_addresses[0]) # アドレスオブジェクトを削除する del mr_jones.my_addresses[0] Session.commit() # 両方の操作をデータベースにコミットする |
しかしながら、 SQLAlchemy は上の操作のために近道をサポートします。代わりに cascade="all, delete-orphan" を使用して、マッパー関連を構成してください:
1 2 3 4 | mapper(Address, addresses_table) mapper(Person, people_table, properties = { 'my_addresses' : relation(Address, secondary = addresses_people_table, cascade="all,delete-orphan"), }) |
すると、 mr_jones.my_address から取り除かれた項目は、データベースからも自動的に削除されます:
1 2 | del mr_jones.my_addresses[0] # アドレスへの参照を削除して、 Address も削除する Session.commit() |
あらゆる関連において、マッパーの relation() に対する追加の引数として cascade="all, delete-orphan" を加えることによって、ジョインが削除された時にジョインされたオブジェクトも確実に削除されるようにできます。それによって、上の delete() 操作は必要でなくなります。 – my_address リストからの削除だけ。 delete-orphan は、その名前にもかかわらず、たとえ別のオブジェクトがジョインされていたとしても、ジョインされたオブジェクトを削除することに注意してください。
非 ORM SQL クエリ
セッションのトランザクション中で 非 ORM SQLクエリを実行するためには、 Session.execute() を使用してください。
1 2 3 4 5 6 7 8 9 10 | from sqlalchemy import select, text q = select([table1.c.id, table1.c.name], order_by=[table1.c.name]) records = Session.execute(q).fetchall() # XXX Example of a bulk SQL UPDATE. Remember Session.commit(). # XXX Example of a bulk SQL INSERT. Remember Session.commit(). # Database specific, use only if SQLAlchemy doesn't have methods to construct the desired query. Session.execute("ALTER TABLE Foo ADD new_column (VARCHAR(255)) NOT NULL") |
警告: 最後の例は、データベース構造を変えるため、ORM 操作に対して悪影響を与えるかもしれません。
Further reading
Query オブジェクトは、条件によるフィルタリング、結果の並べ替え、グルーピングなどの多くの特徴を持っています。これらは SQLAlchemy マニュアルで詳細に説明されます。 特に Data Mapping と Session / Unit of Work の章を見てください。
モデルのテスト
モデルの通常の使用はモデルのテストにおいて正しく機能しますが、メタデータを使用するためにエンジン接続を指定しなければなりません。あなたのプロジェクトであらゆるユニットテストに対してテーブルを作成させるために、以下のような test_models.py を使用してください。
1 2 3 4 5 6 7 8 9 10 | from myapp.tests import * import myapp.model as model class TestModels(TestController): def setUp(self): model.Session.remove() model.metadata.create_all(model.Session.bind) def test_index(self): # test your models |
|
テストが TestController から派生されることに注意してください。 これは、モデルが正しく動くためにアプリケーションが確実にセットアップされるようにするためです。 |
複数エンジン
いくつかのアプリケーションでは複数のデータベース(エンジン)に接続する必要があります。 あるアプリケーションは、特定のテーブルはいつも同じエンジン (例えば、汎用のデータベースとロギング用のデータベース) にバインドします; これは「水平パーティション」と呼ばれます。 他のアプリケーションでは、同じ構造を持ついくつかのデータベースを持っていて、現在の要求に応じてそのうちの 1 つを選びます。例えば、各ブログのために別々のデータベースを持つブログアプリケーションなどです。大規模なアプリケーションでは、データベースサイズが大きくなり過ぎるのを防ぐために、同一の論理テーブル中の異なるレコードを異なるデータベースに保存することがあります; これは「垂直パーティション」または「sharding」と呼ばれます。 上のパターンは、少しのマイナーチェンジでこれらのスキーマのいずれにも適合することができます。
まず最初に、設定ファイルの中でこのように複数のエンジンを定義することができます:
sqlalchemy.default.url = "mysql://..." sqlalchemy.default.pool_recycle = 3600 sqlalchemy.log.url = "sqlite://..."
これは、 "default" と "log" の 2 つのエンジンを、それぞれそれのオプションセットで定義します。 この場合、使用したいすべてのエンジンをインスタンス化しなければなりません。
1 2 | config['pylons.g'].sa_default_engine = engine_from_config(config, 'sqlalchemy.default.') config['pylons.g'].sa_log_engine = engine_from_config(config, 'sqlalchemy.log.') |
エンジン属性は好きなように命名することができます。または、すべてのエンジンを dict に入れて、1 つの属性とすることもできます。後者は、 URL に含まれるブログ識別子がエンジン dict のキーにマッチするなら、上記のブログアプリケーションで便利でしょう。
異なるテーブルを異なるデータベースにバインドする際に、特定のテーブルがいつも同じエンジンにバインドされるようにするためには、 bind ではなく sessionmaker に binds 引数を使用してください:
1 2 | binds = {"table1": engine1, "table2": engine2}) Session = scoped_session(sessionmaker(transactional=True, autoflush=True, binds=binds) |
リクエスト毎のバインドを選ぶなら、 sessionmaker の bind(s) 引数をスキップして、代わりにベースコントローラの _call_ メソッド中のスーパークラス呼び出しの前か、特定のアクションメソッドに直接このコードを入れてください:
1 | model.Session(bind=g.sa_engine) |
ここでも binds= は同じように動きます。 (ここでは煩雑な config['pylons.g'] ではなく g を使用することができます。なぜならコントローラモジュールの中にいてリクエストがアクティブだからです)
コーディングスタイル、Session オブジェクト、バインドされたメタデータ
すべての ORM 操作が session オブジェクトとエンジンを必要とします。 非 ORM SQL 操作はすべてエンジンを必要とします。 (厳密に言うと、それらは代わりに接続を使用することができますが、それはこのチュートリアルの範囲を超えています。) あなたは、実際のデータベースクエリを行うあらゆる SQLAlchemy メソッドにエンジンを bind= 引数として渡すことができ、またエンジンをセッションやメタデータにバインドすることもできます。このチュートリアルではセッションをバインドする方法を勧めます。前述の "Multiple Engines" セクションで示されるように、それが最も柔軟性が高いからです。
また、 MetaData(engine) 構文を使用することでエンジンにメタデータをバインドしたり、 metadata.bind=engine でバインドを変えることも可能です。これにより、 autoload_with 引数なしでオートローディングをすることや、エンジンやセッションを指定しないで特定の SQL 操作を行うことができるようになります。バインドされたメタデータは、 SQLAlchemy の以前のバージョンで一般的でしたが、 ORM と非 ORM 操作が混在して使われたときに予期しない振舞いを引き起こす場合があるので、もはや初心者には推奨されません。
SQLAlchemy のセッションと Pylons のセッションを混同しないでください; それらは 2 つの異なるオブジェクトです! コントローラで使用される session オブジェクト (pylons.session) は、同じユーザによるウェブリクエストの間の状態を維持するためにウェブアプリケーションで使用される業界標準です。 SQLAlchemy のセッションは、メモリ上の ORM オブジェクトをデータベース上の対応するレコードと同期させるオブジェクトです。
この記事の Session 変数は SQLAlchemy のセッションオブジェクトではありません; それは "contextual session" クラスです。 それを呼び出すと、スレッドとミドルウェア問題を考慮に入れて、このウェブリクエストに適切な (新しいまたは既存の) セッションオブジェクトを返します。そのクラスメソッド (Session.commit()、Session.query(...) など) は、暗黙的に適切なセッションの対応するメソッドを呼びます。あなたは通常、ただ Session クラスメソッドを呼んで、内部のセッションオブジェクトを完全に無視することができます。 詳しい情報に関して SQLAlchemy マニュアルの "Contextual/Thread-local Sessions" を見てください。 これは SQLAlchmey 0.3 の SessionContext と同等ですが、 API が異なっています。
"Transactional" セッションは SQLAlchemy 0.4 の新機能です; これは私たちが Session.flush() の代わりに Session.commit() を使用している理由です。 sessionmaker への transactional と autoflush 引数はこれを可能にして、通常は一緒に使用されるはずです。
Contextual session mapper
あなたが SQLAlchemy 0.3 の "assign_mapper" 関数の同等物を探しているなら、これがその構文です:
1 2 | # In model/__init__.py, instead of the regular mapper calls. Session.mapper(MyClass, table1) |
これが何をするかに関する記述については、 Associating Classes and Mappers with a Contextual Session を見てください。 この方法は不注意なユーザを驚かせる不思議な振舞いを可能にするので、これを行う前にマッパー、クエリ、セッション、および scoped_session() を必ず理解してください。
複数のアプリケーションインスタンス
同じ WSGI プロセスの中で 同じ Pylons アプリケーションの複数のインスタンスを実行しているなら (例えば、Paste HTTPServerの "composite" アプリケーションによって)、 スコープ関数を加えなければなりません。 これは、アプリケーション・インスタンスが同じセッションオブジェクトを共有することを防ぎます。以下の関数をモデルに追加して、その関数を scoped_session の 2 番目の引数として渡してください:
1 2 3 4 5 6 | def pylons_scope(): import thread from pylons import config return "Pylons|%s|%s" % (thread.get_ident(), config._current_obj()) Session = scoped_session(sessionmaker(...), pylons_scope) |
XXX これが厳密に必要であるかどうかに関する議論が pylons-devel リストにあります。(あるアクションメソッドで threadlocal セッションが使用されているなら、別のインスタンスの同じアクションメソッドにはどんな害がある?) ここでは念の為にそのコードを残してあります。
スタンドアローンプログラムでのモデルの使用
XXX 手動で設定ファイルを読んで、エンジンをセットアップする方法を説明する。おそらく myapp/websetup.py からコードを借りて、モデルをインポートする前に設定をセットアップすることができます。どのバインドスタイルを選んだかによって、クエリを実行する前に Session.configure(bind=some_engine) をしなければならないかもしれません。
クックブックにはこのテーマに関する基礎をカバーするエントリがあります: Accessing your model outside of Pylons。
サードパーティのフロントエンド
Elixir
あなたが Ruby on Rails で使用される ActiveRecord になじみ深いなら、SQLAlchemy の上に Elixir のレイヤーを使うことができます。
2007-09-27 現在、 Elixir はバージョン 0.4 へ移行中です。 新しいバージョンが安定したときに、指示は加えられるでしょう。差し当たり、設定方法に関して このスレッド を見てください。
XXX Can you add more to this section?
Tesla
Tesla は Pylons と Elixir/SQLAlchemy の上に構築されたフレームワークです。
チュートリアル (これは最新版?)
XXX Can you add more to this section?