モデル

フレームワークはモデルとは最小限にしか関連しないので、 Pyramid と Pylons でモデルは本質的に同じです (これは、例えば Django とは異なります。 Django は ORM (オブジェクト・リレーショナル・マッパー) がフレームワーク に特有で、フレームワークの他の部分はそれが特定の種類であると仮定します)。 Pyramid と Pylons では、アプリケーションスケルトンは単にどこにモデルを 置くべきかを示唆し、 SQLAlchemy データベース接続を初期化します。以下は、 デフォルトの Pyramid 設定です (コメントは除いて、インポートは省略しています):

 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
# pyramidapp/__init__.py
from sqlalchemy import engine_from_config
from .models import DBSession

def main(global_config, **settings):
    engine = engine_from_config(settings, 'sqlalchemy.')
    DBSession.configure(bind=engine)
    ...


# pyramidapp/models.py
from sqlalchemy import Column, Integer, Text
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import scoped_session, sessionmaker
from zope.sqlalchemy import ZopeTransactionExtension

DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension()))
Base = declarative_base()

class MyModel(Base):
    __tablename__ = 'models'
    id = Column(Integer, primary_key=True)
    name = Column(Text, unique=True)
    value = Column(Integer)

    def __init__(self, name, value):
        self.name = name
        self.value = value

そして INI ファイルは:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# development.ini
[app:main]

# Pyramid only
pyramid.includes =
    pyramid_tm

# Pyramid and Pylons
sqlalchemy.url = sqlite:///%(here)s/PyramidApp.db


[logger_sqlalchemy]

# Pyramid and Pylons
level = INFO
handlers =
qualname = sqlalchemy.engine
# "level = INFO" logs SQL queries.
# "level = DEBUG" logs SQL queries and results.
# "level = WARN" logs neither.  (Recommended for production systems.)

それは Pylons と次のような違いがあります:

  1. ZopeTransactionExtension および “pyramid_tm” tween 。
  2. “model” (単数) の代わりに “models” (複数)。
  3. サブパッケージではなくモジュール。
  4. “Session” の代わりに “DBSession” 。
  5. init_model() 関数はありません。
  6. わずかに異なるインポートスタイルおよび変数の命名規則。

最初のものだけが本質的な違いです; 残りは単に審美的なプログラミング スタイルです。したがって、何も傷つけることなくそれらを変更することができます。

model と models の違いは、単語「モデル」の使われ方の曖昧性によります。 ある人々は、個々の ORM クラスを指して “a model” と呼び、その一方で他の 人々は、アプリケーション内の ORM クラスの全コレクションを指して “the model” と呼びます。このガイドでは単語「モデル」を両方の意味で使用します。

何がモデルに属しているか

良いプログラミングプラクティスは、データクラスとユーザインタフェース クラスを分離しておくことを推奨します。このように、ユーザインタフェース はデータに影響せずに変わることがあり、その逆もあります。モデルはデータ クラスの行き先です。例えば、モノポリーゲームにはプレーヤー、ボード、 ゲーム盤、不動産権利書、カードなどがあります。したがって、モノポリー プログラムは、おそらくこれら各々のためのクラスを持つでしょう。 アプリケーションが実行間でのデータの保存 (永続化) を必要とすれば、 データはデータベースまたはそれに相当するものに保存されなければならない でしょう。 Pyramid は様々なデータベースタイプで動作することができます: SQL データベース、オブジェクトデータベース、 Key-Value データベース (“NoSQL”)、ドキュメントデータベース (JSON や XML)、 CSV ファイルなど。 最も一般的な選択は SQLAlchemy です。したがって、それは Pyramid と Pylons によって提供される最初の構成です。

最低限、モデルに ORM クラスを定義する必要があります。さらに、関数、 クラスメソッドあるいはクラスメソッドの形で任意のビジネスロジックを 加えることもできます。特定のコード片がモデルに属するかビューに属するかを 伝えることは時に困難ですが、それはあなたに任せます。

