ドメインモデルの定義(Defining the Domain Model)

cookiecutterで生成されたアプリケーションの最初の変更は、wikiページ domain model を定義することです。

注釈

「user.py」ファイルや「page.py」ファイルは、Pythonモジュールであることを除いて特別なことは何もありません。プロジェクトはコードベース全体に任意の名前のモジュールで多くのモデルを持てます。モデルを実装するモジュールは、しばしば名前に「model」を持つか、 「models」という名前のアプリケーションパッケージのPythonサブパッケージに入っています(このチュートリアルで行ったように)。しかしながらこれは寒冷であり要件です。

`` setup.py``ファイルの依存関係を宣言します(Declaring dependencies in our setup.py file)

アプリケーションのモデルのコードは、オリジナルの 「チュートリアル」アプリケーションの依存関係ではないパッケージに依存します。オリジナルの 「チュートリアル」アプリケーションは、cookiecutterによって生成されました。しかしカスタムアプリケーションの要件についてはわかりません。

依存関係を追加する必要があります。 bcrypt パッケージを「tutorial」パッケージの「setup.py」ファイルに、「setup()」関数の「requires」パラメータにこの依存関係を割り当てます。

「tutorial/setup.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
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
import os

from setuptools import setup, find_packages

here = os.path.abspath(os.path.dirname(__file__))
with open(os.path.join(here, 'README.txt')) as f:
    README = f.read()
with open(os.path.join(here, 'CHANGES.txt')) as f:
    CHANGES = f.read()

requires = [
    'bcrypt',
    'plaster_pastedeploy',
    'pyramid >= 1.9a',
    'pyramid_debugtoolbar',
    'pyramid_jinja2',
    'pyramid_retry',
    'pyramid_tm',
    'SQLAlchemy',
    'transaction',
    'zope.sqlalchemy',
    'waitress',
]

tests_require = [
    'WebTest >= 1.3.1',  # py3 compat
    'pytest',
    'pytest-cov',
]

setup(
    name='tutorial',
    version='0.0',
    description='myproj',
    long_description=README + '\n\n' + CHANGES,
    classifiers=[
        'Programming Language :: Python',
        'Framework :: Pyramid',
        'Topic :: Internet :: WWW/HTTP',
        'Topic :: Internet :: WWW/HTTP :: WSGI :: Application',
    ],
    author='',
    author_email='',
    url='',
    keywords='web pyramid pylons',
    packages=find_packages(),
    include_package_data=True,
    zip_safe=False,
    extras_require={
        'testing': tests_require,
    },
    install_requires=requires,
    entry_points={
        'paste.app_factory': [
            'main = tutorial:main',
        ],
        'console_scripts': [
            'initialize_tutorial_db = tutorial.scripts.initializedb:main',
        ],
    },
)

強調表示された行だけを追加する必要があります。

注釈

PyPIの「bcrypt」パッケージを使ってパスワードを安全にハッシュしています。「bcrypt」がシステム上で問題である場合はパスワード用の他の一方向ハッシュアルゴリズムがあります。これはパスワードの保存に対して一般的な一方向ハッシュに対して承認されたアルゴリズムであることを確認します。

「pip install -e .」を実行しいます。(Running pip install -e . )

新しいソフトウェアの依存関係が追加されたので、 「tutorial」パッケージのルートの中で 「pip install -e .」を再度実行して、新たに追加された依存関係を取得して登録する必要があります。

現在の作業ディレクトリがプロジェクトのルート( 「setup.py」が存在するディレクトリ)であることを確認し、次のコマンドを実行してください。

UNIXの場合:

$ $VENV/bin/pip install -e .

Windowsの場合:

c:\tutorial> %VENV%\Scripts\pip install -e .

このコマンドを実行すると、次のような行がコンソールに表示されます。

Successfully installed bcrypt-3.1.2 cffi-1.9.1 pycparser-2.17 tutorial

