定义域模型

我们将对stock-cookiecutter生成的应用程序进行的第一个更改是定义一个wiki页面。 domain model .

备注

文件名没有什么特别之处 user.pypage.py 但它们是Python模块。一个项目可能在其代码库中的任意命名模块中有许多模型。实现模型的模块通常 model 或者它们可能存在于名为的应用程序包的python子包中。 models (正如我们在本教程中所做的那样),但这只是一个约定,而不是一个要求。

在我们的 setup.py 文件

我们应用程序中的模型代码将依赖于一个不依赖于原始“教程”应用程序的包。最初的“教程”应用程序是由CookiCutter生成的;它不知道我们的自定义应用程序需求。

我们需要添加一个依赖项, bcrypt 包装,给我们 tutorial 包装的 setup.py 通过将此依赖项分配给 requires 中的参数 setup() 功能。

正常开放 tutorial/setup.py 通过添加 bcrypt 把包裹分类:

11requires = [
12    'alembic',
13    'bcrypt',
14    'plaster_pastedeploy',
15    'pyramid',
16    'pyramid_debugtoolbar',
17    'pyramid_jinja2',
18    'pyramid_retry',
19    'pyramid_tm',
20    'SQLAlchemy',
21    'transaction',
22    'waitress',
23    'zope.sqlalchemy',
24]

按字母顺序对包裹进行分类是一个很好的做法,这样更容易找到。我们的cookiecutter没有对其包装进行分类,因为它只是根据我们的选择附加包装。

备注

我们正在使用 bcrypt 从pypi打包以安全地散列密码。如果 bcrypt 是系统中的问题。只需确保它是一个被批准用于存储密码的算法,而不是一个通用的单向散列。

运行 pip install -e .

由于添加了新的软件依赖项,因此需要运行 pip install -e . 再次在根的内部 tutorial 打包以获取并注册新添加的依赖关系分发。

确保当前工作目录是项目的根目录(其中 setup.py 执行以下命令。

在UNIX上:

$VENV/bin/pip install -e .

在Windows上:

%VENV%\Scripts\pip install -e .

成功执行此命令将以一行到控制台结束,如下所示。

Successfully installed bcrypt-3.2.0 cffi-1.14.4 pycparser-2.20 tutorial

去除 mymodel.py

让我们删除文件 tutorial/models/mymodel.py . 这个 MyModel 类只是一个示例,我们不会使用它。

添加 user.py

创建新文件 tutorial/models/user.py 包括以下内容:

 1import bcrypt
 2from sqlalchemy import (
 3    Column,
 4    Integer,
 5    Text,
 6)
 7
 8from .meta import Base
 9
10
11class User(Base):
12    """ The SQLAlchemy declarative model class for a User object. """
13    __tablename__ = 'users'
14    id = Column(Integer, primary_key=True)
15    name = Column(Text, nullable=False, unique=True)
16    role = Column(Text, nullable=False)
17
18    password_hash = Column(Text)
19
20    def set_password(self, pw):
21        pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt())
22        self.password_hash = pwhash.decode('utf8')
23
24    def check_password(self, pw):
25        if self.password_hash is not None:
26            expected_hash = self.password_hash.encode('utf8')
27            return bcrypt.checkpw(pw.encode('utf8'), expected_hash)
28        return False

这是一个非常基本的用户模型,用户可以通过我们的wiki进行身份验证。

我们在上一章中简要讨论了我们的模型将从一个sqlacalchemy继承 sqlalchemy.ext.declarative.declarative_base() . 这将把模型附加到我们的模式中。

如你所见,我们的 User 类具有类级属性 __tablename__ 等于字符串 users . 我们的 User 类还将具有名为 idnamepassword_hashrole (的所有实例 sqlalchemy.schema.Column )这些将映射到 users 表。这个 id 属性将是表中的主键。这个 name 属性将是一个文本列,其中的每个值在该列中都必须是唯一的。这个 password_hash 是一个可以为空的文本属性,它将包含一个安全散列的密码。最后, role 文本属性将保留用户的角色。

