I did a lot of work this weekend on the relational algrebra features
of SA as well as being able to map to a wider variety of structures.
While you could sort of jam a Select statement into a Mapper all
along it wasnt working that great. Now its working better.
Hopefully good enough that people wont stump me with surprise issues
when they start trying to use it seriously.
Mapping against Relational Structures
----------------------------------------------------
When you make joins, aliases, and selects, they all have accessors
for 'columns', 'primary_key', and 'foreign_keys' just like a table.
The column and foreign key objects reported by these accessors are
usually not the original ones from the underlying tables but are
instead copies that are adjusted to have the relational object they
came from as their parent. You can now make a join against a table
alias and a plain table, or a join against a table and an arbitrary
select statement, and it can still figure out the proper join
conditions from the foreign keys of the underlying tables. I also
streamlined the creation of all these "exported" objects so that they
get created only when first called upon, and also with as little
redundant looping as possible.
As a stress-test I made a change to the Zblog demo in how it loads
the listing of posts and all its comment counts. My goal with
constructing mappers is usually focused on decreasing the number of
individual queries as much as possible since that is typically the
biggest speed hit. Previously, the homepage that listed out all the
posts in a blog would have to do another round trip for each post to
get the count of comments. Now I have managed to roll that count up
into the main mapper for the Post object.
So the query to select a Post along with its comment count looks like
this:
posts_with_ccount = select(
[c for c in tables.posts.c if c.key != 'body'] + [
func.count(tables.comments.c.comment_id).label('comment_count')
],
from_obj = [outerjoin(tables.posts, tables.comments)],
group_by=[c for c in tables.posts.c]
) .alias('postswcount')
The "body" column is left out since we are going to deferred load
that. When mapping against a regular table, the 'deferred' loaders
will take their corresponding column out of the select list. But
here we are removing it from the "inner" query (though it might not
matter for most databases if its data isnt ultimately sent over the
wire). Anyway, the select statement for this looks like:
SELECT
posts.post_id AS post_id,
posts.blog_id AS blog_id,
posts.user_id AS user_id,
posts.datetime AS datetime,
posts.headline AS headline,
posts.summary AS summary,
count(comments.comment_id) AS comment_count
FROM posts
LEFT OUTER JOIN comments ON posts.post_id = comments.post_id
GROUP BY posts.post_id,
posts.blog_id, posts.user_id, posts.datetime,
posts.headline, posts.summary, posts.body
Then we can now just map to that query, complete with the eager join
of the keywords, deferred loading of the post's body, etc.:
Post.mapper = mapper(Post, posts_with_ccount, properties={
'id':posts_with_ccount.c.post_id,
'body':deferred(tables.posts.c.body),
'user':relation(user.User, lazy=True, backref='posts'),
'blog':relation(Blog, lazy=True, backref='posts'),
'topics':relation(TopicAssociation, lazy=False, private=True,
association=Topic)
}, is_primary=True, order_by=[desc(posts_with_ccount.c.datetime)])
The "deferred" property on "body" is made using the column from the
underlying table. This has the effect of using just the original
table when it makes the second query for the body, instead of joining
against the bigger query used by the primary load. The generated
queries work with SQLite (which is lenient) and Postgres (which is
very strict), which bodes well for MySQL and Oracle which I havent
tested yet.
Lazy Load uses mapper.get() when possible
-------------------------------------------------------------
I made an additional improvement to lazy loading as well; above, when
the "user" property on the "Post" is loaded, since its searching for
a User object by its primary key, it will skip the query if the
desired User object has already been loaded. So "many-to-one" lazy
loads are less expensive in many cases.
objectstore.import_instance()
-------------------------------------------------
several people have made comments about how SQLAlchemy has fewer than
expected "surprises", partially because theres a lot of exceptions it
raises when constructing mappers and committing to make sure various
pitfalls dont occur. these exceptions are usually the result of my
own coming across a particular pitfall and then spending four hours
trying to figure out why; as this thing goes pretty deep, a small
error condition can easily lead to insanely confounding issues.
when you fall into a hole with this thing, the hole can be pretty
deep and dark.
so todays insanely confounding issue was due to something I knew
would be needed, the idea of "de-passivating" objects that come from
the outside, meaning outside the unit-of-work, into the current
session. Every object instance you deal with in SQLAlchemy is
stored in a thread local dictionary called an "identity map". This
is to insure that every object is unique to its given identity, and
that operations arent performed on two different copies of an object
that should only happen on a single instance. If this condition
does occur, you get things like attaching a User to a Post, which
should be the same User that was already there, results in a "delete"
of one copy and an "add" operation of the other via a backreference,
and the commit totally blows up since one operation wants to delete
the Post and the other wants to save it.
The "database identity" of an object as SA is concerned is
deterimined by an attribute it sets on your object called
"_instance_key". So as long as you dont screw with that attribute,
the only way to have two copies like this is to share an object
across threads or unit-of-work instances, which happens for example
when you serialize something into the session and then pull it
back. Now, if you try to commit the current unit of work and it
detects objects with _instance_key that arent in the identity map, it
will raise "Detected a mapped object not present in the current
thread's Identity Map: '%s'. Use objectstore.import_instance() to
place deserialized instances or instances from other threads".
So all that calls for is, when you are pulling something out of a
session or some other place not from a Mapper or from a new
constructor, is to say:
myobj = objectstore.import_instance(myobj)
which will either return the object you give it, or the one that was
already in the identity map. This is illustrated in the ZBlog demo
where it pulls the User object from the session.
Also keep in mind that you *can* use objects across threads and
serializations without dealing with import_instance(), as long as its
in a read-only fashion. The only time their "identity" becomes an
issue is when you change the objects, particularly when you go to
commit() them, or if you are relying upon in-memory identity
comparisons with them. So for objects that are to be modified and
saved, its best to insure that they are all loaded within the scope
of the current unit of work. For objects that just get loaded and
read by various threads and serializers, theres less to worry about.
-------------------------------------------------------
This SF.net email is sponsored by: Splunk Inc. Do you grep through log files
for problems? Stop! Download the new AJAX search engine that makes
searching your log files as easy as surfing the web. DOWNLOAD SPLUNK!
http://ads.osdn.com/?ad_id=7637&alloc_id=16865&op=click
_______________________________________________
Sqlalchemy-users mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/sqlalchemy-users