Events (load and after_attach) for instance stamping

138 views
Skip to first unread message

YKdvd

unread,
Jan 11, 2013, 1:22:13 AM1/11/13
to sqlal...@googlegroups.com
In the MySQL system I'm redoing with SQLAlchemy, there is effectively a "master' schema that exists once top-level stuff (including a table describing projects) , and "project" schemas (one identical set of tables per project), which have project-level stuff.  When an object is retrieved from a project, there is nothing in the data itself which indicates which project it was loaded from, but this may in certain cases be needed.  I have a nice collection of Project objects loaded from the master schema lying around, and I pass my own subclass of SQLA's Session to sessionmaker() so I can store a reference to the appropriate Project with a session.  But since Detached and Transient instances don't have an associated session, I can't just use object_session() to access the Project via the instance's session.  So I thought I'd stamp a reference to the appropriate Project onto instances.

SQLA's event system has the "after_attach" session event.  Hooking into this works for new instances I attach to a session with add(), but does't fire when items are loaded from a query - presumably "attached" means direct userland attachment only, not instances being associated with a session via a query. So it's only half the solution - there doesn't seem to be a session-level  event that fires in the latter case?

There is a "load" instance-level event, which seems to solve the other half of this.  The QueryContext passed to the handler contains the session, and I can stamp the instance with the Project reference I've stored in the session.  The only drawback is that I apparently need to separately attach the event listener to each project instance class?  Presumably I could somehow retrieve a list of classes from the declarative_base (_decl_class_registry_ ?) and walk through that adding the listeners.

Is this a reasonable approach, and between the two events I'll be seeing all the objects that enter the session either from database retrieval or my code?


Michael Bayer

unread,
Jan 11, 2013, 1:34:09 AM1/11/13
to sqlal...@googlegroups.com

On Jan 11, 2013, at 1:22 AM, YKdvd wrote:

>
> SQLA's event system has the "after_attach" session event. Hooking into this works for new instances I attach to a session with add(), but does't fire when items are loaded from a query - presumably "attached" means direct userland attachment only, not instances being associated with a session via a query. So it's only half the solution - there doesn't seem to be a session-level event that fires in the latter case?

there is not, you'd use the instance event

>
> There is a "load" instance-level event, which seems to solve the other half of this. The QueryContext passed to the handler contains the session, and I can stamp the instance with the Project reference I've stored in the session. The only drawback is that I apparently need to separately attach the event listener to each project instance class?

you can associate the instance event with the mapper() callable or Mapper class, and it will take effect for all mapped classes.

Using 0.8 you can also associate the listener with a common base class, such as your declarative Base, or even a mixin that's common to some subset of classes.

The approach of listening for configured mappers and then associating the instance event at that point works as well too but hopefully that shouldn't be necessary here.


YKdvd

unread,
Jan 11, 2013, 2:26:28 AM1/11/13
to sqlal...@googlegroups.com
On Friday, January 11, 2013 2:34:09 AM UTC-4, Michael Bayer wrote:

> you can associate the instance event with the mapper() callable or Mapper class, and it will take effect for all mapped classes. 

I think that would work for my case, although I'm a little fuzzy as to the exact syntax to provide the mapper reference for event.listen - I've just been using declarative and haven't dealt with mappers directly - each of my classes would have a different mapper instance, and I'd have to attach to each (no benefit over class attachment)?  Or using the Mapper class itself would trigger for anything mapped, and I'd have to discriminate in the handler for classes of interest, or create a Mapper subclass and somehow have my declarative_base subclass use it?  
 
Using 0.8 you can also associate the listener with a common base class, such as your declarative Base, or even a mixin that's common to some subset of classes.

That sounds even nicer, and since I'm just starting out with no legacy code I've been meaning to try dropping 0.8 in even before it is final.  It sounds like I could just do something like event.listen(MyDeclarativeSubclass, "load", myStamperFunc).

YKdvd

unread,
Jan 11, 2013, 10:19:14 AM1/11/13
to sqlal...@googlegroups.com
That sounds even nicer, and since I'm just starting out with no legacy code I've been meaning to try dropping 0.8 in even before it is final.  It sounds like I could just do something like event.listen(MyDeclarativeSubclass, "load", myStamperFunc).

 I popped in 0.8b2 and tried something like this:

class SubBase(object):
...
def MyLoadHandler(instance, context):
   print "hit the load handler"
MyBase = declarative_base(cls=SubBase)
event.listen(MyBase, "load", MyLoadHandler)
...
class MyDataClass(MyBase):
... do all the declarative stuff
...
anInstance=session.query(MyDataClass).first()


It doesn't complain about a non-existent event when I install the handler, but the event handler doesn't seem to get called when the instance gets loaded with the query.  I'll try and do a minimal little standalone test to make sure it isn't something in my system messing things up, but in theory this should work?

Michael Bayer