在以后使用用户对象时,有两个助手方法可以帮助我们。第一个是 set_password 它将获取原始密码并使用 bcrypt 变成一种不可逆的表示,称为“散列”的过程。第二种方法, check_password ,将允许我们将提交的密码的哈希值与数据库中用户记录中存储的密码的哈希值进行比较。如果两个哈希值匹配,则提交的密码有效,我们可以对用户进行身份验证。

我们散列密码,这样就不可能解密它们并使用它们在应用程序中进行身份验证。如果我们愚蠢地将密码以明文存储,那么任何有权访问数据库的人都可以检索任何密码以作为任何用户进行身份验证。

添加 page.py

创建新文件 tutorial/models/page.py 包括以下内容:

 1from sqlalchemy import (
 2    Column,
 3    ForeignKey,
 4    Integer,
 5    Text,
 6)
 7from sqlalchemy.orm import relationship
 8
 9from .meta import Base
10
11
12class Page(Base):
13    """ The SQLAlchemy declarative model class for a Page object. """
14    __tablename__ = 'pages'
15    id = Column(Integer, primary_key=True)
16    name = Column(Text, nullable=False, unique=True)
17    data = Column(Text, nullable=False)
18
19    creator_id = Column(ForeignKey('users.id'), nullable=False)
20    creator = relationship('User', backref='created_pages')

如你所见,我们的 Page 类与 User 上面定义的,除了属性集中存储关于wiki页面的信息,包括 idnamedata . 这里介绍的唯一新构造是 creator_id 列,它是引用 users 表。外键在模式级别非常有用,但是因为我们希望 User 对象与 Page 对象,我们还定义了 creator 属性作为两个表之间的ORM级别映射。SQLAlchemy将使用引用用户的外键自动填充此值。因为外键 nullable=False ,我们保证 page 会有一个对应的 page.creator ,这将是一个 User 实例。

编辑 models/__init__.py

由于我们使用的是模型包,因此我们还需要更新 __init__.py 文件以确保模型附加到元数据。

打开 tutorial/models/__init__.py 文件并将其编辑为如下所示:

  1from sqlalchemy import engine_from_config
  2from sqlalchemy.orm import sessionmaker
  3from sqlalchemy.orm import configure_mappers
  4import zope.sqlalchemy
  5
  6# Import or define all models here to ensure they are attached to the
  7# ``Base.metadata`` prior to any initialization routines.
  8from .page import Page  # flake8: noqa
  9from .user import User  # flake8: noqa
 10
 11# Run ``configure_mappers`` after defining all of the models to ensure
 12# all relationships can be setup.
 13configure_mappers()
 14
 15
 16def get_engine(settings, prefix='sqlalchemy.'):
 17    return engine_from_config(settings, prefix)
 18
 19
 20def get_session_factory(engine):
 21    factory = sessionmaker()
 22    factory.configure(bind=engine)
 23    return factory
 24
 25
 26def get_tm_session(session_factory, transaction_manager, request=None):
 27    """
 28    Get a ``sqlalchemy.orm.Session`` instance backed by a transaction.
 29
 30    This function will hook the session to the transaction manager which
 31    will take care of committing any changes.
 32
 33    - When using pyramid_tm it will automatically be committed or aborted
 34      depending on whether an exception is raised.
 35
 36    - When using scripts you should wrap the session in a manager yourself.
 37      For example:
 38
 39      .. code-block:: python
 40
 41          import transaction
 42
 43          engine = get_engine(settings)
 44          session_factory = get_session_factory(engine)
 45          with transaction.manager:
 46              dbsession = get_tm_session(session_factory, transaction.manager)
 47
 48    This function may be invoked with a ``request`` kwarg, such as when invoked
 49    by the reified ``.dbsession`` Pyramid request attribute which is configured
 50    via the ``includeme`` function below. The default value, for backwards
 51    compatibility, is ``None``.
 52
 53    The ``request`` kwarg is used to populate the ``sqlalchemy.orm.Session``'s
 54    "info" dict.  The "info" dict is the official namespace for developers to
 55    stash session-specific information.  For more information, please see the
 56    SQLAlchemy docs:
 57    https://docs.sqlalchemy.org/en/stable/orm/session_api.html#sqlalchemy.orm.session.Session.params.info
 58
 59    By placing the active ``request`` in the "info" dict, developers will be
 60    able to access the active Pyramid request from an instance of an SQLAlchemy
 61    object in one of two ways:
 62
 63    - Classic SQLAlchemy. This uses the ``Session``'s utility class method:
 64
 65      .. code-block:: python
 66
 67          from sqlalchemy.orm.session import Session as sa_Session
 68
 69          dbsession = sa_Session.object_session(dbObject)
 70          request = dbsession.info["request"]
 71
 72    - Modern SQLAlchemy. This uses the "Runtime Inspection API":
 73
 74      .. code-block:: python
 75
 76          from sqlalchemy import inspect as sa_inspect
 77
 78          dbsession = sa_inspect(dbObject).session
 79          request = dbsession.info["request"]
 80    """
 81    dbsession = session_factory(info={"request": request})
 82    zope.sqlalchemy.register(
 83        dbsession, transaction_manager=transaction_manager
 84    )
 85    return dbsession
 86
 87
 88def includeme(config):
 89    """
 90    Initialize the model for a Pyramid app.
 91
 92    Activate this setup using ``config.include('tutorial.models')``.
 93
 94    """
 95    settings = config.get_settings()
 96    settings['tm.manager_hook'] = 'pyramid_tm.explicit_manager'
 97
 98    # Use ``pyramid_tm`` to hook the transaction lifecycle to the request.
 99    # Note: the packages ``pyramid_tm`` and ``transaction`` work together to