別の原理は、モデル単独で使用できるように、モデルはアプリケーションの 残りに依存すべきでないということです; 例えば、ユーティリティープログラム、 あるいは他のアプリケーションの中で。またそれによって、フレームワークまたは アプリケーションが壊れた時にデータを抽出することが可能になります。 したがって、ビューはモデルのことを知っていますが、逆は真ではありません。 必ずしも誰もがこれに賛成するとは限りませんが、手始めとしては良い考えです。

より大きなプロジェクトは、多数のウェブアプリケーションと非ウェブ プログラムの間の統一モデルを共有するかもしれません。その場合、個別の トップレベルのパッケージにモデルを入れて、 Pyramid アプリケーションへ それをインポートすることは意味のあることです。

トランザクションマネージャ

Pylons はトランザクションマネージャを使用していませんが、 TurboGears と Zope においてそれは一般的です。トランザクションマネージャはあなたのために コミット- ロールバック・サイクルの面倒を見ます。上記の両方のアプリケーション でのデータベースセッションは スコープ セッションです。その意味は、 それは threadlocal グローバル変数で、すべてのリクエストの終わりにそれを 空にしなければならないということです。Pylons アプリでは、ベースコントローラ の中にセッションを空にする特別なコードがあります。トランザクション マネージャはこれをさらに一歩進め、リクエストの間に行なわれたすべての変更 をコミットするか、またはリクエストの間に例外が上げられた場合、変更をロール バックします。ビューがいつ/どのようにコミットが生じるかをカスタマイズ したい場合、 ZopeTransactionExtension はモジュールレベルの API を提供します。

結論は、ビューメソッドが DBSession.commit() を呼ぶ必要がないという ことです: トランザクションマネージャが代わりにそれをします。さらに、例外が 生じればトランザクションマネージャが DBSession.rollback() を呼ぶので、 変更を try-except ブロックに置く必要はありません (多くの Pylons アクション はこれをしないので、それらは厳密には正しくありません)。 副作用は、 DBSession.commit() あるいは DBSession.rollback() を直接呼ぶこと ができないということです。何かがいつコミットされるか正確にコントロール したければ、このようにそれをしなければなりません:

1
2
3
4
5
import transaction

transaction.commit()
# Or:
transaction.rollback()

さらに transaction.doom() 関数もあります。これは、アプリケーションの 他の部分によって行なわれるものを含めてこのリクエストの間の あらゆる データベース書き込みを防ぐために呼ぶことができるものです。 もちろん、これは、既にコミットされた変更には影響しません。

“commit veto” 関数を定義することにより、自動ロールバックが生じる状況を カスタマイズすることができます。これは pyramid_tm ドキュメンテーションに 述べられています。

トラバーサルをモデルとして使う

Pylons はトラバーサルモードを持っていません。したがって、ビューコード 中のデータベースオブジェクトを取って来なければなりません。Pyramid の トラバーサルモードは、本質的にこれを自動的に行います。オブジェクトを context としてビューに渡し、 “not found” を扱います。トラバーサル リソースツリーはこのように、 models とは独立した、ほとんど 2 番目の 種類のモデルのように見えます (それは典型的に resources モジュールに 定義されています)。これは問題を提起します。 2 つの間の違いは何ですか? 私のモデルをトラバーサルや route の管理下でのトラバーサルに変換すること に意味がありますか? これは認可に関して特に問題になります。なぜなら Pyramid のデフォルトの認可機構が context オブジェクトに取り付けられた 許可 (アクセスコントロールリストあるいは ACL) に対して設計されている からです。これらは高度な質問なので、ここではそれらを扱いません。 トラバーサルには学習曲線があります。また、異なる種類のアプリケーション にとって適切かもしれないし、適切ではないかもしれません。とはいえ、 それが存在するということを知っておくのは良いことです。そうすれば、 時間をかけて徐々にそれを調査して、おそらくいつかその使い道を見つける ことができるでしょう。

