模型

模型在 Pyramid 和 Pylons 中基本上是相同的,因为框架与模型的关系最小,不像Django那样,ORM(对象关系映射器)是特定于框架的,而框架的其他部分假定它是特定类型的。在 Pyramid 和 Pylons 中,应用程序框架只建议将模型放在哪里,并为您初始化一个SQLAlchemy数据库连接。这里是默认的 Pyramid 配置(注释被删除,导入被压扁):

 1# pyramidapp/__init__.py
 2from sqlalchemy import engine_from_config
 3from .models import DBSession
 4
 5def main(global_config, **settings):
 6    engine = engine_from_config(settings, 'sqlalchemy.')
 7    DBSession.configure(bind=engine)
 8    ...
 9
10
11# pyramidapp/models.py
12from sqlalchemy import Column, Integer, Text
13from sqlalchemy.ext.declarative import declarative_base
14from sqlalchemy.orm import scoped_session, sessionmaker
15from zope.sqlalchemy import ZopeTransactionExtension
16
17DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension()))
18Base = declarative_base()
19
20class MyModel(Base):
21    __tablename__ = 'models'
22    id = Column(Integer, primary_key=True)
23    name = Column(Text, unique=True)
24    value = Column(Integer)
25
26    def __init__(self, name, value):
27        self.name = name
28        self.value = value
29

及其ini文件:

 1# development.ini
 2[app:main]
 3
 4# Pyramid only
 5pyramid.includes =
 6    pyramid_tm
 7
 8# Pyramid and Pylons
 9sqlalchemy.url = sqlite:///%(here)s/PyramidApp.db
10
11
12[logger_sqlalchemy]
13
14# Pyramid and Pylons
15level = INFO
16handlers =
17qualname = sqlalchemy.engine
18# "level = INFO" logs SQL queries.
19# "level = DEBUG" logs SQL queries and results.
20# "level = WARN" logs neither.  (Recommended for production systems.)

It has the following differences from Pylons:

  1. ZopeTransactionExtension和“Pyramid”Tween。

  2. “models”(复数)而不是“model”(单数)。

  3. A module rather than a subpackage.

  4. “dbsession”而不是“session”。

  5. 没有init_model()函数。

  6. 导入样式和变量命名略有不同。

只有第一种是本质上的区别,其余的只是美学编程风格。所以你可以在不伤害任何东西的情况下改变它们。

模型模型的差异是由于“模型”一词的使用方式不明确造成的。有些人说“模型”是指单个ORM类,而另一些人说“模型”是指应用程序中ORM类的整个集合。本指南使用“双向”一词。

模型中属于什么?

良好的编程实践建议将数据类与用户界面类分开。这样用户界面就可以在不影响数据的情况下进行更改,反之亦然。模型就是数据类的去处。例如,一个垄断游戏有玩家、一个棋盘、方块、头衔契约、卡片等,所以一个垄断程序可能会为每一个都设置类。如果应用程序需要在运行之间保存数据(持久性),则必须将数据存储在数据库或等效数据库中。 Pyramid 可以使用各种数据库类型:SQL数据库、对象数据库、键值数据库(“nosql”)、文档数据库(json或xml)、csv文件等。最常见的选择是sqlacalchemy,这是 Pyramid 和 Pylons 提供的第一个配置。

您至少应该在模型中定义ORM类。您还可以以函数、类方法或常规方法的形式添加任何业务逻辑。有时很难判断一段特定的代码是属于模型还是视图,但我们将由您来决定。

另一个原则是,模型不应依赖于应用程序的其余部分,这样它就可以单独使用;例如,在实用程序或其他应用程序中。这还允许您在框架或应用程序中断时提取数据。所以视图知道模型,但反之亦然。不是每个人都同意这一点,但这是一个很好的开始。

大型项目可能在多个Web应用程序和非Web程序之间共享一个通用模型。在这种情况下,将模型放在单独的顶级包中并导入 Pyramid 应用程序是有意义的。

