What’s new in SQLAlchemy 0.4?

About this Document

This document describes changes between SQLAlchemy version 0.3,last released October 14, 2007, and SQLAlchemy version 0.4,last released October 12, 2008.

Document date: March 21, 2008

First Things First

If you’re using any ORM features, make sure you import fromsqlalchemy.orm:

  1. from sqlalchemy import *
  2. from sqlalchemy.orm import *

Secondly, anywhere you used to say engine=,connectable=, bind_to=, something.engine,metadata.connect(), use bind:

  1. myengine = create_engine('sqlite://')
  2.  
  3. meta = MetaData(myengine)
  4.  
  5. meta2 = MetaData()
  6. meta2.bind = myengine
  7.  
  8. session = create_session(bind=myengine)
  9.  
  10. statement = select([table], bind=myengine)

Got those ? Good! You’re now (95%) 0.4 compatible. Ifyou’re using 0.3.10, you can make these changes immediately;they’ll work there too.

Module Imports

In 0.3, “from sqlachemy import *” would import all ofsqlachemy’s sub-modules into your namespace. Version 0.4 nolonger imports sub-modules into the namespace. This may meanyou need to add extra imports into your code.

In 0.3, this code worked:

  1. from sqlalchemy import *
  2.  
  3. class UTCDateTime(types.TypeDecorator):
  4. pass

In 0.4, one must do:

  1. from sqlalchemy import *
  2. from sqlalchemy import types
  3.  
  4. class UTCDateTime(types.TypeDecorator):
  5. pass

Object Relational Mapping

Querying

New Query API

Query is standardized on the generative interface (oldinterface is still there, just deprecated). While most ofthe generative interface is available in 0.3, the 0.4 Queryhas the inner guts to match the generative outside, and hasa lot more tricks. All result narrowing is via filter()and filter_by(), limiting/offset is either through arrayslices or limit()/offset(), joining is viajoin() and outerjoin() (or more manually, throughselect_from() as well as manually-formed criteria).

To avoid deprecation warnings, you must make some changes toyour 03 code

User.query.get_by( **kwargs )

  1. User.query.filter_by(**kwargs).first()

User.query.select_by( **kwargs )

  1. User.query.filter_by(**kwargs).all()

User.query.select()

  1. User.query.filter(xxx).all()

New Property-Based Expression Constructs

By far the most palpable difference within the ORM is thatyou can now construct your query criterion using class-basedattributes directly. The “.c.” prefix is no longer neededwhen working with mapped classes:

  1. session.query(User).filter(and_(User.name == 'fred', User.id > 17))

While simple column-based comparisons are no big deal, theclass attributes have some new “higher level” constructsavailable, including what was previously only available infilter_by():

  1. # comparison of scalar relations to an instance
  2. filter(Address.user == user)
  3.  
  4. # return all users who contain a particular address
  5. filter(User.addresses.contains(address))
  6.  
  7. # return all users who *dont* contain the address
  8. filter(~User.address.contains(address))
  9.  
  10. # return all users who contain a particular address with
  11. # the email_address like '%foo%'
  12. filter(User.addresses.any(Address.email_address.like('%foo%')))
  13.  
  14. # same, email address equals 'foo@bar.com'. can fall back to keyword
  15. # args for simple comparisons
  16. filter(User.addresses.any(email_address = 'foo@bar.com'))
  17.  
  18. # return all Addresses whose user attribute has the username 'ed'
  19. filter(Address.user.has(name='ed'))
  20.  
  21. # return all Addresses whose user attribute has the username 'ed'
  22. # and an id > 5 (mixing clauses with kwargs)
  23. filter(Address.user.has(User.id > 5, name='ed'))

The Column collection remains available on mappedclasses in the .c attribute. Note that property-basedexpressions are only available with mapped properties ofmapped classes. .c is still used to access columns inregular tables and selectable objects produced from SQLExpressions.