SQLAHelper と “models” サブパッケージ

Akhet の初期バージョンでは、エンジンとセッションを構成するために SQLAHelper ライブラリを使用していました。これはあまり有益ではないため、 もはや文書化されません。覚えておくべき重要なことは、 models.py を パッケージに分割した場合、循環インポートに用心する必要がある、 ということです。 models/__ini__.pyBaseDBSession を 定義して、サブモジュールでそれらをインポートして、さらに init モジュール がサブモジュールをインポートすれば、互いにインポートする2つのモジュール の循環インポートが発生します。あるモジュールのグローバルコードを実行 している間、別のモジュールは semi-empty に見えるでしょう。 これは例外を引き起こす可能性があります。

Pylons は、サブモジュール (models/meta.py) に Base と Session を置き、 このモジュールでは他のモデルのモジュールをインポートしないことでこの問題 に対処しました。 SQLAHelper は、エンジン、セッションおよびベースクラスを 格納するためのサードパーティライブラリを提供することにより、この問題に 対処します。 Pyramid 開発者は、モデル全体を 1 つのモジュール中に置く最も 単純な場合をデフォルトすることに決定しました。そして、あなたがそうした ければモデルを分割する方法が分かるようにしました。

モデルの例

これらの例はしばらく前に書かれたため、トランザクションマネージャを使って いません。また、第三のインポート形式がまだ残っています。これらは SQLAlchemy 0.6, 0.7, 0.8 で動くはずです。

単純な1テーブルのモデル

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import sqlalchemy as sa
import sqlalchemy.orm as orm
import sqlalchemy.ext.declarative as declarative
from zope.sqlalchemy import ZopeTransactionExtension as ZTE

DBSession = orm.scoped_session(om.sessionmaker(extension=ZTE()))
Base = declarative.declarative_base()

class User(Base):
    __tablename__ = "users"

    id = sa.Column(sa.Integer, primary_key=True)
    name = sa.Column(sa.Unicode(100), nullable=False)
    email = sa.Column(sa.Unicode(100), nullable=False)

このモデルには1つの ORM クラス (データベーステーブル users に対応 する User) があります。テーブルには3つのカラムがあります: id, name, user です。

3テーブルのモデル

上記のモデルを、中規模のアプリケーションに適した 3 テーブルモデルへと 拡張することができます。

 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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import sqlalchemy as sa
import sqlalchemy.orm as orm
import sqlalchemy.ext.declarative as declarative
from zope.sqlalchemy import ZopeTransactionExtension as ZTE

DBSession = orm.scoped_session(om.sessionmaker(extension=ZTE()))
Base = declarative.declarative_base()

class User(Base):
    __tablename__ = "users"

    id = sa.Column(sa.Integer, primary_key=True)
    name = sa.Column(sa.Unicode(100), nullable=False)
    email = sa.Column(sa.Unicode(100), nullable=False)

    addresses = orm.relationship("Address", order_by="Address.id")
    activities = orm.relationship("Activity",
        secondary="assoc_users_activities")

    @classmethod
    def by_name(class_):
        """Return a query of users sorted by name."""
        User = class_
        q = Session.query(User)
        q = q.order_by(User.name)
        return q


class Address(Base):
    __tablename__ = "addresses"

    id = sa.Column(sa.Integer, primary_key=True)
    user_id = foreign_key_column(None, sa.Integer, "users.id")
    street = sa.Column(sa.Unicode(40), nullable=False)
    city = sa.Column(sa.Unicode(40), nullable=False)
    state = sa.Column(sa.Unicode(2), nullable=False)
    zip = sa.Column(sa.Unicode(10), nullable=False)
    country = sa.Column(sa.Unicode(40), nullable=False)
    foreign_extra = sa.Column(sa.Unicode(100, nullable=False))

    def __str__(self):
        """Return the address as a string formatted for a mailing label."""
        state_zip = u"{0} {1}".format(self.state, self.zip).strip()
        cityline = filterjoin(u", ", self.city, state_zip)
        lines = [self.street, cityline, self.foreign_extra, self.country]
        return filterjoin(u"|n", *lines) + u"\n"