100    # automatically close the active database session after every request.
101    # If your project migrates away from ``pyramid_tm``, you may need to use a
102    # Pyramid callback function to close the database session after each
103    # request.
104    config.include('pyramid_tm')
105
106    # use pyramid_retry to retry a request when transient exceptions occur
107    config.include('pyramid_retry')
108
109    # hook to share the dbengine fixture in testing
110    dbengine = settings.get('dbengine')
111    if not dbengine:
112        dbengine = get_engine(settings)
113
114    session_factory = get_session_factory(dbengine)
115    config.registry['dbsession_factory'] = session_factory
116
117    # make request.dbsession available for use in Pyramid
118    def dbsession(request):
119        # hook to share the dbsession fixture in testing
120        dbsession = request.environ.get('app.dbsession')
121        if dbsession is None:
122            # request.tm is the transaction manager used by pyramid_tm
123            dbsession = get_tm_session(
124                session_factory, request.tm, request=request
125            )
126        return dbsession
127
128    config.add_request_method(dbsession, reify=True)

在这里,我们将我们的进口商品与模型的名称对齐, PageUser .

使用Alembic迁移数据库

现在我们已经编写了模型,我们需要修改数据库模式以反映对代码的更改。让我们生成一个新版本,然后将数据库升级到最新版本(head)。

在UNIX上:

$VENV/bin/alembic -c development.ini revision --autogenerate \
    -m "use new models Page and User"
$VENV/bin/alembic -c development.ini upgrade head

在Windows上:

%VENV%\Scripts\alembic -c development.ini revision \
    --autogenerate -m "use new models Page and User"
%VENV%\Scripts\alembic -c development.ini upgrade head

成功执行这些命令将生成类似以下内容的输出。

2021-01-07 08:00:14,550 INFO  [alembic.runtime.migration:155][MainThread] Context impl SQLiteImpl.
2021-01-07 08:00:14,551 INFO  [alembic.runtime.migration:158][MainThread] Will assume non-transactional DDL.
2021-01-07 08:00:14,553 INFO  [alembic.autogenerate.compare:134][MainThread] Detected added table 'users'
2021-01-07 08:00:14,553 INFO  [alembic.autogenerate.compare:134][MainThread] Detected added table 'pages'
2021-01-07 08:00:14,558 INFO  [alembic.autogenerate.compare:622][MainThread] Detected removed index 'my_index' on 'models'
2021-01-07 08:00:14,558 INFO  [alembic.autogenerate.compare:176][MainThread] Detected removed table 'models'
  Generating <somepath>/tutorial/tutorial/alembic/versions/20210107_bc9a3dead43a.py ...  done
2021-01-07 08:00:21,318 INFO  [alembic.runtime.migration:155][MainThread] Context impl SQLiteImpl.
2021-01-07 08:00:21,318 INFO  [alembic.runtime.migration:158][MainThread] Will assume non-transactional DDL.
2021-01-07 08:00:21,320 INFO  [alembic.runtime.migration:517][MainThread] Running upgrade 90658c4a9673 -> bc9a3dead43a, use new models Page and User

