モデル

モデルについて

MVC パラダイムでは、 モデル はアプリケーションドメインに関する振舞い とデータを管理します。そして、状態に関する情報のリクエストに応じて、状 態を変えるための指示に応じます。

モデルはエンタープライズデータとビジネスルールを表します。 MVC デザイン パターンを使用するとき、大部分の処理はモデルで行われるます。データベー スは in the remit of the model 。それは EJBsColdFusion Components のようなコンポーネントオブジェクトです。

モデルの返すデータは表示中立です。すなわち、モデルはどんなフォーマット も適用されません。単一のモデルが任意の数の表示インタフェースに対してデー タを提供できます。 モデルコードは一度だけ書かれるので、これはコードの重 複を抑えて、すべてのビューで再利用されます。

モデルは何のフォーマットも適用しない状態でデータを返すので、同じコンポー ネントをあらゆるインタフェースで使うことができます。例えば、ほとんどの データは通常 HTML でフォーマットされますが、 Macromedia Flash や WAP で フォーマットすることもできます。

モデルはまた、状態管理とデータ永続を分割し、扱います。例えば、 Flash サ イトとワイヤレスアプリケーションが、ともに同じセッションベースのショッ ピングカートと電子商取引プロセスに依存することができます。

モデルは自己完結的であって、コントローラとビューからは独立しているので、 データ層やビジネスルールを変えるのはそれほど苦痛ではありません。 例えば、 データベースを MySQL から Oracle に切り換えたり、データソースを RDBMS から LDAP に変えたりする必要があると判明したら、唯一の必要なタスクはモ デルを変更することです。ビューが正しく書かれていれば、ユーザーリストが データベースから来たのか、それとも LDAP サーバから来たのか、それは全く 気にしないでしょう。

この自由度は、 MVC ベースのアプリケーションの 3 つの部分が ブラックボッ クス として振る舞い、それぞれの内部の作業が他から隠されていて、他の 2 つから独立していることから生じます。このアプローチは明確なインタフェー スと自己完結的なコンポーネントを促進します。

Note

adapted from an Oct 2002 TechRepublic article by by Brian Kotek: “MVC design pattern brings about better organization and code reuse” - http://articles.techrepublic.com.com/5100-10878_11-1049862.html

モデルの基本

Pylons は、データベースコードを置くために model パッケージを提 供していますが、データベースエンジンや API は提供しません。 代わりに、 選択できるいくつかのサードパーティ製 API があります。

SQL データベース

SQLAlchemy

SQLAlchemy は Pylons データベースのため の極めて一般的なアプローチです。 それはコネクションプール、 SQL 文ビル ダー、オブジェクト・リレーション・マッパー (ORM) 、およびトランザクショ ンサポートを提供します。 SQLAlchemy はいくつかのデータベースエンジン (MySQL 、 PostgreSQL 、 SQLite 、 Oracle 、 Firebird 、 MS-SQL 、 ODBC 経由の Access など) とともに働きます。そして、それぞれの独自の SQL 方言 を理解します。これにより、単にコネクション文字列を変えることによって、 あるエンジンから別のエンジンにプログラムを移植することが可能になります。 そのAPI はまだ徐々に変化していますが、 SQLAlchemy は十分テストされ、広 く普及しており、素晴らしいドキュメンテーションがあります。そして、メー リングリストでは素早く答えが返ってきます。 Using SQLAlchemy は、 Pylons アプリケーションを構成するお勧め の方法を述べます。

SQLAlchemy は次のような 3 つの異なったレベルで動かすことができ、しかも 同じアプリケーションの中で複数のレベルを混在させることもできます:

  • オブジェクトリレーションマッパー (ORM) は、 SQL コードを書く代わりに オブジェクトクラスを使用してデータベースと対話することを可能にします。
  • SQL 式言語には、カスタマイズされた SQL 文を作成するための多くのメソッ ドがあり、結果のカーソルは DBAPI のものより使いやすいです。
  • 低レベル execute メソッドは、 SQL ビルダーができないこと(既存のテーブ ルにカラムを追加することや、カラムの型を変更することなど) が見つかっ た場合に、リテラルの SQL 文字列を受け付けます。それらが結果を返すなら、 あなたはまだ SQLAlchemy の結果カーソルの利益を得ています。

最初の 2 つのレベルは データベース中立 です。その意味は、それらはデー タベースの SQL 方言の違いを隠すということです。異なるデータベースに変更 するのは、単に新しいコネクション URL を与えるだけです。 これに対する限 界がもちろんありますが、 SQLAlchemy はすべての SQL クエリを書き直すより 90% 簡単です。

SQLAlchemy マニュアル はここで カバーされなかった質問のために次に読むべきです。 それは、非常に良く書か れており網羅的です。

SQLAlchemy 拡張