class Activity(Base):
    __tablename__ = "activities"

    id = sa.Column(sa.Integer, primary_key=True)
    activity = sa.Column(sa.Unicode(100), nullable=False)


assoc_users_activities = sa.Table("assoc_users_activities", Base.metadata,
    foreign_key_column("user_id", sa.Integer, "users.id"),
    foreign_key_column("activities_id", sa.Unicode(100), "activities.id"))

# Utility functions
def filterjoin(sep, *items):
    """Join the items into a string, dropping any that are empty.
    """
    items = filter(None, items)
    return sep.join(items)

def foreign_key_column(name, type_, target, nullable=False):
    """Construct a foreign key column for a table.

    ``name`` is the column name. Pass ``None`` to omit this arg in the
    ``Column`` call; i.e., in Declarative classes.

    ``type_`` is the column type.

    ``target`` is the other column this column references.

    ``nullable``: pass True to allow null values. The default is False
    (the opposite of SQLAlchemy's default, but useful for foreign keys).
    """
    fk = sa.ForeignKey(target)
    if name:
        return sa.Column(name, type_, fk, nullable=nullable)
    else:
        return sa.Column(type_, fk, nullable=nullable)

このモデルには users テーブルに対応する User クラス、 addresses テーブルに対応する Address クラス、 activities テーブルに対応する Activity クラスがあります。 usersaddresses と 1:Many 関連を持っています。 users はさらに、関連 テーブル assoc_users_activities を使用して activitiesMany:Many 関係を持っています。これは SQLAlchemy の “declarative” 構文です。それは宣言的な Base クラスからサブクラス化された ORM クラス としてテーブルを定義します。関連テーブルは SQLAlchemy に ORM クラスを 持っていません。したがって、あたかも declarative を使用していないかのように Table コンストラクタを使用してそれを定義します。しかし、それはやはり Base の「メタデータ」に紐付けられます。

Address.__str__ メソッドのように、 ORM クラスにインスタンスメソッド を加えることができます。そうするとそれらは 1 つのデータベース・レコードで 有効になります。さらに、 User.by_name メソッドのように、いくつかの レコード上で動作するか、クエリオブジェクトを返すクラスメソッドを定義する ことができます。

User.by_name がクラスメソッドとスタティックメソッドのどちらの方が よりうまく機能するかということに関して、多少の意見の不一致があります。 通常クラスメソッドでは、最初の引数は class_ または cls あるいは klass と呼ばれます。そしてメソッド全体でそれが使われます。 しかし ORM クエリの中では、そのメソッドは固有の名前で ORM クラスを参照 する方が普通です。しかし、それをすると class_ 変数を使用しません。 そうであれば、スタティックメソッドではいけないのでしょうか? しかし、 通常のスタティックメソッドとは異なり、そのメソッドはクラスに属します。 私はこの考えを行ったり来たりして、メソッドの初めで User = class_ を 割り当てることがあります。しかし、これらの方法のどれも完全に満足とは 感じません。したがって、私はどれが最良か分かりません。

共通ベースクラス

すべての ORM クラスに対するスーパークラスを定義して、すべてのサブクラス で使用できる共通のクラスメソッドを持たせることができます。それは declarative base の親になるでしょう:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class ORMClass(object):
    @classmethod
    def query(class_):
        return DBSession.query(class_)

    @classmethod
    def get(class_, id):
        return Session.query(class_).get(id)

Base = declarative.declarative_base(cls=ORMClass)

class User(Base):
    __tablename__ = "users"

    # Column definitions omitted

その後、ビューの中でこのようなことができます:

user_1 = models.User.get(1)
q = models.User.query()

これが良いかどうかは、あなたの考え方次第です。

複数データベース