unread,
Jan 11, 2013, 10:25:06 AM1/11/13
to sqlal...@googlegroups.com
in theory.  It's new stuff and was very tricky to get it to work, so feel free to send a brief test along.




--
You received this message because you are subscribed to the Google Groups "sqlalchemy" group.
To view this discussion on the web visit https://groups.google.com/d/msg/sqlalchemy/-/rQwgslRRDGsJ.
To post to this group, send email to sqlal...@googlegroups.com.
To unsubscribe from this group, send email to sqlalchemy+...@googlegroups.com.
For more options, visit this group at http://groups.google.com/group/sqlalchemy?hl=en.

Michael Bayer

unread,
Jan 11, 2013, 10:29:45 AM1/11/13
to sqlal...@googlegroups.com
On Jan 11, 2013, at 2:26 AM, YKdvd wrote:

On Friday, January 11, 2013 2:34:09 AM UTC-4, Michael Bayer wrote:

> you can associate the instance event with the mapper() callable or Mapper class, and it will take effect for all mapped classes. 

I think that would work for my case, although I'm a little fuzzy as to the exact syntax to provide the mapper reference for event.listen - I've just been using declarative and haven't dealt with mappers directly - each of my classes would have a different mapper instance, and I'd have to attach to each (no benefit over class attachment)?  Or using the Mapper class itself would trigger for anything mapped, and I'd have to discriminate in the handler for classes of interest, or create a Mapper subclass and somehow have my declarative_base subclass use it?  

the events just assign special meaning to the actual mapper() function:

from sqlalchemy.orm import mapper

@event.listens_for(mapper, "init")
def foo(...)


YKdvd

unread,
Jan 11, 2013, 11:35:04 AM1/11/13
to sqlal...@googlegroups.com
On Friday, January 11, 2013 11:25:06 AM UTC-4, Michael Bayer wrote:

in theory.  It's new stuff and was very tricky to get it to work, so feel free to send a brief test along.

Here's a minimal example I quickly put together - it retrieves from the database, but the handler doesn't seem to fire, and so the print statement on the second-last line fails.  I'm using the standard MySQLdb with MySQL 5.0.43 (sigh, they promise to update soon) - the server is on a Debian box, and the client code is running in Python 1.6.4 on Windows 7 (it's actually the 64-bit Python interpreter bundled with the Maya graphics package):

from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Table, Column, Integer, event, create_engine
from sqlalchemy.orm import sessionmaker

def My_load_instance_handler(instance, context):
instance.itsProduction = None #should add itsProduction attribute to all loaded instances
print "stamped loaded instance with production"

AlchemyBase = declarative_base()
event.listen(AlchemyBase, "load", My_load_instance_handler)
dbURL = "mysql://User:password@%s/%s" % ("dbServer", "dbName")  #replace with actual parameters
engine = create_engine(dbURL)
AlchemyBase.metadata.bind=engine

class MyDataClass(AlchemyBase):
__tablename__ = "vw_users"  #replace with actual table name
id = Column(Integer, primary_key=True)

Session = sessionmaker(bind=engine)
session = Session()
instance = session.query(MyDataClass).first()
print instance.id, instance.itsProduction #this fails with no itsProduction attribute
session.close()

Michael Bayer

unread,
Jan 11, 2013, 1:58:31 PM1/11/13
to sqlal...@googlegroups.com
On Jan 11, 2013, at 11:35 AM, YKdvd wrote:

On Friday, January 11, 2013 11:25:06 AM UTC-4, Michael Bayer wrote:

in theory.  It's new stuff and was very tricky to get it to work, so feel free to send a brief test along.

Here's a minimal example I quickly put together - it retrieves from the database, but the handler doesn't seem to fire, and so the print statement on the second-last line fails.  I'm using the standard MySQLdb with MySQL 5.0.43 (sigh, they promise to update soon) - the server is on a Debian box, and the client code is running in Python 1.6.4 on Windows 7 (it's actually the 64-bit Python interpreter bundled with the Maya graphics package):

from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Table, Column, Integer, event, create_engine
from sqlalchemy.orm import sessionmaker

def My_load_instance_handler(instance, context):
instance.itsProduction = None #should add itsProduction attribute to all loaded instances
print "stamped loaded instance with production"

AlchemyBase = declarative_base()
event.listen(AlchemyBase, "load", My_load_instance_handler)

whoops, forgot the propagate flag:

event.listen(Base, "load", My_load_instance_handler, propagate=True)



YKdvd

unread,
Jan 11, 2013, 2:40:12 PM1/11/13
to sqlal...@googlegroups.com
On Friday, January 11, 2013 2:58:31 PM UTC-4, Michael Bayer wrote:
whoops, forgot the propagate flag:

event.listen(Base, "load", My_load_instance_handler, propagate=True)

That seems to do the trick.  I'd seen mention of propagate flags while wandering through the event docs, but the session event I started with didn't need it, and it didn't come to mind.  Thanks again. 
Reply all
Reply to author
Forward
0 new messages