Automatic Join Aliasing

We’ve had join() and outerjoin() for a while now:

  1. session.query(Order).join('items')...

Now you can alias them:

  1. session.query(Order).join('items', aliased=True).
  2. filter(Item.name='item 1').join('items', aliased=True).filter(Item.name=='item 3')

The above will create two joins from orders->items usingaliases. the filter() call subsequent to each willadjust its table criterion to that of the alias. To get atthe Item objects, use add_entity() and target eachjoin with an id:

  1. session.query(Order).join('items', id='j1', aliased=True).
  2. filter(Item.name == 'item 1').join('items', aliased=True, id='j2').
  3. filter(Item.name == 'item 3').add_entity(Item, id='j1').add_entity(Item, id='j2')

Returns tuples in the form: (Order, Item, Item).

Self-referential Queries

So query.join() can make aliases now. What does that giveus ? Self-referential queries ! Joins can be done withoutany Alias objects:

  1. # standard self-referential TreeNode mapper with backref
  2. mapper(TreeNode, tree_nodes, properties={
  3. 'children':relation(TreeNode, backref=backref('parent', remote_side=tree_nodes.id))
  4. })
  5.  
  6. # query for node with child containing "bar" two levels deep
  7. session.query(TreeNode).join(["children", "children"], aliased=True).filter_by(name='bar')

To add criterion for each table along the way in an aliasedjoin, you can use from_joinpoint to keep joining againstthe same line of aliases:

  1. # search for the treenode along the path "n1/n12/n122"
  2.  
  3. # first find a Node with name="n122"
  4. q = sess.query(Node).filter_by(name='n122')
  5.  
  6. # then join to parent with "n12"
  7. q = q.join('parent', aliased=True).filter_by(name='n12')
  8.  
  9. # join again to the next parent with 'n1'. use 'from_joinpoint'
  10. # so we join from the previous point, instead of joining off the
  11. # root table
  12. q = q.join('parent', aliased=True, from_joinpoint=True).filter_by(name='n1')
  13.  
  14. node = q.first()

query.populate_existing()

The eager version of query.load() (orsession.refresh()). Every instance loaded from thequery, including all eagerly loaded items, get refreshedimmediately if already present in the session:

  1. session.query(Blah).populate_existing().all()

Relations

SQL Clauses Embedded in Updates/Inserts

For inline execution of SQL clauses, embedded right in theUPDATE or INSERT, during a flush():

  1. myobject.foo = mytable.c.value + 1
  2.  
  3. user.pwhash = func.md5(password)
  4.  
  5. order.hash = text("select hash from hashing_table")

The column-attribute is set up with a deferred loader afterthe operation, so that it issues the SQL to load the newvalue when you next access.

Self-referential and Cyclical Eager Loading

Since our alias-fu has improved, relation() can joinalong the same table any number of times; you tell it howdeep you want to go. Lets show the self-referentialTreeNode more clearly:

  1. nodes = Table('nodes', metadata,
  2. Column('id', Integer, primary_key=True),
  3. Column('parent_id', Integer, ForeignKey('nodes.id')),
  4. Column('name', String(30)))
  5.  
  6. class TreeNode(object):
  7. pass
  8.  
  9. mapper(TreeNode, nodes, properties={
  10. 'children':relation(TreeNode, lazy=False, join_depth=3)
  11. })

So what happens when we say:

  1. create_session().query(TreeNode).all()