Transaction manger

Pylons 从未使用过事务管理器,但在涡轮齿轮和Zope中很常见。事务管理器为您处理提交回滚周期。上面两个应用程序中的数据库会话都是 scoped 会话,这意味着它是线程本地全局的,必须在每个请求结束时清除。 Pylons 应用程序在基础控制器中有特殊的代码来清除会话。事务管理器通过提交在请求期间所做的任何更改,或如果在请求期间引发异常,则会回滚更改,从而进一步实现这一点。ZopeTransactionExtension提供了一个模块级API,以防视图希望在/是否发生提交时进行自定义。

The upshot is that your view method does not have to call DBSession.commit() : the transaction manager will do it for you. Also, it doesn't have to put the changes in a try-except block because the transaction manager will call DBSession.rollback() if an exception occurs. (Many Pylons actions don't do this so they're technically incorrect.) A side effect is that you cannot 呼叫 DBSession.commit()DBSession.rollback() 直接。If you want to precisely control when something is committed, you'll have to do it this way:

1import transaction
2
3transaction.commit()
4# Or:
5transaction.rollback()

还有一个 transaction.doom() 可以调用以防止 any database writes during this request, including those performed by other parts of the application. Of course, this doesn't affect changes that have already been committed.

您可以通过定义“提交否决”函数来定制自动回滚发生的情况。这在Pyramid图文档中有描述。

Using traversal as a model

Pylons doesn't have a traversal mode, so you have to fetch database objects in the view code. Pyramid's traversal mode essentially does this for you, delivering the object to the view as its context, and handling "not found" for you. Traversal resource tree thus almost looks like a second kind of model, separate from models . (通常在 resources module.) This raises the question of, what's the difference between the two? Does it make sense to convert my model to traversal, or to traversal under the control of a route? The issue comes up further with authorization, because Pyramid's default authorization mechanism is designed for permissions (an access-control list or ACL) to be attached to the context 对象。These are advanced questions so we won't cover them here. Traversal has a learning curve, and it may or may not be appropriate for different kinds of applications. Nevertheless, it's good to know it exists so that you can explore it gradually over time and maybe find a use for it someday.

SQLAHelper and a "models" subpackage

models.py into a package, beware of circular imports. If you define the BaseDBSession 在里面 models/__ini__.py and import them into submodules, and the init module imports the submodules, there will be a circular import of two modules importing each other. One module will appear semi-empty while the other module is running its global code, which could lead to exceptions.

Pylons 通过把基地和会议放在一个子模块中来解决这个问题, models/meta.py, 它没有导入任何其他模型模块。sqlahelper通过提供第三方库来存储引擎、会话和基础来处理它。 Pyramid 开发人员决定默认将整个模型放在一个模块中的最简单的情况,并让您了解如果需要如何拆分它。

模型实例

这些例子是在前一段时间写的,所以他们不使用事务管理器,而且他们还有第三的导入语法。他们应该与SqLalChany 0.6、0.7和0.8一起工作。

A simple one-table model

 1import sqlalchemy as sa
 2import sqlalchemy.orm as orm
 3import sqlalchemy.ext.declarative as declarative
 4from zope.sqlalchemy import ZopeTransactionExtension as ZTE
 5
 6DBSession = orm.scoped_session(orm.sessionmaker(extension=ZTE()))
 7Base = declarative.declarative_base()
 8
 9class User(Base):
10    __tablename__ = "users"
11
12    id = sa.Column(sa.Integer, primary_key=True)
13    name = sa.Column(sa.Unicode(100), nullable=False)
14    email = sa.Column(sa.Unicode(100), nullable=False)

This model has one ORM class, User corresponding to a database table users idnameuser .

三张桌子的模型