`` mymodel.py`を削除する( Remove mymodel.py )

「tutorial/models/mymodel.py」ファイルを削除しましょう。 「MyModel」クラスは単なるサンプルで使用しません。

「user.py」を追加してください( Add user.py )

「tutorial/models/user.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
import bcrypt
from sqlalchemy import (
    Column,
    Integer,
    Text,
)

from .meta import Base


class User(Base):
    """ The SQLAlchemy declarative model class for a User object. """
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(Text, nullable=False, unique=True)
    role = Column(Text, nullable=False)

    password_hash = Column(Text)

    def set_password(self, pw):
        pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt())
        self.password_hash = pwhash.decode('utf8')

    def check_password(self, pw):
        if self.password_hash is not None:
            expected_hash = self.password_hash.encode('utf8')
            return bcrypt.checkpw(pw.encode('utf8'), expected_hash)
        return False

これはwikiで認証ユーザーのための非常に基本的なモデルです。

前の章で、私たちのモデルがSQLAlchemy sqlalchemy.ext.declarative.declarative_base() から継承することを簡単に説明しました。モデルをスキーマに添付します。

ご覧のように「User」クラスにはクラスレベルの「__tablename__」アトリビュートがあります。このアトリビュートは文字列 「users」に相当します。「User」クラスには 「id」、 「name」、 「password_hash」、 「role」というクラスレベルのアトリビュートもあります(すべてのインスタンスの sqlalchemy.schema.Column )。これらは「users」テーブルの列にマップされます。 「id」アトリビュートがテーブルの主キーになります。「name」アトリビュートはテキスト列になり、各値は列内で一意である必要があります。 「password_hash」は、安全にハッシュされたパスワードを含むnullを含むテキストアトリビュートです。最後に「role」のテキストアトリビュートがユーザの役割を担います。

後でユーザーオブジェクトを使用するときに役立つ2つのヘルパーメソッドがあります。最初は「set_password」です。これは未処理のパスワードをとり、「bcrypt」を使って非可逆的な表現に変換します。このプロセスは「hashing」と呼ばれます。 2番目のメソッド「check_password」では、送信されたパスワードのハッシュ値と、データベースのユーザーレコードに保存されているパスワードのハッシュ値を比較できます。 2つのハッシュ値が一致すると送信されたパスワードが有効なのでユーザーを認証できます。

ハッシュされたパスワードを解読してアプリケーションで認証することは不可能です。パスワードを愚かにも平文で保存した場合は、データベースにアクセスできる人は誰でもパスワードを取得して認証できます。

「page.py」を追加してください

「tutorial/models/page.py」ファイルを次の内容で作成します:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from sqlalchemy import (
    Column,
    ForeignKey,
    Integer,
    Text,
)
from sqlalchemy.orm import relationship

from .meta import Base


class Page(Base):
    """ The SQLAlchemy declarative model class for a Page object. """
    __tablename__ = 'pages'
    id = Column(Integer, primary_key=True)
    name = Column(Text, nullable=False, unique=True)
    data = Column(Text, nullable=False)

    creator_id = Column(ForeignKey('users.id'), nullable=False)
    creator = relationship('User', backref='created_pages')

見て分かるように、「Page」クラスは、 「id」、 「name」、「data」とwikiページに関する情報を格納することに焦点を当てた属性を除けば、上で定義した「User」とよく似ています。 ここで紹介した唯一の新しいものは「creator_id」カラムで、これは「users」テーブルを参照する外部キーです。外部キーはスキーマレベルで非常に便利ですが、 「User」オブジェクトと「Page」オブジェクトを関連付けるため、 「creator」属性を2つのORMレベルのマッピングとして定義します。SQLAlchemyはUserを参照する外部キーを使用してこの値を自動的に設定します。外部キーには 「nullable = False」があるので、 「page」のインスタンスに対応する「page.creator」があることが保証されます。これは「User」インスタンスになります。

「models/__init__.py」を編集する「Edit models/__init__.py

モデルにパッケージを使用しているので、 「__init __.py」ファイルを更新してモデルがメタデータに添付されていることを確認する必要もあります。

「tutorial/models/__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
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
from sqlalchemy import engine_from_config
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import configure_mappers
import zope.sqlalchemy

# import or define all models here to ensure they are attached to the
# Base.metadata prior to any initialization routines
from .page import Page  # noqa
from .user import User  # noqa

# run configure_mappers after defining all of the models to ensure
# all relationships can be setup
configure_mappers()


def get_engine(settings, prefix='sqlalchemy.'):
    return engine_from_config(settings, prefix)


def get_session_factory(engine):
    factory = sessionmaker()
    factory.configure(bind=engine)
    return factory


def get_tm_session(session_factory, transaction_manager):
    """
    Get a ``sqlalchemy.orm.Session`` instance backed by a transaction.

    This function will hook the session to the transaction manager which
    will take care of committing any changes.

    - When using pyramid_tm it will automatically be committed or aborted
      depending on whether an exception is raised.

    - When using scripts you should wrap the session in a manager yourself.
      For example::

          import transaction

          engine = get_engine(settings)
          session_factory = get_session_factory(engine)
          with transaction.manager:
              dbsession = get_tm_session(session_factory, transaction.manager)

    """
    dbsession = session_factory()
    zope.sqlalchemy.register(
        dbsession, transaction_manager=transaction_manager)
    return dbsession


def includeme(config):
    """
    Initialize the model for a Pyramid app.

    Activate this setup using ``config.include('tutorial.models')``.

    """
    settings = config.get_settings()
    settings['tm.manager_hook'] = 'pyramid_tm.explicit_manager'

    # use pyramid_tm to hook the transaction lifecycle to the request
    config.include('pyramid_tm')

    # use pyramid_retry to retry a request when transient exceptions occur
    config.include('pyramid_retry')

    session_factory = get_session_factory(get_engine(settings))
    config.registry['dbsession_factory'] = session_factory

    # make request.dbsession available for use in Pyramid
    config.add_request_method(
        # r.tm is the transaction manager used by pyramid_tm
        lambda r: get_tm_session(session_factory, r.tm),
        'dbsession',
        reify=True
    )

ここでは、インポートとモデルの名前、 「Page」と「User」を合わせています。

「scripts/initializedb.py」を編集する( Edit scripts/initializedb.py

ファイルの詳細はまだ見ていませんが、チュートリアルパッケージの「scripts」ディレクトリに「initializedb.py」という名前のファイルがあります。このファイルのコードはチュートリアルのインストール手順で行ったように、「initialize_tutorial_db」コマンドを実行するたびに実行されます。

注釈

このコマンドは、プロジェクトの 「setup.py」ファイルの [console_scripts] エントリポイントで定義されたマッピングのため、「initialize_tutorial_db」という名前です。

モデルを変更したので、「initializedb.py」スクリプトを変更する必要があります。具体的には、MyModelのインポートを「User」と「Page」のものに置き換えます。また、スクリプトの最後を「MyMode」ではなく「User」オブジェクト(「basic」と「editor」)と「Page」を作成するように変更します。「dbsession」に追加するようにスクリプトを変更します。

「tutorial/scripts/initializedb.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
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
import os
import sys
import transaction

from pyramid.paster import (
    get_appsettings,
    setup_logging,
    )

from pyramid.scripts.common import parse_vars

from ..models.meta import Base
from ..models import (
    get_engine,
    get_session_factory,
    get_tm_session,
    )
from ..models import Page, User


def usage(argv):
    cmd = os.path.basename(argv[0])
    print('usage: %s <config_uri> [var=value]\n'
          '(example: "%s development.ini")' % (cmd, cmd))
    sys.exit(1)


def main(argv=sys.argv):
    if len(argv) < 2:
        usage(argv)
    config_uri = argv[1]
    options = parse_vars(argv[2:])
    setup_logging(config_uri)
    settings = get_appsettings(config_uri, options=options)

    engine = get_engine(settings)
    Base.metadata.create_all(engine)

    session_factory = get_session_factory(engine)

    with transaction.manager:
        dbsession = get_tm_session(session_factory, transaction.manager)

        editor = User(name='editor', role='editor')
        editor.set_password('editor')
        dbsession.add(editor)

        basic = User(name='basic', role='basic')
        basic.set_password('basic')
        dbsession.add(basic)

        page = Page(
            name='FrontPage',
            creator=editor,
            data='This is the front page',
        )
        dbsession.add(page)

ハイライト表示されている行だけを変更する必要があります。※翻訳の注意。最新版ではこの次にAlembicを使用したDBのマイグレーションの項目がある

プロジェクトのインストールとデータベースの再初期化(Installing the project and re-initializing the database)

私たちのモデルは変更されており、データベースを再初期化するためには、「initialize_tutorial_db」コマンドを再実行して、models.pyファイルとinitializedb.pyファイルの両方の変更を取り上げる必要があります。内容については データベースの初期化(Initializing the database) を参照してください。

成功すると次のようになります。

2016-12-20 02:51:11,195 INFO  [sqlalchemy.engine.base.Engine:1235][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
2016-12-20 02:51:11,195 INFO  [sqlalchemy.engine.base.Engine:1236][MainThread] ()
2016-12-20 02:51:11,195 INFO  [sqlalchemy.engine.base.Engine:1235][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
2016-12-20 02:51:11,195 INFO  [sqlalchemy.engine.base.Engine:1236][MainThread] ()
2016-12-20 02:51:11,196 INFO  [sqlalchemy.engine.base.Engine:1140][MainThread] PRAGMA table_info("pages")
2016-12-20 02:51:11,196 INFO  [sqlalchemy.engine.base.Engine:1143][MainThread] ()
2016-12-20 02:51:11,196 INFO  [sqlalchemy.engine.base.Engine:1140][MainThread] PRAGMA table_info("users")
2016-12-20 02:51:11,197 INFO  [sqlalchemy.engine.base.Engine:1143][MainThread] ()
2016-12-20 02:51:11,197 INFO  [sqlalchemy.engine.base.Engine:1140][MainThread]
CREATE TABLE users (
        id INTEGER NOT NULL,
        name TEXT NOT NULL,
        role TEXT NOT NULL,
        password_hash TEXT,
        CONSTRAINT pk_users PRIMARY KEY (id),
        CONSTRAINT uq_users_name UNIQUE (name)
)


2016-12-20 02:51:11,197 INFO  [sqlalchemy.engine.base.Engine:1143][MainThread] ()
2016-12-20 02:51:11,198 INFO  [sqlalchemy.engine.base.Engine:719][MainThread] COMMIT
2016-12-20 02:51:11,199 INFO  [sqlalchemy.engine.base.Engine:1140][MainThread]
CREATE TABLE pages (
        id INTEGER NOT NULL,
        name TEXT NOT NULL,
        data TEXT NOT NULL,
        creator_id INTEGER NOT NULL,
        CONSTRAINT pk_pages PRIMARY KEY (id),
        CONSTRAINT uq_pages_name UNIQUE (name),
        CONSTRAINT fk_pages_creator_id_users FOREIGN KEY(creator_id) REFERENCES users (id)
)


2016-12-20 02:51:11,199 INFO  [sqlalchemy.engine.base.Engine:1143][MainThread] ()
2016-12-20 02:51:11,200 INFO  [sqlalchemy.engine.base.Engine:719][MainThread] COMMIT
2016-12-20 02:51:11,755 INFO  [sqlalchemy.engine.base.Engine:679][MainThread] BEGIN (implicit)
2016-12-20 02:51:11,755 INFO  [sqlalchemy.engine.base.Engine:1140][MainThread] INSERT INTO users (name, role, password_hash) VALUES (?, ?, ?)
2016-12-20 02:51:11,755 INFO  [sqlalchemy.engine.base.Engine:1143][MainThread] ('editor', 'editor', '$2b$12$ds7h2Zb7.l6TEFup5h8f4ekA9GRfEpE1yQGDRvT9PConw73kKuupG')
2016-12-20 02:51:11,756 INFO  [sqlalchemy.engine.base.Engine:1140][MainThread] INSERT INTO users (name, role, password_hash) VALUES (?, ?, ?)
2016-12-20 02:51:11,756 INFO  [sqlalchemy.engine.base.Engine:1143][MainThread] ('basic', 'basic', '$2b$12$KgruXP5Vv7rikr6dGB3TF.flGXYpiE0Li9K583EVomjY.SYmQOsyi')
2016-12-20 02:51:11,757 INFO  [sqlalchemy.engine.base.Engine:1140][MainThread] INSERT INTO pages (name, data, creator_id) VALUES (?, ?, ?)
2016-12-20 02:51:11,757 INFO  [sqlalchemy.engine.base.Engine:1143][MainThread] ('FrontPage', 'This is the front page', 1)
2016-12-20 02:51:11,757 INFO  [sqlalchemy.engine.base.Engine:719][MainThread] COMMIT

ブラウザでアプリケーションを表示する(View the application in a browser)

できません。この時点で、私たちのシステムは「実行不可能」な状態にあります。次の章のビュー関連のファイルを変更して、アプリケーションを正常に起動できるようにします。アプリケーションを起動しようとすると( アプリケーションを起動する(Start the application) 参照)、コンソールに例外で終わる Pythonのトレースバックが表示されます:

ImportError: cannot import name MyModel

実行不能な現象はテストを実行しようとした場合にも発生します。