? A join along aliases, three levels deep off the parent:

  1. SELECT
  2. nodes_3.id AS nodes_3_id, nodes_3.parent_id AS nodes_3_parent_id, nodes_3.name AS nodes_3_name,
  3. nodes_2.id AS nodes_2_id, nodes_2.parent_id AS nodes_2_parent_id, nodes_2.name AS nodes_2_name,
  4. nodes_1.id AS nodes_1_id, nodes_1.parent_id AS nodes_1_parent_id, nodes_1.name AS nodes_1_name,
  5. nodes.id AS nodes_id, nodes.parent_id AS nodes_parent_id, nodes.name AS nodes_name
  6. FROM nodes LEFT OUTER JOIN nodes AS nodes_1 ON nodes.id = nodes_1.parent_id
  7. LEFT OUTER JOIN nodes AS nodes_2 ON nodes_1.id = nodes_2.parent_id
  8. LEFT OUTER JOIN nodes AS nodes_3 ON nodes_2.id = nodes_3.parent_id
  9. ORDER BY nodes.oid, nodes_1.oid, nodes_2.oid, nodes_3.oid

Notice the nice clean alias names too. The joining doesn’tcare if it’s against the same immediate table or some otherobject which then cycles back to the beginning. Any kindof chain of eager loads can cycle back onto itself whenjoin_depth is specified. When not present, eagerloading automatically stops when it hits a cycle.

Composite Types

This is one from the Hibernate camp. Composite Types letyou define a custom datatype that is composed of more thanone column (or one column, if you wanted). Lets define anew type, Point. Stores an x/y coordinate:

  1. class Point(object):
  2. def __init__(self, x, y):
  3. self.x = x
  4. self.y = y
  5. def __composite_values__(self):
  6. return self.x, self.y
  7. def __eq__(self, other):
  8. return other.x == self.x and other.y == self.y
  9. def __ne__(self, other):
  10. return not self.__eq__(other)

The way the Point object is defined is specific to acustom type; constructor takes a list of arguments, and thecomposite_values() method produces a sequence ofthose arguments. The order will match up to our mapper, aswe’ll see in a moment.

Let’s create a table of vertices storing two points per row:

  1. vertices = Table('vertices', metadata,
  2. Column('id', Integer, primary_key=True),
  3. Column('x1', Integer),
  4. Column('y1', Integer),
  5. Column('x2', Integer),
  6. Column('y2', Integer),
  7. )

Then, map it ! We’ll create a Vertex object whichstores two Point objects:

  1. class Vertex(object):
  2. def __init__(self, start, end):
  3. self.start = start
  4. self.end = end
  5.  
  6. mapper(Vertex, vertices, properties={
  7. 'start':composite(Point, vertices.c.x1, vertices.c.y1),
  8. 'end':composite(Point, vertices.c.x2, vertices.c.y2)
  9. })

Once you’ve set up your composite type, it’s usable justlike any other type:

  1. v = Vertex(Point(3, 4), Point(26,15))
  2. session.save(v)
  3. session.flush()
  4.  
  5. # works in queries too
  6. q = session.query(Vertex).filter(Vertex.start == Point(3, 4))

If you’d like to define the way the mapped attributesgenerate SQL clauses when used in expressions, create yourown sqlalchemy.orm.PropComparator subclass, defining anyof the common operators (like eq(), le(),etc.), and send it in to composite(). Composite typeswork as primary keys too, and are usable in query.get():

  1. # a Document class which uses a composite Version
  2. # object as primary key
  3. document = query.get(Version(1, 'a'))

dynamic_loader() relations

A relation() that returns a live Query object forall read operations. Write operations are limited to justappend() and remove(), changes to the collection arenot visible until the session is flushed. This feature isparticularly handy with an “autoflushing” session which willflush before each query.

  1. mapper(Foo, foo_table, properties={
  2. 'bars':dynamic_loader(Bar, backref='foo', <other relation() opts>)
  3. })
  4.  
  5. session = create_session(autoflush=True)
  6. foo = session.query(Foo).first()
  7.  
  8. foo.bars.append(Bar(name='lala'))
  9.  
  10. for bar in foo.bars.filter(Bar.name=='lala'):
  11. print(bar)
  12.  
  13. session.commit()

New Options: undefer_group(), eagerload_all()