We can expand the above into a three-table model suitable for a medium-sized application. ::

 1import sqlalchemy as sa
 2import sqlalchemy.orm as orm
 3import sqlalchemy.ext.declarative as declarative
 4from zope.sqlalchemy import ZopeTransactionExtension as ZTE
 5
 6DBSession = orm.scoped_session(orm.sessionmaker(extension=ZTE()))
 7Base = declarative.declarative_base()
 8
 9class User(Base):
10    __tablename__ = "users"
11
12    id = sa.Column(sa.Integer, primary_key=True)
13    name = sa.Column(sa.Unicode(100), nullable=False)
14    email = sa.Column(sa.Unicode(100), nullable=False)
15
16    addresses = orm.relationship("Address", order_by="Address.id")
17    activities = orm.relationship("Activity",
18        secondary="assoc_users_activities")
19
20    @classmethod
21    def by_name(class_):
22        """Return a query of users sorted by name."""
23        User = class_
24        q = DBSession.query(User)
25        q = q.order_by(User.name)
26        return q
27
28
29class Address(Base):
30    __tablename__ = "addresses"
31
32    id = sa.Column(sa.Integer, primary_key=True)
33    user_id = foreign_key_column(None, sa.Integer, "users.id")
34    street = sa.Column(sa.Unicode(40), nullable=False)
35    city = sa.Column(sa.Unicode(40), nullable=False)
36    state = sa.Column(sa.Unicode(2), nullable=False)
37    zip = sa.Column(sa.Unicode(10), nullable=False)
38    country = sa.Column(sa.Unicode(40), nullable=False)
39    foreign_extra = sa.Column(sa.Unicode(100, nullable=False))
40
41    def __str__(self):
42        """Return the address as a string formatted for a mailing label."""
43        state_zip = u"{0} {1}".format(self.state, self.zip).strip()
44        cityline = filterjoin(u", ", self.city, state_zip)
45        lines = [self.street, cityline, self.foreign_extra, self.country]
46        return filterjoin(u"|n", *lines) + u"\n"
47
48
49class Activity(Base):
50    __tablename__ = "activities"
51
52    id = sa.Column(sa.Integer, primary_key=True)
53    activity = sa.Column(sa.Unicode(100), nullable=False)
54
55
56assoc_users_activities = sa.Table("assoc_users_activities", Base.metadata,
57    foreign_key_column("user_id", sa.Integer, "users.id"),
58    foreign_key_column("activities_id", sa.Unicode(100), "activities.id"))
59
60# Utility functions
61def filterjoin(sep, *items):
62    """Join the items into a string, dropping any that are empty.
63    """
64    items = filter(None, items)
65    return sep.join(items)
66
67def foreign_key_column(name, type_, target, nullable=False):
68    """Construct a foreign key column for a table.
69
70    ``name`` is the column name. Pass ``None`` to omit this arg in the
71    ``Column`` call; i.e., in Declarative classes.
72
73    ``type_`` is the column type.
74
75    ``target`` is the other column this column references.
76
77    ``nullable``: pass True to allow null values. The default is False
78    (the opposite of SQLAlchemy's default, but useful for foreign keys).
79    """
80    fk = sa.ForeignKey(target)
81    if name:
82        return sa.Column(name, type_, fk, nullable=nullable)
83    else:
84        return sa.Column(type_, fk, nullable=nullable)

This model has a User usersAddress 类用 addresses 表和一个 Activity 上课用 activities users is in a 1:Many relationship with addresses . users is also in a Many:Many `` relationship with `` 活动 `` using the association table `` assoc_users_activities .  This is the SQLAlchemy "declarative" syntax, which defines the tables in terms of ORM classes subclassed from a declarative `` 基地 `` class. Association tables do not have an ORM class in SQLAlchemy, so we define it using the `` Table constructor as if we weren't using declarative, but it's still tied to the Base's "metadata".

We can add instance methods to the ORM classes and they will be valid for one database record, as with the Address.__str__ 方法。We can also define class methods that operate on several records or return a query object, as with the User.by_name 方法。

