定义域模型¶
我们将对stock-cookiecutter生成的应用程序进行的第一个更改是定义一个wiki页面。 domain model .
备注
文件名没有什么特别之处 user.py
或 page.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
类还将具有名为 id
, name
, password_hash
和 role
(的所有实例 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页面的信息,包括 id
, name
和 data
. 这里介绍的唯一新构造是 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)
在这里,我们将我们的进口商品与模型的名称对齐, Page
和 User
.
使用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
与那些 User
和 Page
。我们还将更改脚本以创建两个 User
对象 (basic
和 editor
)以及一个 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'
如果尝试运行测试,也会发生这种情况。