A couple of query options which are handy.undefer_group() marks a whole group of “deferred”columns as undeferred:

  1. mapper(Class, table, properties={
  2. 'foo' : deferred(table.c.foo, group='group1'),
  3. 'bar' : deferred(table.c.bar, group='group1'),
  4. 'bat' : deferred(table.c.bat, group='group1'),
  5. )
  6.  
  7. session.query(Class).options(undefer_group('group1')).filter(...).all()

and eagerload_all() sets a chain of attributes to beeager in one pass:

  1. mapper(Foo, foo_table, properties={
  2. 'bar':relation(Bar)
  3. })
  4. mapper(Bar, bar_table, properties={
  5. 'bat':relation(Bat)
  6. })
  7. mapper(Bat, bat_table)
  8.  
  9. # eager load bar and bat
  10. session.query(Foo).options(eagerload_all('bar.bat')).filter(...).all()

New Collection API

Collections are no longer proxied by an{{{InstrumentedList}}} proxy, and access to members, methodsand attributes is direct. Decorators now intercept objectsentering and leaving the collection, and it is now possibleto easily write a custom collection class that manages itsown membership. Flexible decorators also replace the namedmethod interface of custom collections in 0.3, allowing anyclass to be easily adapted to use as a collection container.

Dictionary-based collections are now much easier to use andfully dict-like. Changing iter is no longerneeded for dicts, and new built-indict types covermany needs:

  1. # use a dictionary relation keyed by a column
  2. relation(Item, collection_class=column_mapped_collection(items.c.keyword))
  3. # or named attribute
  4. relation(Item, collection_class=attribute_mapped_collection('keyword'))
  5. # or any function you like
  6. relation(Item, collection_class=mapped_collection(lambda entity: entity.a + entity.b))

Existing 0.3 dict-like and freeform object derivedcollection classes will need to be updated for the new API.In most cases this is simply a matter of adding a coupledecorators to the class definition.

Mapped Relations from External Tables/Subqueries

This feature quietly appeared in 0.3 but has been improvedin 0.4 thanks to better ability to convert subqueriesagainst a table into subqueries against an alias of thattable; this is key for eager loading, aliased joins inqueries, etc. It reduces the need to create mappers againstselect statements when you just need to add some extracolumns or subqueries:

  1. mapper(User, users, properties={
  2. 'fullname': column_property((users.c.firstname + users.c.lastname).label('fullname')),
  3. 'numposts': column_property(
  4. select([func.count(1)], users.c.id==posts.c.user_id).correlate(users).label('posts')
  5. )
  6. })

a typical query looks like:

  1. SELECT (SELECT count(1) FROM posts WHERE users.id = posts.user_id) AS count,
  2. users.firstname || users.lastname AS fullname,
  3. users.id AS users_id, users.firstname AS users_firstname, users.lastname AS users_lastname
  4. FROM users ORDER BY users.oid

Horizontal Scaling (Sharding) API

[browser:/sqlalchemy/trunk/examples/sharding/attribute_shard.py]

Sessions

New Session Create Paradigm; SessionContext, assignmapper Deprecated

That’s right, the whole shebang is being replaced with twoconfigurational functions. Using both will produce the most0.1-ish feel we’ve had since 0.1 (i.e., the least amount oftyping).

Configure your own Session class right where you defineyour engine (or anywhere):

  1. from sqlalchemy import create_engine
  2. from sqlalchemy.orm import sessionmaker
  3.  
  4. engine = create_engine('myengine://')
  5. Session = sessionmaker(bind=engine, autoflush=True, transactional=True)
  6.  
  7. # use the new Session() freely
  8. sess = Session()
  9. sess.save(someobject)
  10. sess.flush()

If you need to post-configure your Session, say with anengine, add it later with configure():

  1. Session.configure(bind=create_engine(...))

All the behaviors of SessionContext and the queryand init methods of assignmapper are moved intothe new scoped_session() function, which is compatiblewith both sessionmaker as well as create_session():

  1. from sqlalchemy.orm import scoped_session, sessionmaker
  2.  
  3. Session = scoped_session(sessionmaker(autoflush=True, transactional=True))
  4. Session.configure(bind=engine)
  5.  
  6. u = User(name='wendy')
  7.  
  8. sess = Session()
  9. sess.save(u)
  10. sess.commit()
  11.  
  12. # Session constructor is thread-locally scoped. Everyone gets the same
  13. # Session in the thread when scope="thread".
  14. sess2 = Session()
  15. assert sess is sess2

When using a thread-local Session, the returned classhas all of Session's interface implemented asclassmethods, and “assignmapper“‘s functionality isavailable using the mapper classmethod. Just like theold objectstore days….

  1. # "assignmapper"-like functionality available via ScopedSession.mapper
  2. Session.mapper(User, users_table)
  3.  
  4. u = User(name='wendy')
  5.  
  6. Session.commit()

Sessions are again Weak Referencing By Default

The weak_identity_map flag is now set to True by defaulton Session. Instances which are externally deferenced andfall out of scope are removed from the sessionautomatically. However, items which have “dirty” changespresent will remain strongly referenced until those changesare flushed at which case the object reverts to being weaklyreferenced (this works for ‘mutable’ types, like picklableattributes, as well). Setting weak_identity_map toFalse restores the old strong-referencing behavior forthose of you using the session like a cache.

Auto-Transactional Sessions

As you might have noticed above, we are calling commit()on Session. The flag transactional=True means theSession is always in a transaction, commit()persists permanently.

Auto-Flushing Sessions

Also, autoflush=True means the Session willflush() before each query as well as when you callflush() or commit(). So now this will work:

  1. Session = sessionmaker(bind=engine, autoflush=True, transactional=True)
  2.  
  3. u = User(name='wendy')
  4.  
  5. sess = Session()
  6. sess.save(u)
  7.  
  8. # wendy is flushed, comes right back from a query
  9. wendy = sess.query(User).filter_by(name='wendy').one()

Transactional methods moved onto sessions

commit() and rollback(), as well as begin() arenow directly on Session. No more need to useSessionTransaction for anything (it remains in thebackground).

  1. Session = sessionmaker(autoflush=True, transactional=False)
  2.  
  3. sess = Session()
  4. sess.begin()
  5.  
  6. # use the session
  7.  
  8. sess.commit() # commit transaction

Sharing a Session with an enclosing engine-level (i.e.non-ORM) transaction is easy:

  1. Session = sessionmaker(autoflush=True, transactional=False)
  2.  
  3. conn = engine.connect()
  4. trans = conn.begin()
  5. sess = Session(bind=conn)
  6.  
  7. # ... session is transactional
  8.  
  9. # commit the outermost transaction
  10. trans.commit()

Nested Session Transactions with SAVEPOINT

Available at the Engine and ORM level. ORM docs so far:

http://www.sqlalchemy.org/docs/04/session.html#unitofwork_managing

Two-Phase Commit Sessions

Available at the Engine and ORM level. ORM docs so far:

http://www.sqlalchemy.org/docs/04/session.html#unitofwork_managing

Inheritance

Polymorphic Inheritance with No Joins or Unions

New docs for inheritance: http://www.sqlalchemy.org/docs/04/mappers.html#advdatamapping_mapper_inheritance_joined

Better Polymorphic Behavior with get()

All classes within a joined-table inheritance hierarchy getan _instance_key using the base class, i.e.(BaseClass, (1, ), None). That way when you callget() a Query against the base class, it can locatesubclass instances in the current identity map withoutquerying the database.

Types

Custom Subclasses of sqlalchemy.types.TypeDecorator

There is a New API for subclassing a TypeDecorator.Using the 0.3 API causes compilation errors in some cases.

SQL Expressions

All New, Deterministic Label/Alias Generation

All the “anonymous” labels and aliases use a simple<name>_<number> format now. SQL is much easier to read andis compatible with plan optimizer caches. Just check outsome of the examples in the tutorials:http://www.sqlalchemy.org/docs/04/ormtutorial.htmlhttp://www.sqlalchemy.org/docs/04/sqlexpression.html

Generative select() Constructs

This is definitely the way to go with select(). See http://www.sqlalchemy.org/docs/04/sqlexpression.html#sql_transform .

New Operator System

SQL operators and more or less every SQL keyword there isare now abstracted into the compiler layer. They now actintelligently and are type/backend aware, see:http://www.sqlalchemy.org/docs/04/sqlexpression.html#sql_operators

All type Keyword Arguments Renamed to type_

Just like it says:

  1. b = bindparam('foo', type_=String)

in_ Function Changed to Accept Sequence or Selectable

The in_ function now takes a sequence of values or aselectable as its sole argument. The previous API of passingin values as positional arguments still works, but is nowdeprecated. This means that

  1. my_table.select(my_table.c.id.in_(1,2,3)
  2. my_table.select(my_table.c.id.in_(*listOfIds)

should be changed to

  1. my_table.select(my_table.c.id.in_([1,2,3])
  2. my_table.select(my_table.c.id.in_(listOfIds)

Schema and Reflection

MetaData, BoundMetaData, DynamicMetaData…

In the 0.3.x series, BoundMetaData andDynamicMetaData were deprecated in favor of MetaDataand ThreadLocalMetaData. The older names have beenremoved in 0.4. Updating is simple:

  1. +-------------------------------------+-------------------------+
  2. |If You Had | Now Use |
  3. +=====================================+=========================+
  4. | ``MetaData`` | ``MetaData`` |
  5. +-------------------------------------+-------------------------+
  6. | ``BoundMetaData`` | ``MetaData`` |
  7. +-------------------------------------+-------------------------+
  8. | ``DynamicMetaData`` (with one | ``MetaData`` |
  9. | engine or threadlocal=False) | |
  10. +-------------------------------------+-------------------------+
  11. | ``DynamicMetaData`` | ``ThreadLocalMetaData`` |
  12. | (with different engines per thread) | |
  13. +-------------------------------------+-------------------------+

The seldom-used name parameter to MetaData types hasbeen removed. The ThreadLocalMetaData constructor nowtakes no arguments. Both types can now be bound to anEngine or a single Connection.

One Step Multi-Table Reflection

You can now load table definitions and automatically createTable objects from an entire database or schema in onepass:

  1. >>> metadata = MetaData(myengine, reflect=True)
  2. >>> metadata.tables.keys()
  3. ['table_a', 'table_b', 'table_c', '...']

MetaData also gains a .reflect() method enablingfiner control over the loading process, includingspecification of a subset of available tables to load.

SQL Execution

engine, connectable, and bind_to are all now bind

Transactions, NestedTransactions and TwoPhaseTransactions

Connection Pool Events

The connection pool now fires events when new DB-APIconnections are created, checked out and checked back intothe pool. You can use these to execute session-scoped SQLsetup statements on fresh connections, for example.

Oracle Engine Fixed

In 0.3.11, there were bugs in the Oracle Engine on howPrimary Keys are handled. These bugs could cause programsthat worked fine with other engines, such as sqlite, to failwhen using the Oracle Engine. In 0.4, the Oracle Engine hasbeen reworked, fixing these Primary Key problems.

Out Parameters for Oracle

  1. result = engine.execute(text("begin foo(:x, :y, :z); end;", bindparams=[bindparam('x', Numeric), outparam('y', Numeric), outparam('z', Numeric)]), x=5)
  2. assert result.out_parameters == {'y':10, 'z':75}

Connection-bound MetaData, Sessions

MetaData and Session can be explicitly bound to aconnection:

  1. conn = engine.connect()
  2. sess = create_session(bind=conn)

Faster, More Foolproof ResultProxy Objects