Models¶
Models are essentially the same in Pyramid and Pylons because the framework is only minimally involved with the model unlike, say, Django where the ORM (object-relational mapper) is framework-specific and other parts of the framework assume it's that specific kind. In Pyramid and Pylons, the application skeleton merely suggests where to put the models and initializes a SQLAlchemy database connection for you. Here's the default Pyramid configuration (comments stripped and imports squashed):
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
and its INI files:
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:
ZopeTransactionExtension and the "pyramid_tm" tween.
"models" (plural) instead of "model" (singular).
A module rather than a subpackage.
"DBSession" instead of "Session".
No init_model() function.
Slightly different import style and variable naming.
Only the first one is an essential difference; the rest are just aesthetic programming styles. So you can change them without harming anything.
The model-models difference is due to an ambiguity in how the word "model" is used. Some people say "a model" to refer to an individual ORM class, while others say "the model" to refer to the entire collection of ORM classes in an application. This guide uses the word both ways.
What belongs in the model?¶
Good programming practice recommends keeping your data classes separate from user-interface classes. That way the user interface can change without affecting the data and vice-versa. The model is where the data classes go. For instance, a Monopoly game has players, a board, squares, title deeds, cards, etc, so a Monopoly program would likely have classes for each of these. If the application requires saving data between runs (persistence), the data will have to be stored in a database or equivalent. Pyramid can work with a variety of database types: SQL database, object database, key-value database ("NoSQL"), document database (JSON or XML), CSV files, etc. The most common choice is SQLAlchemy, so that's the first configuration provided by Pyramid and Pylons.
At minimum you should define your ORM classes in the model. You can also add any business logic in the form of functions, class methods, or regular methods. It's sometimes difficult to tell whether a particular piece of code belongs in the model or the view, but we'll leave that up to you.
Another principle is that the model should not depend on the rest of the application so that it can be used on its own; e.g., in utility programs or in other applications. That also allows you to extract the data if the framework or application breaks. So the view knows about the model but not vice-versa. Not everybody agrees with this but it's a good place to start.
Larger projects may share a common model between multiple web applications and non-web programs. In that case it makes sense to put the model in a separate top-level package and import it into the Pyramid application.
Transaction manger¶
Pylons has never used a transaction manager but it's common in TurboGears and Zope. A transaction manager takes care of the commit-rollback cycle for you. The database session in both applications above is a scoped session, meaning it's a threadlocal global and must be cleared out at the end of every request. The Pylons app has special code in the base controller to clear out the session. A transaction manager takes this a step further by committing any changes made during the request, or if an exception was raised during the request, it rolls back the changes. The ZopeTransactionExtension provides a module-level API in case the view wants to customize when/whether committing occurs.
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
call DBSession.commit()
or DBSession.rollback()
directly. 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()
There's also a transaction.doom()
function which you can call to prevent
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.
You can customize the circumstances under which an automatic rollback occurs by defining a "commit veto" function. This is described in the pyramid_tm documentation.
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
. (It's typically defined in a 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 object. 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¶
Earlier versions of Akhet used the SQLAHelper library to organize engines and
sessions. This is no longer documented because it's not that much benefit. The
main thing to remember is that if you split models.py into a package, beware
of circular imports. If you define the Base
and DBSession
in
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 dealt with this by putting the Base and Session in a submodule, models/meta.py, which did not import any other model modules. SQLAHelper deals with it by providing a third-party library to store engines, sessions, and bases. The Pyramid developers decided to default to the simplest case of the putting entire model in one module, and let you figure out how to split it if you want to.
Model Examples¶
These examples were written a while ago so they don't use the transaction manager, and they have yet at third importing syntax. They should work with SQLAlchemy 0.6, 0.7, and 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
. The table has three columns: id
, name
, and user
.
A three-table model¶
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
class corresponding to a users
table, an
Address
class with an addresses
table, and an Activity
class with
activities
table. users
is in a 1:Many relationship with
addresses
. users
is also in a Many:Many`` relationship with
activities
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 Base
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__
method. We can also define
class methods that operate on several records or return a query object, as with
the User.by_name
method.
There's a bit of disagreement on whether User.by_name
works better as a
class method or static method. Normally with class methods, the first argument
is called class_
or cls
or klass
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_
at the beginning of the method. But none of
these ways feels completely satisfactory, so I'm not sure which is best.
Common base class¶
You can define a superclass for all your ORM classes, with common class methods that all of them can use. It will be the parent of the declarative base:
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()
Whether this is a good thing or not depends on your perspective.
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. For instance:
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.")
At this point you have a choice. Do you want to bind different tables to different databases in the same DBSession? That's easy:
DBSession.configure(binds={models.Person: engine, models.Score: stats})
The keys in the binds
dict can be SQLAlchemy ORM classes, table objects, or
mapper objects.
But some applications prefer multiple DBSessions, each connected to a different database. Some applications prefer multiple declarative bases, so that different groups of ORM classes have a different declarative base. Or perhaps you want to bind the engine directly to the Base's metadata for low-level SQL queries. Or you may be using a third-party package that defines its own DBSession or Base. In these cases, you'll have to modify the model itself, e.g., to add a DBSession2 or Base2. If the configuration is complex you may want to define a model initialization function like Pylons does, so that the top-level routine (the main function or a standalone utility) only has to make one simple call. Here's a pretty elaborate init routine for a complex application:
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
Reflected tables¶
Reflected tables pose a dilemma because they depend on a live database connection in order to be initialized. But the engine is not known when the model is imported. This situation pretty much requires an initialization function; or at least we haven't found any way around it. The ORM classes can still be defined as module globals (not using the declarative syntax), but the table definitions and mapper calls will have to be done inside the function when the engine is known. Here's how you'd do it non-declaratively:
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)
With the declarative syntax, we think Michael Bayer has posted recipies for this somewhere, but you'll have to poke around the SQLAlchmey planet to find them. At worst you could put the entire declarative class inside the init_model function and assign it to a global variable.