これらの大部分は、テーブル定義と ORM クラス定義をワンステップで結合する か、 “active record” スタイルのアクセスをサポートすることで、より高レベ ルの ORM を提供します。 製品アプリケーションでこれらのショートカットを 使用する前に、「通常のやり方」で物事を行う方法を学ぶ時間を取ってくださ い 。これらの add-ons が舞台裏で何をしているのかを理解することで、デー タベースエラーの障害調査をしなければならない場合や、または add-on の層 での制限に対処しなければならない場合に役に立つでしょう。

SQLSoup は SQLAlchemy の拡張で、既存のデータベースのテーブルに基づいて ORM クラス を生成する迅速な方法を提供します。

Ruby on Rails で使われている ActiveRecord になじみ深いなら、 SQLAlchemy の上で Elixir 層を使用するとよ いかもしれません。

Tesla は Pylons と Elixir/SQLAlchemy の上に築き上げられたフレームワークです。

SQLAlchemy 以外のライブラリ

これらの大部分は object-relation マッパーだけを露出します。それらの SQL ビルダーとコネクションプールは直接使用されることは想定されていませ ん。

Storm

DB-API

上記のすべての SQL ライブラリは、 Python の DB-API の上に構築されていま す。 DB-API は MySQL 、 PostgreSQL 、 SQLite 、 Oracle 、Firebird 、 MS-SQL 、 ODBC 経由の Access など、いくつかのデータベースエンジンと対話 するための共通の低レベルインタフェースを提供します。 DB-API は低レベル で繰り返しが多く、コネクションプールを提供しないので、ほとんどのプログ ラマは DB-API を直接使用しません。それはソフトウェアというよりむしろ抽 象的なインタフェースなので、インストールするための「DB-APIパッケージ」 というものはありません。代わりに、あなたが興味のある特定のエンジンのた めの Python パッケージをインストールしてください。 Python Database Topic Guide は、 DB-API につ いて説明し、各エンジンのために必要とされるパッケージをリストします。 SQLite のための sqlite3 パッケージは Python 2.5 に含まれています。

オブジェクトデータベース

オブジェクトデータベースは、 Python 辞書、リスト、およびクラスを pickle 形式で保存できます。階層データをテーブル、リレーション、および外 国語 (SQL) に写像する代わりに、通常の Python 文を使用してそれらにアクセ スすることができます。

ZODB

Durus [1]

[1]Durus はスレッド・セーフではないので、アプリケーションがデータベー スに書き込むなら Durus のサーバモードを使用するべきです。スレッド間 でコネクションを共有してはいけません。 ZODB はスレッド・セーフなので、 それはより便利な代替手段になるかもしれません。

その他のデータベース

Pylons は以下のような他のデータベース・システムとも動かすことができます:

Schevo は、リレーショナルデータベースとオブジェ クトデータベースのいくつかの特徴を結合するために Durus を使用します。 それは Python で書かれています。

CouchDb はドキュメントベースのデータベースです。 それは Python API を特徴 としています。

Google App Engine の Datastore データベース。

SQLAlchemy を使う

SQLAlchemy のインストール

あなたが既に Pylons をインストールして、 easy_install コマンドを持っ ていると仮定します。 コマンドラインで、以下を実行してください:

easy_install SQLAlchemy

次に、データベースエンジンとその Python バインディングをインストールし なければなりません。 どれを選んだらよいか分からなければ、 SQLite は最初 に選択するのに良いものです。それは小さくて、インストールするのが簡単で あり、Python 2.5 はそのためのバインディングを含んでいます。 データベー スエンジンをインストールすることはこの記事の範囲を超えていますが、これ らは最もポピュラーなエンジンに必要とされる Python バインディングです:

easy_install pysqlite # If you use SQLite and Python 2.4 (not needed for Python 2.5)
easy_install MySQL-python # If you use MySQL
easy_install psycopg2 # If you use PostgreSQL

他のデータベースドライバーは Python Package Index (以前の Cheeseshop)を見てください。

バージョンをチェックする

SQLAlchemy のどのバージョンがインストールされているかを確認するために、 Python シェルに行き、 sqlalchemy.__version__ を見てください:

>>> import sqlalchemy
>>> sqlalchemy.__version__
0.5.0

Defining tables and ORM classes

Pylons プロジェクトを作るときに SQLAlchemy の質問に “yes” と答えた場合、 簡単なデフォルトモデルが構成されます。 モデルは 2 つのファイルから成り ます: __init__.pymeta.py です。 __init__.py はテーブル定義と ORM のクラス、およびアプリケーション開始時に呼ばなければならない init_model() 関数を含んでいます。 meta.py は単に SQLAlchemy のハ ウスキーピングのオブジェクト (Session, metadata, engine) の ためのコンテナです。これらはすべてのアプリケーションで使用するわけでは ないでしょう。アプリケーションが小さいなら、簡潔さのためにテーブル定義 を __init__.py に入れることができます。アプリケーションに多くのテーブ ルや複数のデータベースがあるなら、それらをモデルの中の複数のモジュール に分けると良いかもしれません。

