19: SQLAlchemyを使用したデータベース(19: Databases Using SQLAlchemy)

SQLAlchemy ORMを使用してSQLiteデータベース上のデータを格納および取得します。

背景(Background)

Pyramidベースのwikiアプリケーションでデータベースに基づいたページの格納が必要になりました。これはSQLデータベースのことを意味します。Pyramidコミュニティは、SQLAlchemy プロジェクトとその object-relational mapper (ORM) をパイソニックなデータベースへのインタフェースとして強くサポートしています。

今回のステップではSQLAlchemyをSQLiteデータベーステーブルに接続して、前回の手順でwikiページの格納と検索を行います。

注釈

The pyramid-cookiecutter-alchemy cookiecutter is really helpful for getting an SQLAlchemy project going, including generation of the console script. Since we want to see all the decisions, we will forgo convenience in this tutorial, and wire it up ourselves.

目的(Objectives)

  • SQLAlchemyのモデルを使用してSQLiteにページを格納します。
  • SQLAlchemyのクエリを使用してページの一覧・追加・表示・編集を行います。
  • コマンドラインから実行できるPyramidコンソールスクリプトを作成して、database-initializeコマンドを提供します

手順(Steps)

  1. フォ―ムを出発点にして使用します:

    $ cd ..; cp -r forms databases; cd databases
    
  2. 依存関係を追加する databases/setup.py とともに「エントリポイント」のコマンドラインスクリプトを追加する必要があります:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    from setuptools import setup
    
    requires = [
        'deform',
        'pyramid',
        'pyramid_chameleon',
        'pyramid_tm',
        'sqlalchemy',
        'waitress',
        'zope.sqlalchemy',
    ]
    
    setup(name='tutorial',
          install_requires=requires,
          entry_points="""\
          [paste.app_factory]
          main = tutorial:main
          [console_scripts]
          initialize_tutorial_db = tutorial.initialize_db:main
          """,
    )
    

    注釈

    後で変更するために、$VENV/bin/pip install -e . はここでは行いません。

  3. 設定ファイル( databases/development.ini )は一緒にいくつかの部品をつなぎます:

    [app:main]
    use = egg:tutorial
    pyramid.reload_templates = true
    pyramid.includes =
        pyramid_debugtoolbar
        pyramid_tm
    
    sqlalchemy.url = sqlite:///%(here)s/sqltutorial.sqlite
    
    [server:main]
    use = egg:waitress#main
    listen = localhost:6543
    
    # Begin logging configuration
    
    [loggers]
    keys = root, tutorial, sqlalchemy.engine.base.Engine
    
    [logger_tutorial]
    level = DEBUG
    handlers =
    qualname = tutorial
    
    [handlers]
    keys = console
    
    [formatters]
    keys = generic
    
    [logger_root]
    level = INFO
    handlers = console
    
    [logger_sqlalchemy.engine.base.Engine]
    level = INFO
    handlers =
    qualname = sqlalchemy.engine.base.Engine
    
    [handler_console]
    class = StreamHandler
    args = (sys.stderr,)
    level = NOTSET
    formatter = generic
    
    [formatter_generic]
    format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
    
    # End logging configuration
    
  4. エンジンの設定 databases/tutorial/__init__.py )は以下の変更をとおしてアプリケーションに読み込む必要があります:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    from pyramid.config import Configurator
    
    from sqlalchemy import engine_from_config
    
    from .models import DBSession, Base
    
    def main(global_config, **settings):
        engine = engine_from_config(settings, 'sqlalchemy.')
        DBSession.configure(bind=engine)
        Base.metadata.bind = engine
    
        config = Configurator(settings=settings,
                              root_factory='tutorial.models.Root')
        config.include('pyramid_chameleon')
        config.add_route('wiki_view', '/')
        config.add_route('wikipage_add', '/add')
        config.add_route('wikipage_view', '/{uid}')
        config.add_route('wikipage_edit', '/{uid}/edit')
        config.add_static_view('deform_static', 'deform:static/')
        config.scan('.views')
        return config.make_wsgi_app()
    
  5. 以下のコマンドラインスクリプト( databases/tutorial/initialize_db.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
    import os
    import sys
    import transaction
    
    from sqlalchemy import engine_from_config
    
    from pyramid.paster import (
        get_appsettings,
        setup_logging,
        )
    
    from .models import (
        DBSession,
        Page,
        Base,
        )
    
    
    def usage(argv):
        cmd = os.path.basename(argv[0])
        print('usage: %s <config_uri>\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]
        setup_logging(config_uri)
        settings = get_appsettings(config_uri)
        engine = engine_from_config(settings, 'sqlalchemy.')
        DBSession.configure(bind=engine)
        Base.metadata.create_all(engine)
        with transaction.manager:
            model = Page(title='Root', body='<p>Root</p>')
            DBSession.add(model)
    
  6. setup.py は変更されるので実行します:

    $ $VENV/bin/pip install -e .
    
  7. スクリプトは、以下のモデル( databases/tutorial/models.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
    from pyramid.security import Allow, Everyone
    
    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 Page(Base):
        __tablename__ = 'wikipages'
        uid = Column(Integer, primary_key=True)
        title = Column(Text, unique=True)
        body = Column(Text)
    
    
    class Root(object):
        __acl__ = [(Allow, Everyone, 'view'),
                   (Allow, 'group:editors', 'edit')]
    
        def __init__(self, request):
            pass
    
  8. コンソールスクリプトを実行してデータベースとテーブルを生成しましょう:

    $ $VENV/bin/initialize_tutorial_db development.ini
    
    2016-04-16 13:01:33,055 INFO  [sqlalchemy.engine.base.Engine][MainThread] SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
    2016-04-16 13:01:33,055 INFO  [sqlalchemy.engine.base.Engine][MainThread] ()
    2016-04-16 13:01:33,056 INFO  [sqlalchemy.engine.base.Engine][MainThread] SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
    2016-04-16 13:01:33,056 INFO  [sqlalchemy.engine.base.Engine][MainThread] ()
    2016-04-16 13:01:33,057 INFO  [sqlalchemy.engine.base.Engine][MainThread] PRAGMA table_info("wikipages")
    2016-04-16 13:01:33,057 INFO  [sqlalchemy.engine.base.Engine][MainThread] ()
    2016-04-16 13:01:33,058 INFO  [sqlalchemy.engine.base.Engine][MainThread]
    CREATE TABLE wikipages (
            uid INTEGER NOT NULL,
            title TEXT,
            body TEXT,
            PRIMARY KEY (uid),
            UNIQUE (title)
    )
    
    
    2016-04-16 13:01:33,058 INFO  [sqlalchemy.engine.base.Engine][MainThread] ()
    2016-04-16 13:01:33,059 INFO  [sqlalchemy.engine.base.Engine][MainThread] COMMIT
    2016-04-16 13:01:33,062 INFO  [sqlalchemy.engine.base.Engine][MainThread] BEGIN (implicit)
    2016-04-16 13:01:33,062 INFO  [sqlalchemy.engine.base.Engine][MainThread] INSERT INTO wikipages (title, body) VALUES (?, ?)
    2016-04-16 13:01:33,063 INFO  [sqlalchemy.engine.base.Engine][MainThread] ('Root', '<p>Root</p>')
    2016-04-16 13:01:33,063 INFO  [sqlalchemy.engine.base.Engine][MainThread] COMMIT
    
  9. データがSQLAlchemyクエリによって駆動されたので、以下のファイル( databases/tutorial/views.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
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    import colander
    import deform.widget
    
    from pyramid.httpexceptions import HTTPFound
    from pyramid.view import view_config
    
    from .models import DBSession, Page
    
    
    class WikiPage(colander.MappingSchema):
        title = colander.SchemaNode(colander.String())
        body = colander.SchemaNode(
            colander.String(),
            widget=deform.widget.RichTextWidget()
        )
    
    
    class WikiViews(object):
        def __init__(self, request):
            self.request = request
    
        @property
        def wiki_form(self):
            schema = WikiPage()
            return deform.Form(schema, buttons=('submit',))
    
        @property
        def reqts(self):
            return self.wiki_form.get_widget_resources()
    
        @view_config(route_name='wiki_view', renderer='wiki_view.pt')
        def wiki_view(self):
            pages = DBSession.query(Page).order_by(Page.title)
            return dict(title='Wiki View', pages=pages)
    
        @view_config(route_name='wikipage_add',
                     renderer='wikipage_addedit.pt')
        def wikipage_add(self):
            form = self.wiki_form.render()
    
            if 'submit' in self.request.params:
                controls = self.request.POST.items()
                try:
                    appstruct = self.wiki_form.validate(controls)
                except deform.ValidationFailure as e:
                    # Form is NOT valid
                    return dict(form=e.render())
    
                # Add a new page to the database
                new_title = appstruct['title']
                new_body = appstruct['body']
                DBSession.add(Page(title=new_title, body=new_body))
    
                # Get the new ID and redirect
                page = DBSession.query(Page).filter_by(title=new_title).one()
                new_uid = page.uid
    
                url = self.request.route_url('wikipage_view', uid=new_uid)
                return HTTPFound(url)
    
            return dict(form=form)
    
    
        @view_config(route_name='wikipage_view', renderer='wikipage_view.pt')
        def wikipage_view(self):
            uid = int(self.request.matchdict['uid'])
            page = DBSession.query(Page).filter_by(uid=uid).one()
            return dict(page=page)
    
    
        @view_config(route_name='wikipage_edit',
                     renderer='wikipage_addedit.pt')
        def wikipage_edit(self):
            uid = int(self.request.matchdict['uid'])
            page = DBSession.query(Page).filter_by(uid=uid).one()
    
            wiki_form = self.wiki_form
    
            if 'submit' in self.request.params:
                controls = self.request.POST.items()
                try:
                    appstruct = wiki_form.validate(controls)
                except deform.ValidationFailure as e:
                    return dict(page=page, form=e.render())
    
                # Change the content and redirect to the view
                page.title = appstruct['title']
                page.body = appstruct['body']
                url = self.request.route_url('wikipage_view', uid=uid)
                return HTTPFound(url)
    
            form = self.wiki_form.render(dict(
                uid=page.uid, title=page.title, body=page.body)
            )
    
            return dict(page=page, form=form)
    
  10. databases/tutorial/tests.py のテストにはSQLAlchemyのブートストラップが含まれています:

     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
    import unittest
    import transaction
    
    from pyramid import testing
    
    
    def _initTestingDB():
        from sqlalchemy import create_engine
        from .models import (
            DBSession,
            Page,
            Base
            )
        engine = create_engine('sqlite://')
        Base.metadata.create_all(engine)
        DBSession.configure(bind=engine)
        with transaction.manager:
            model = Page(title='FrontPage', body='This is the front page')
            DBSession.add(model)
        return DBSession
    
    
    class WikiViewTests(unittest.TestCase):
        def setUp(self):
            self.session = _initTestingDB()
            self.config = testing.setUp()
    
        def tearDown(self):
            self.session.remove()
            testing.tearDown()
    
        def test_wiki_view(self):
            from tutorial.views import WikiViews
    
            request = testing.DummyRequest()
            inst = WikiViews(request)
            response = inst.wiki_view()
            self.assertEqual(response['title'], 'Wiki View')
    
    
    class WikiFunctionalTests(unittest.TestCase):
        def setUp(self):
            from pyramid.paster import get_app
            app = get_app('development.ini')
            from webtest import TestApp
            self.testapp = TestApp(app)
    
        def tearDown(self):
            from .models import DBSession
            DBSession.remove()
    
        def test_it(self):
            res = self.testapp.get('/', status=200)
            self.assertIn(b'Wiki: View', res.body)
            res = self.testapp.get('/add', status=200)
            self.assertIn(b'Add/Edit', res.body)
    
  11. py.test パッケージを使用してテストを実行します:

    $ $VENV/bin/py.test tutorial/tests.py -q
    ..
    2 passed in 1.41 seconds
    
  12. Pyramidアプリケーションを実行します:

    $ $VENV/bin/pserve development.ini --reload
    
  13. ブラウザで http://localhost:6543/ を開きます。

分析(Analysis)

依存関係についてから始めましょう。 SQLAlchemy を使用してデータベースと会話することにしました。同時に pyramid_tmzope.sqlalchemy もインストールしました。どうして

Pyramid は、トランザクションのサポートに関して強い志向を持っています。具体的にはアプリケーションにミドルウェアまたはPyramid "tween" としてトランザクションマネージャをインストールができます。次にレスポンスを返す直前に、アプリケーションのすべてのトランザクションの対応する部分が実行されます。

これはPyramidのビューのコードは通常トランザクションを管理しないことを意味しています。ビューのコードまたはテンプレートでエラーが発生した場合はトランザクションマネージャはトランザクションを中止します。これはコードを書くための非常に解放的な方法です。

pyramid_tm パッケージは構成ファイル development.ini で構成された「 "tween" 」を提供します。"tween" をインストールします。次にSQLAlchemyつまりはRDBMSトランザクションマネージャをPyramidのトランザクションマネージャと統合するパッケージが必要です。これが zope.sqlalchemy の役割です。

SQLiteファイルはディスク内のどこにありますでしょうか?設定ファイル内にあります。これによりコンシューマーはパッケージを安全な(コードではない)方法で場所を変更できます。すなわ設定のことです。この構成指向のアプローチはPyramidでは必須ではありません。 __init__.py や他のコンパニオンモジュールでもこの​​ようなステートメントを作成できます。

initialize_tutorial_db はフレームワークをサポートする好例です。いくつかの [console_scripts] の場所の設定に関しては仮想環境の bin ディレクトリに生成されます。コンソールスクリプトはすべてのブートストラップを含む設定ファイルが入力されるパターンに従います。次にSQLAlchemyを開き、SQLiteファイルを作成するwikiのルートを作成します。トランザクションのスコープ内に作業を置く with transaction.manager のパーツに注意してください。これはトランザクションが自動的に行われるWebリクエスト内にないためです。

models.py はSQLAlchemyをPyramidのトランザクションマネージャーに接続するために少し追加作業を行います。その後、Page モデルを宣言します。

ビューではダミー辞書データを適切なデータベースサポート(行の一覧表示、行の追加、行の編集、および行の削除)に置き換えるところ主な変更です。

エクストラクレジット(Extra credit)

  1. なぜこのコードはすべて? なぜ2行を入力して魔法を続けられないのですか?
  2. Wikiページを作成するボタンの実装にチャレンジしてみてください。