是否有一点分歧 User.by_name works better as a class method or static method. Normally with class methods, the first argument is called class_clsklass and you use it that way throughout the method, but in ORM queries it's more normal to refer to the ORM class by its proper name. But if you do that you're not using the class_ variable so why not make it a static method? But the method does belong to the class in a way that an ordinary static method does not. I go back and forth on this, and sometimes assign User = class_

Common base class

您可以为所有ORM类定义一个超类,使用所有ORM类都可以使用的公共类方法。它将是声明基的父级:

 1class ORMClass(object):
 2    @classmethod
 3    def query(class_):
 4        return DBSession.query(class_)
 5
 6    @classmethod
 7    def get(class_, id):
 8        return Session.query(class_).get(id)
 9
10Base = declarative.declarative_base(cls=ORMClass)
11
12class User(Base):
13    __tablename__ = "users"
14
15    # Column definitions omitted

Then you can do things like this in your views:

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

这是不是好事取决于你的观点。

Multiple databases

The default configuration in the main function configures one database. To connect to multiple databases, list them all in development.ini under distinct prefixes. You can put additional engine arguments under the same prefixes. 例如:

Then modify the main function to add each engine. You can also pass even more engine arguments that override any same-name ones in the INI file. ::

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(binds={models.Person: engine, models.Score: stats})

键在 binds

但有些应用程序更喜欢多个dbsession,每个dbsession连接到不同的数据库。有些应用程序更喜欢多个声明性基,因此不同的ORM类组具有不同的声明性基。或者您可能希望将引擎直接绑定到底层SQL查询的基础元数据。或者您可能正在使用定义自己的dbsession或base的第三方包。在这些情况下,您必须修改模型本身,例如,添加dbsession2或base2。如果配置很复杂,您可能需要像pylons那样定义模型初始化函数,这样顶级例程(主函数或独立实用程序)只需进行一个简单的调用。对于复杂的应用程序,这里有一个非常复杂的初始化例程:

 1DBSession1 = orm.scoped_session(orm.sessionmaker(extension=ZTE())
 2DBSession2 = orm.scoped_session(orm.sessionmaker(extension=ZTE())
 3Base1 = declarative.declarative_base()
 4Base2 = declarative.declarative_base()
 5engine1 = None
 6engine2 = None
 7
 8def init_model(e1, e2):
 9    # e1 and e2 are SQLAlchemy engines. (We can't call them engine1 and
10    # engine2 because we want to access globals with the same name.)
11    global engine1, engine2
12    engine1 = e1
13    engine2 = e2
14    DBSession1.configure(bind=e1)
15    DBSession2.configure(bind=e2)
16    Base1.metadata.bind = e1
17    Base2.metadata.bind = e2

反射表

反射表造成了一个困境,因为它们依赖于活动的数据库连接来初始化。但是在导入模型时,引擎并不知道。这种情况几乎需要一个初始化函数;或者至少我们还没有找到任何解决方法。ORM类仍然可以定义为模块全局(不使用声明性语法),但是当引擎已知时,必须在函数内部完成表定义和映射器调用。以下是非声明性操作的方法:

 1DBSession = orm.scoped_session(orm.sessionmaker(extension=ZTE())
 2# Not using Base; not using declarative syntax
 3md = sa.MetaData()
 4persons = None   # Table, set in init_model().
 5
 6class Person(object):
 7    pass
 8
 9def init_model(engine):
10    global persons
11    DBSession.configure(bind=engine)
12    md.bind = engine
13    persons = sa.Table("persons", md, autoload=True, autoload_with=engine)
14    orm.mapper(Person, persons)

使用声明性语法,我们 think 迈克尔拜耳已经在某个地方发布了这个食谱,但你必须在sqlachmey星球周围寻找它们。最坏情况下,您可以将整个声明性类放入init_model函数中,并将其分配给全局变量。