ここに、サンプルの model/__init__.py と “persons” テーブルがあります (which is based on the default model with the comments removed):

"""The application's model objects"""
import sqlalchemy as sa
from sqlalchemy import orm

from myapp.model import meta

def init_model(engine):
    """Call me before using any of the tables or classes in the model"""
    ## Reflected tables must be defined and mapped here
    #global reflected_table
    #reflected_table = sa.Table("Reflected", meta.metadata, autoload=True,
    #                           autoload_with=engine)
    #orm.mapper(Reflected, reflected_table)
    #
    meta.Session.configure(bind=engine)
    meta.engine = engine


t_persons = sa.Table("persons", meta.metadata,
    sa.Column("id", sa.types.Integer, primary_key=True),
    sa.Column("name", sa.types.String(100), primary_key=True),
    sa.Column("email", sa.types.String(100)),
    )

class Person(object):
    pass

orm.mapper(Person, t_persons)

このモデルには、 変数 t_persons に割り当てられた 1 個のテーブル “persons” があります。 Person はマッパーを通してテーブルに結びつけ られた ORM のクラスです。

テーブルが既に存在しているなら、手動でカラム定義を指定する代わりにデー タベースからそれを読むことができます。 これはテーブルの リフレクション と呼ばれます。 利点は Python コードでカラムタイプを指定する必要がないと いうことです。リフレクションは生きたデータベースエンジンを必要とするた め、 init_model() の中で行わなければなりません。モジュールがインポー トされるときにエンジンは利用可能でないからです (エンジン とは、特定の データベースにどのように接続すればよいかを知っている SQLAlchemy のオブ ジェクトです)。ここに、リフレクションを使用した 2 番目の例があります:

"""The application's model objects"""
import sqlalchemy as sa
from sqlalchemy import orm

from myapp.model import meta

def init_model(engine):
    """Call me before using any of the tables or classes in the model"""
    # Reflected tables must be defined and mapped here
    global t_persons
    t_persons = sa.Table("persons", meta.metadata, autoload=True,
                         autoload_with=engine)
    orm.mapper(Person, t_persons)

    meta.Session.configure(bind=engine)
    meta.engine = engine


t_persons = None

class Person(object):
    pass

t_personsorm.mapper() 呼び出しがどのように init_model() に移動されたか、その一方で Person クラスを移動する 必要がなかったことに注意してください。また、 global t_persons 文に 注意してください。これは t_persons が関数外の大域変数であると Python に伝えます。関数内部で大域変数に代入するときは global が必要 です。単に mutable なオブジェクトを in place で変更するだけなら、それは 必要ではありません (これは meta を global と宣言する必要がない理由 です)。

SQLAlchemy 0.5 には、 1 ステップでテーブルと ORM クラスを定義するための オプションの Declarative (宣言的) 構文があります:

"""The application's model objects"""
import sqlalchemy as sa
from sqlalchemy import orm
from sqlalchemy.ext.declarative import declarative_base

from myapp.model import meta

_Base = declarative_base()

def init_model(engine):
    """Call me before using any of the tables or classes in the model"""
    meta.Session.configure(bind=engine)
    meta.engine = engine


class Person(_Base):
    __tablename__ = "persons"

    id = sa.Column(sa.types.Integer, primary_key=True)
    name = sa.Column(sa.types.String(100))
    email = sa.Column(sa.types.String(100))

関連の例

ここに、 Person クラスと Address クラス、そして people.my_addresses 上の多対他関連に関する例があります。詳細に関して は Relational Databases for People in a Hurry と SQLAlchemy マニュアルを見てください。

t_people = sa.Table('people', meta.metadata,
    sa.Column('id', sa.types.Integer, primary_key=True),
    sa.Column('name', sa.types.String(100)),
    sa.Column('email', sa.types.String(100)),
    )

t_addresses_people = sa.Table('addresses_people', meta.metadata,
    sa.Column('id', sa.types.Integer, primary_key=True),
    sa.Column('person_id', sa.types.Integer, sa.ForeignKey('people.id')),
    sa.Column('address_id', sa.types.Integer, sa.ForeignKey('addresses.id')),
    )

t_addresses = sa.Table('addresses', meta.metadata,
    sa.Column('id', sa.types.Integer, primary_key=True),
    sa.Column('address', sa.types.String(100)),
    )

class Person(object):
    pass

class Address(object):
    pass

orm.mapper(Address, t_addresses)
orm.mapper(Person, t_people, properties = {
    'my_addresses' : orm.relation(Address, secondary = t_addresses_people),
    })