main 関数の中のデフォルト設定は1つのデータベースを設定します。複数の データベースに接続するためには、それらをすべて別々のプリフィックスで development.ini にリスト化してください。同じプリフィックスの下に 追加のエンジン引数を置くことができます。例えば:

次に、各エンジンを追加するように main 関数を修正してください。さらに、 INI ファイル中の同名の設定を上書きする追加のエンジン引数を渡すことが できます。

engine = sa.engine_from_config(settings, prefix="sqlalchemy.",
    pool_recycle=3600, convert_unicode=True)
stats = sa.engine_from_config(settings, prefix="stats.")

この時点で選択肢があります。同じ DBSession の内で異なるテーブルを異なる データベースに結び付けたいですか? それは容易です:

DBSession.configure(bind={models.Person: engine, models.Score: stats})

binds 辞書中のキーは SQLAlchemy ORM クラス、テーブルオブジェクト あるいはマッパーオブジェクトになります。

しかし、アプリケーションによっては各々が異なるデータベースに接続された 複数の DBSession を好みます。また、アプリケーションによっては複数の declarative base を好みます。その結果、異なるグループの ORM クラスは異 なる declarative base を持つことになります。あるいは、低レベルの SQL クエリのためにエンジンを Base のメタデータに直接 bind したい場合もある でしょう。あるいは、それ自身の DBSession あるいは Base を定義するサード パーティのパッケージを使用しているかもしれません。これらの場合では、 例えば DBSession2 や Base2 を加えるためにモデル自体を修正しなければ ならないでしょう。構成が複雑な場合、 Pylons が行うようにモデル初期化関数を 定義すると良いかもしれません。その結果、トップレベルのルーチン (main 関数あるいはスタンドアロンのユーティリティ) は、単純な呼び出しを一回だけ 行えば済むことになります。これは、複雑なアプリケーション用のかなり精巧な init ルーチンです:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
DBSession1 = orm.scoped_session(orm.sessionmaker(extension=ZTE())
DBSession2 = orm.scoped_session(orm.sessionmaker(extension=ZTE())
Base1 = declarative.declarative_base()
Base2 = declarative.declarative_base()
engine1 = None
engine2 = None

def init_model(e1, e2):
    # e1 and e2 are SQLAlchemy engines. (We can't call them engine1 and
    # engine2 because we want to access globals with the same name.)
    global engine1, engine2
    engine1 = e1
    engine2 = e2
    DBSession1.configure(bind=e1)
    DBSession2.configure(bind=e2)
    Base1.metadata.bind = e1
    Base2.metadata.bind = e2

リフレクションテーブル

リフレクションテーブルは初期化のために実際のデータベース接続に依存する ので、ジレンマが生じます。しかし、モデルがインポートされる時点でエンジンは 未知です。この状況はほぼ確実に初期化関数を必要とします; あるいは、 少なくともそれを回避する方法は見つかっていません。 ORM クラスは今まで 通りモジュールのグローバル変数として (宣言的な構文を使用せずに) 定義することができます。しかし、テーブル定義およびマッパー呼び出しは、 エンジンが既知になった時点で関数の内部で行われなければならないでしょう。 これは、それをどのように非宣言的に行うかを示したものです:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
DBSession = orm.scoped_session(orm.sessionmaker(extension=ZTE())
# Not using Base; not using declarative syntax
md = sa.MetaData()
persons = None   # Table, set in init_model().

class Person(object):
    pass

def init_model(engine):
    global persons
    DBSession.configure(bind=engine)
    md.bind = engine
    persons = sa.Table("persons", md, autoload=True, autoload_with=engine)
    orm.mapper(Person, persons)

宣言的な構文に関して、Michael Bayer はこのためのレシピをどこかに投稿し ていたと 思います が、それらを見つけるためには SQLAlchmey 界隈を あちこち覗き回る必要があるでしょう。最悪の場合、declarative クラス 全体を init_model 関数の内部に置き、グローバル変数にそれを代入することが できます。