Alembic概述

让我们简单地讨论一下Alembic的配置。

在炼金术中 development.ini 文件,的设置 script_location 将alembic配置为在目录中查找迁移脚本 tutorial/alembic . 默认情况下,alembic将迁移文件更深地存储在 tutorial/alembic/versions . 这些文件由alembic生成,然后在运行升级或降级迁移时执行。设置 file_template 为每个迁移的文件名提供格式。我们已经配置了 file_template 设置为便于按文件名查找迁移。

在本教程的这一点上,我们有两个迁移文件。检查它们,看看当您将数据库升级或降级到特定版本时,Alembic将做什么。请注意修订标识符以及它们如何以链接顺序相互关联。

参见

有关更多信息,请参阅 Alembic documentation .

编辑 scripts/initialize_db.py

我们还没有查看此文件的详细信息,但在 scripts 您的目录 tutorial 包是名为 initialize_db.py . 每当我们运行 initialize_tutorial_db 命令,就像我们在本教程的安装步骤中所做的那样。

备注

命令名为 initialize_tutorial_db 因为在 [console_scripts] 我们项目的入口点 setup.py 文件。

既然我们已经改变了我们的模型,我们需要对我们的 initialize_db.py 剧本。特别是,我们将替换我们进口的 MyModel 与那些 UserPage 。我们还将更改脚本以创建两个 User 对象 (basiceditor )以及一个 Page ,而不是 MyModel ,并将它们添加到我们的 dbsession

正常开放 tutorial/scripts/initialize_db.py 并编辑如下:

 1import argparse
 2import sys
 3
 4from pyramid.paster import bootstrap, setup_logging
 5from sqlalchemy.exc import OperationalError
 6
 7from .. import models
 8
 9
10def setup_models(dbsession):
11    """
12    Add or update models / fixtures in the database.
13
14    """
15    editor = models.User(name='editor', role='editor')
16    editor.set_password('editor')
17    dbsession.add(editor)
18
19    basic = models.User(name='basic', role='basic')
20    basic.set_password('basic')
21    dbsession.add(basic)
22
23    page = models.Page(
24        name='FrontPage',
25        creator=editor,
26        data='This is the front page',
27    )
28    dbsession.add(page)
29
30
31def parse_args(argv):
32    parser = argparse.ArgumentParser()
33    parser.add_argument(
34        'config_uri',
35        help='Configuration file, e.g., development.ini',
36    )
37    return parser.parse_args(argv[1:])
38
39
40def main(argv=sys.argv):
41    args = parse_args(argv)
42    setup_logging(args.config_uri)
43    env = bootstrap(args.config_uri)
44
45    try:
46        with env['request'].tm:
47            dbsession = env['request'].dbsession
48            setup_models(dbsession)
49    except OperationalError:
50        print('''
51Pyramid is having a problem using your SQL database.  The problem
52might be caused by one of the following things:
53
541.  You may need to initialize your database tables with `alembic`.
55    Check your README.txt for description and try to run it.
56
572.  Your database server may not be running.  Check that the
58    database server referred to by the "sqlalchemy.url" setting in
59    your "development.ini" file is running.
60            ''')

只需更改突出显示的行。

填充数据库

因为我们的模型已经更改,为了重新填充数据库,我们需要重新运行 initialize_tutorial_db 命令获取我们对 initialize_db.py 文件。

在UNIX上

$VENV/bin/initialize_tutorial_db development.ini

在Windows上

%VENV%\Scripts\initialize_tutorial_db development.ini

您的控制台不应该有任何指示成功的输出。

在浏览器中查看应用程序

我们不能。此时,我们的系统处于“不可运行”状态;我们需要在下一章中更改与视图相关的文件,才能成功启动应用程序。如果您试图启动应用程序(请参见 启动应用程序 )并访问http://localhost:6543,您将在控制台上得到一个以以下异常结束的python跟踪:

AttributeError: module 'tutorial.models' has no attribute 'MyModel'

如果尝试运行测试,也会发生这种情况。