スタンドアローンでモデルを使用する

ここまでで cron ジョブなどのスタンドアロンスクリプトでモデルを使用した り、インタラクティブにモデルをテストするために必要なものはすべて揃って います。あなたは、ただ SQLAlchemy engine を作成して、それをモデルに接続 する必要があります。 この例はカレントディレクトリ中の “test.sqlite” と いうデータベースを使用します:

% python
Python 2.5.1 (r251:54863, Oct 5 2007, 13:36:32)
[GCC 4.1.3 20070929 (prerelease) (Ubuntu 4.1.2-16ubuntu2)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import sqlalchemy as sa
>>> engine = sa.create_engine("sqlite:///test.sqlite")
>>> from myapp import model
>>> model.init_model(engine)

すると、 SLQAlchemy マニュアルで説明されるようにテーブル、クラス、およ び Session を使用できます。例えば:

#!/usr/bin/env python
import sqlalchemy as sa
import tmpapp.model as model
import tmpapp.model.meta as meta

DB_URL = "sqlite:///test.sqlite"

engine = sa.create_engine(DB_URL)
model.init_model(engine)

# Create all tables, overwriting them if they exist.
if hasattr(model, "_Base"):
    # SQLAlchemy 0.5 Declarative syntax
    model._Base.metadata.drop_all(bind=engine, checkfirst=True)
    model._Base.metadata.create_all(bind=engine)
else:
    # SQLAlchemy 0.4 and 0.5 syntax without Declarative
    meta.metadata.drop_all(bind=engine, checkfirst=True)
    meta.metadataa.create_all(bind=engine)

# Create two records and insert them into the database using the ORM.
a = model.Person()
a.name = "Aaa"
a.email = "aaa@example.com"
meta.Session.add(a)

b = model.Person()
b.name = "Bbb"
b.email = "bbb@example.com"
meta.Session.add(b)

meta.Session.commit()

# Display all records in the persons table.
print "Database data:"
for p in meta.Session.query(model.Person):
    print "id:", p.id
    print "name:", p.name
    print "email:", p.email
    print

設定ファイル

Pylons アプリケーションは、実行されるときにどのデータベースに接続するか を知る必要があります。 通常、 development.ini にこの情報を入れて、 environment.py でモデルを activate します。使用するデータベースに応じ て、以下を development.ini[app:main] セクションに置いてくださ い:

SQLite の設定

sqlalchemy.url = sqlite:///%(here)s/mydatabasefilename.sqlite

ここで mydatabasefilename.db は SQLite データベースファイルへのパスで す。”%(here)s” は development.ini ファイルを含むディレクトリを表します。 絶対パスを使用するなら、コロンの後に 4 つのスラッシュを使用してください: “sqlite:////var/lib/myapp/database.sqlite” 。カレントディレクトリが何で あるか分からないので、相対パス (スラッシュ 3 つ) は使用しないでください。 例では 3 つのスラッシュが使われていますが、これは “%(here)s” の値は常に スラッシュ (またはプラットホームの同等物; 例えば Windows では “C:\foo”) から始まるためです。

MySQL の設定

sqlalchemy.url = mysql://username:password@host:port/database
sqlalchemy.pool_recycle = 3600

ユーザ名、パスワード、ホスト (自分のマシン上であれば localhost)、ポート 番号 (通常は 3306)、およびデータベースの名前を入力してください。2 行目 は engine オプション を 設定する例です。

MySQL の場合、 “MySQL server has gone away” エラーを防ぐために “pool_recycle” をセットすることは重要です。これは、 MySQL がアプリケー ションに知らせずに idle なデータベースコネクションを自動的に閉じるから です。 コネクション存続期間を 3600 秒 (1時間) に設定することで、コネク ションがidle であると MySQL が判断する前に有効期限が切れて再接続するよ うになります。

SQL ログを有効にするのに ”.echo” オプションを使いたくなるかもしれません が、それは重複するログ出力を引き起こすので使わないようにしてください。 代わりに下の “Logging” セクションを見て、 MySQL ログを Paste のログシス テムに統合してください。

PostgreSQL の設定

sqlalchemy.url = postgres://username:password@host:port/database

ユーザ名、パスワード、ホスト (自分のマシン上なら localhost)、ポート番号 (通常は 5432)、およびデータベースの名前を入力してください。

エンジン

myapp/config/environment.py の先頭にこれを置いてください:

from sqlalchemy import engine_from_config
from myapp.model import init_model

そしてこれを load_environment 関数に置いてください:

engine = engine_from_config(config, 'sqlalchemy.')
init_model(engine)

2番目の引数は検索する接頭語です。キーが “sqlalchemy.default.url” という 名前なら、これは “sqlalchemy.default.” になります。設定ファイルとこの関 数呼び出しの間で一貫している限り、接頭語は何でも構いません。

コントローラ

paster create SQLAlchemy オプションは myapp/lib/base.py (ベースコント ローラ) の先頭に以下を追加します:

from myapp.model import meta

そして、 .__call__ メソッドを以下のように変更します:

def __call__(self, environ, start_response):
    try:
        return WSGIController.__call__(self, environ, start_response)
    finally:
        meta.Session.remove()

.remove() メソッドは、現在のウェブリクエストにおける ORM データのあらゆ る残り物が捨てられるようにします。これは通常ガーベージコレクションの product として自動的に起こりますが、.remove() を呼ぶことでそれを確実に します。

データベースを構築する

データベースに実際にテーブルを作成するために、メタデータの .create_all() メソッドを呼びます。インタラクティブにこれをするか、ま たは paster のアプリケーション初期化機能を使用できます。これをするた めに、 myapp/websetup.py にコードを追加します。 load_environment() 呼び出しの後に、以下を置いてください:

from myapp.model import meta
log.info("Creating tables")
meta.metadata.drop_all(bind=meta.engine, checkfirst=True)
meta.metadata.create_all(bind=meta.engine)
log.info("Successfully setup")

または、 SQLAlchemy 0.5 の新しい Declarative 構文に対しては:

from myapp import model
log.info("Creating tables")
model._Base.metadata.drop_all(bind=meta.engine, checkfirst=True)
model._Base.metadata.create_all(bind=meta.engine)
log.info("Successfully setup")

そしてコマンドラインから以下を実行します:

$ paster setup-app development.ini

データのクエリと修正

Important

このセクションは、コードを高レベルのモデル関数に入れること を想定しています。 コントローラメソッドに直接コードを入れるなら、 モデルで定義されたあらゆるオブジェクトの前に model. を置くか、オ ブジェクトを個別にインポートする必要があるでしょう。また、ここでの Session オブジェクト (大文字の s) が、コントローラにおける Beaker session オブジェクト (小文字の s) と同じでないことに注意し てください。

これは、新しいデータをデータベースに入力する方法です:

mr_jones = Person()
mr_jones.name = 'Mr Jones'
meta.Session.add(mr_jones)
meta.Session.commit()

ここで mr_jonesPerson のインスタンスです。そのプロパティが t_people のカラムに対応していて、選択された行からのデータを含んでいま す。より精巧なアプリケーションには、引数に基づいて自動的に属性を設定す る Person.__init__ メソッドがあるでしょう。

コントローラメソッドでデータベースエントリをロードして、性別の変化を実 行して、それを保存する例です:

person_q = meta.Session.query(Person) # An ORM Query object for accessing the Person table
mr_jones = person_q.filter(Person.name=='Mr Jones').one()
print mr_jones.name # prints 'Mr Jones'
mr_jones.name = 'Mrs Jones' # only the object instance is changed here ...
meta.Session.commit() # ... only now is the database updated

エントリのリストを返すのに、以下を使用してください。

all_mr_joneses = person_q.filter(Person.name=='Mr Jones').all()

テーブルのすべての人のすべてのリストを得るには、以下を使用してください。

everyone = person_q.all()

id で検索する場合:

someuser = person_q.get(5)

もっと簡単に、すべての人に対して繰り返すことができます:

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

エントリーを削除するには、以下を使用してください:

mr_jones = person_q.filter(Person.name=='Mr Jones').one()
meta.Session.delete(mr_jones)
meta.Session.commit()

join されたオブジェクトを使う

my_addresses プロパティは Address オブジェクトのリストであったこと を思い出してください。

print mr_jones.my_addresses[0].address # prints first address

‘Mr Jones’ に既存のアドレスを加えるためには、以下をします:

address_q = meta.Session.query(Address)

# Retrieve an existing address
address = address_q.filter(Address.address=='33 Pine Marten Lane, Pleasantville').one()

# Add to the list
mr_jones.my_addresses.append(new_address)

# issue updates to the join table
meta.Session.commit()

‘Mr Jones’ にまったく新しいアドレスを追加するために、以下をします:

new_address = Address() # Construct an empty address object
new_address.address = '33 Pine Marten Lane, Pleasantville'
mr_jones.my_addresses.append(new_address) # Add to the list
meta.Session.commit() # Commit changes to the database

変更を行った後に、 meta.Session.commit() を呼んでそれらをデータベース に永久に格納する必要があります。さもなければ、それらはウェブリクエスト の終わりに捨てられるでしょう。 また、コミットされていないあらゆる変更を 元に戻すために、いつでも meta.Session.rollback() を呼ぶことができます。

join されたオブジェクトを対象に検索するために、クエリとしてオブジェクト 全体を渡すことができます:

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()

クエリオブジェクトのすべての属性がマッチしなければなりません。

または、 join されたオブジェクトのプロパティを検索することができます。

residents_at_33_pine_marten_lane = \
 person_q.join('my_addresses').filter(
    Address.address=='33 Pine Marten Lane, Pleasantville').all()

上記の近道は any() を使用することです:

residents_at_33_pine_marten_lane = \
 person_q.filter(Person.my_addresses.any(
    Address.address=='33 Pine Marten Lane, Pleasantville')).all()

Mr Jones からアドレスを分離するために、以下をします:

del mr_jones.my_addresses[0] # Delete the reference to the address
meta.Session.commit()

address テーブルのアドレス自体を削除するために、通常 Address オブジェ クト自体のために別々の delete() を発行しなければなりません:

meta.Session.delete(mr_jones.my_addresses[0]) # Delete the Address object
del mr_jones.my_addresses[0]
meta.Session.commit() # Commit both operations to the database

しかしながら、 SQLAlchemy は上の操作のために近道をサポートします。マッ パーリレーションを構成する際に、代わりに cascade = “all, delete-orphan” を使用してください:

orm.mapper(Address, t_addresses)
orm.mapper(Person, t_people, properties = {
'my_addresses': orm.relation(
        Address, secondary=t_addresses_people, cascade="all,delete-orphan"),
})

すると、 mr_jones.my_addresses から取り除かれた項目は、データベースか らも自動的に削除されます:

del mr_jones.my_addresses[0] # Delete the reference to the address,
                             # also deletes the Address
meta.Session.commit()

マッパーのどんな関係にも、 join が削除されたときに join されたオブジェ クトも同時に削除されるように relation() の追加の引数として`cascade = “all, delete-orphan”` を渡すことができます。従って上の delete() 操作は 必要ありません。 my_addresses リストから削除するだけです。ただし、そ の名前にもかかわらず delete-orphan は、別のオブジェクトがそれに join していたとしても、 join されたオブジェクトを取り除くことに注意してくだ さい

非 ORM SQL クエリ

セッションのトランザクション中で 非 ORM SQL クエリを実行するには、 meta.Session.execute() を使用してください。 bulk update と delete は、 クエリでループして ORM インスタンスを変更するよりかなり速くレコードを変 更できます。

q = sa.select([table1.c.id, table1.c.name], order_by=[table1.c.name])
records = meta.Session.execute(q).fetchall()

# Example of a bulk SQL UPDATE.
update = table1.update(table1.c.name=="Jack")
meta.Session.execute(update, name="Ed")
meta.Session.commit()

# Example of updating all matching records using an expression.
update = table1.update(values={table1.c.entry_id: table1.c.entry_id + 1000})
meta.Session.exececute(update)
meta.Session.commit()

# Example of a bulk SQL DELETE.
delete = table1.delete(table1.c.name.like("M%"))
meta.Session.execute(delete)
meta.Session.commit()

# Database specific, use only if SQLAlchemy doesn't have methods to construct the desired query.
meta.Session.execute("ALTER TABLE Foo ADD new_column (VARCHAR(255)) NOT NULL")

Warning

最後の例は、データベース構造を変えるので、ORM 操作に悪影響を及ぼす かもしれません。

Further reading

Query オブジェクトには、条件によるフィルタリング、結果の並び替え、グルー ピングを含む他の多くの特徴があります。これらは SQLAlchemy マニュアルに 優れた説明があります。 特に Data MappingSession / Unit of Work の章を見てく ださい。

モデルをテストする

通常のモデルの使い方はモデルテストでも同様にうまく働きますが、メタデー タを使用するためには、そのためのエンジンコネクションを指定しなければな りません。プロジェクトの中で毎回ユニットテストの度にテーブルを作成する ために、以下のような test_models.py を使用してください。

from myapp.tests import *
from myapp import model
from myapp.model import meta

class TestModels(TestController):

    def setUp(self):
        meta.Session.remove()
        meta.metadata.create_all(meta.engine)

    def test_index(self):
        # test your models
        pass

Note

テストが TestController から派生されることに注意してください。 これ は、モデルが動くようにアプリケーションがセットアップされることを保 証するためのものです。

“nosetests –with-pylons=/path/to/test.ini ...” は、テストが実行される 前にモデルが適切に初期化されるのを保証する別の方法です。これは非コント ローラテストを実行するときに使用できます。

複数のエンジン

いくつかのアプリケーションでは、複数のデータベース (エンジン) に接続す る必要があります。 あるアプリケーションは、特定のテーブルをいつも同じエ ンジンに bind します (例えば、一般的なデータベースとログデータベース)。 これは「水平パーティショニング」と呼ばれます。 他のアプリケーションは、 同じ構造を持ついくつかのデータベースを持っていて、現在のリクエストによっ て、どれかを選びます。 例えば、それぞれのブログのための別々のデータベー スを持ったウェブログアプリです。 大きなアプリケーションでは、データベー スサイズが大きくなり過ぎるのを防ぐために、同じ論理的なテーブルからの別 のレコードを別のデータベースに保存します。これは「垂直パーティショニン グまたは sharding」と呼ばれます。 上のパターンはいくつかのマイナーチェ ンジがあるこれらの体系のいずれにも対応できます。

まず最初に、設定ファイルに複数のエンジンをこのように定義することができ ます:

sqlalchemy.default.url = "mysql://..."
sqlalchemy.default.pool_recycle = 3600
sqlalchemy.log.url = "sqlite://..."

これは 2 つのエンジン “default” および “log” を、それぞれに固有のオプショ ンで定義しています。 次に、使用するすべてのエンジンをインスタンス化しな ければなりません。

default_engine = engine_from_config(config, 'sqlalchemy.default.')
log_engine = engine_from_config(config, 'sqlalchemy.log.')
init_model(default_engine, log_engine)

もちろん、 init_model() が両方の引数を受け取って 2 つのエンジンを作成 するように変更しなければならないでしょう。

異なるテーブルを異なるデータベースに bind するが、いつも特定のテーブル が同じエンジンに bind されるようにするには、 sessionmaker の引数とし て bind ではなく binds を使用してください。

binds = {"table1": engine1, "table2": engine2}
Session = scoped_session(sessionmaker(binds=binds))

リクエスト毎に binding を選択するなら、 sessionmaker の bind(s) 引数を 省略して、代わりにベースコントローラの __call__ メソッドのスーパーク ラス呼び出しの前か、特定のアクションメソッドで、直接これを実行してくだ さい:

meta.Session.configure(bind=meta.engine)

binds= はここでも同じように働いています。

コーディングスタイル、セッションオブジェクト、 bind されたメタデータに関する議論

すべての ORM 操作は Session とエンジンを必要とします。 すべての非 ORM SQL 操作は、エンジンを必要とします。 (厳密に言うと、それらは代わり にコネクションを使用できますが、それはこのチュートリアルの範囲を超えて います。) 実際のデータベースクエリを行うあらゆる SQLAlchemy メソッドに 対して`bind=` 引数でエンジンを渡すか、またはセッションまたはメタデータ にエンジンを bind することができます。このチュートリアルは、それが最も 柔軟性があるので、上の “Multiple Engines” セクションで示されるように、 セッションを bind することを勧めます。

MetaData(engine) 構文を使用することで、メタデータをエンジンに bind し たり、 metadata.bind = engine で binding を変えることも可能です。これ は autoload_with 引数なしにオートロードをできるようにします。そして、 エンジンまたはセッションを指定することなく特定の SQL 操作を実行できるよ うにします。 bind されたメタデータは 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” を見てください。これは SQLAlchemy 0.3 の SessionContext と同等のものですが、 API が異なっています。

「トランザクション」セッションは SQLAlchemy 0.4 の新機能です。 これは私 たちが Session.flush() の代わりに Session.commit() を使用している理 由です。 sessionmaker に対する autocommit=False (SQLALchemy 0.4 で は transactional=True) と autoflush=True 引数 (これはデフォルトです) はこれを可能にして、通常それらは一緒に使用されるはずです。

Fancy classes

これは、いくつかの追加機能を持つ ORM クラスです:

class Person(object):

    def __init__(self, firstname, lastname, sex):
        if not firstname:
            raise ValueError("arg 'firstname' cannot be blank")
        if not lastname:
            raise ValueError("arg 'lastname' cannot be blank")
        if sex not in ["M", "F"]:
            raise ValueError("sex must be 'M' or 'F'")
        self.firstname = firstname
        self.lastname = lastname
        self.sex = sex

    def __repr__(self):
        myclass = self.__class__.__name__
        return "<%s %s %s>" % (myclass, self.firstname, self.lastname)
        #return "%s(%r, %r)" % (myclass, self.firstname, self.lastname, self.sex)
        #return "<%s %s>" % (self.firstname, self.lastname)

    @property
    def name(self):
        return "%s %s" % (self.firstname, self.lastname)

    @classmethod
    def all(cls, order=None, sex=None):
        """Return a Query of all Persons. The caller can iterate this,
        do q.count(), add additional conditions, etc.
        """
        q = meta.Session.query(Person)
        if order and order.lower().startswith("d"):
            q = q.order_by([Person.birthdate.desc()])
        else:
            q = q.order_by([Person.lastname, Person.firstname])
        return q

    @classmethod
    def recent(self, cutoff_days=30):
        cutoff = datetime.date.today() - datetime.timedelta(days=cutoff_days)
        q = meta.Session.query(Person).order_by(
                [Person.last_transaction_date.desc()])
        q = q.filter(Person.last_transaction_date >= cutoff)
        return q

このクラスを使うと、コンストラクタ引数と共に新しいレコードを作成できま す。 これは、便利であるだけでなく、レコードが有効なデータによって始めら れることを確実にします (空の required フィールドがありません)。データベー スから既存のレコードをロードするときは . __init__ が呼ばれないので、 それは干渉しません。インスタンスは読みやすく表示されます。そして、書き 込み禁止のプロパティが複数のフィールドから計算されます。

クラスメソッドはコントローラに、高レベルのクエリを返します。 クラスメソッ ドが好きでないなら、そのための別々の PersonSearch クラスを作ることが できます。 そのメソッドはそれを保存した myapp.model.meta モジュールか らセッションを得ます。 このモジュールが直接 Session オブジェクトをイ ンポートせずに`meta` モジュールをインポートしたことに注意してください。 init_model()Session オブジェクトを置き換えるので、直接 Session オブジェクトをインポートすると現在の値ではなく元の値を得るた めです。

あなたは、隠れたカラムの読み書き可能プロパティや orm.mapper 呼び出し にデフォルトの並び順を指定することなど、 SQLAlchemy のずっと多くのこと をすることができます。 latitudelongitude が異なるテーブルカラム から来る person.location.latitudeperson.location.longitude のよ うな合成プロパティを作ることができます。リストや辞書に見えるが、あるテー ブルに関連しているクラスを作ることができます。これらのプロパティのいく つかは Pylons の通常のプロパティのメカニズムで作ることができます。その 他は orm.mapperproperty 引数で実現できます。そして gazoo との関 連を lazy に読み込むか (gazoo を関連の one side にあまり使用しないなら)、 または eager に読み込む (クエリの数を最小にするために) ことができます。 (後者だけがSQL join を使用します) クラスの中のあるカラムを lazy に読み 込ませることができます。ただし SQLAlchemy では、これを “lazy” ではなく “deferred” と呼んでいます。カラムまたは関連するテーブルがアクセスされた とき、SQLAlchemy は自動的にそれらを読み込むでしょう。

fancy classes に対してより巧妙なアイデアがあれば、この記事にコメントを 追加してください。

ログ出力

SQLAlchemy は、操作の様々な側面について chat するいくつかのロガーを持っ ています。実行されたすべての SQL 文をそのパラメタ値と共にログに記録する には、 development.ini に以下を入れてください:

[logger_sqlalchemy]
level = INFO
handlers =
qualname = sqlalchemy.engine

次に、新しいロガーを有効にするように “[logger]” セクションを変更します:

[loggers]
keys = root, myapp, sqlalchemy

SQL 文の結果をログに記録するには、レベルを DEBUG に設定してください。 これは大量の出力を引き起こす場合があります! SQL を登録するのを止めるに は、レベルを WARN か ERROR に設定してください。

SQLAlchemy には、同様の方法で構成できる他のロガーがいくつかあります。 “sqlalchemy.pool” レベル INFO は、コネクションがエンジンのコネクション プールからいつ調べられるか、そして、それらがいつ返されるかを伝えます。 “sqlalchemy.orm” と buddies は様々な ORM 操作を記録します。 SQLAlchemy マニュアルの “Configuring Logging” を見てください。

複数のアプリケーションインスタンス

同じ Pylons アプリケーションの複数のインスタンスを同じ WSGI プロセス で (例えば、 Paste HTTPServerの “composite” アプリケーションで) 実行し ているなら、並行性問題に出くわすでしょう。この問題は、 Session はスレッドローカルであってアプリケーションインスタンス ローカルではないということです。ベースコントローラの中で Session.remove() が適切に呼ばれるなら、これが実際にはどれほど問題に なるかはっきりしませんが、もしそれが問題になる場合、可能な療法がありま す:

  1. meta モジュールの代わりに pylons.app_globals (別名 config["pylons.app_globals"]) にエンジンを取り付けます。 globals オブジェ クトはアプリケーションインスタンスの間で共有されません。
  1. スコープ関数を加えます。 これは、アプリケーションインスタンスが同じ セッションオブジェクトを共有するのを防ぎます。 以下の関数をモデルに 追加してください。そして、scoped_session に対する 2 番目の引数とし てそれを渡してください:
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)

これによって影響を受けるか、または影響を受けると思うなら、それを pylons-discuss メーリングリストに提起してください。ここでのアドバイスが 正しいことを検証するために、私たちはこの状況に直面している実際のユーザ からのフィードバックを必要としています。

Read the Docs v: v1.0.1rc1
Versions
latest
v1.0.1rc1
v0.9.7
Downloads
PDF
HTML
Epub
On Read the Docs
Project Home
Builds

Free document hosting provided by Read the Docs.