OK, this new unit test blows me away:
# Place mapper Place.mapper = mapper(Place, place)
# Transition mapper, with Place relations Transition.mapper = mapper(Transition, transition, properties = dict( inputs = relation(Place.mapper, place_output, lazy=True), outputs = relation(Place.mapper, place_input, lazy=True), ) )
# add Transition relations to Place mapper Place.mapper.add_property('inputs', relation(Transition.mapper, place_output, lazy=True)) Place.mapper.add_property('outputs', relation(Transition.mapper, place_input, lazy=True))
# Place eager mapper Place.eagermapper = Place.mapper.options( eagerload('inputs', selectalias='ip_alias'), eagerload('outputs', selectalias='op_alias') )
t1 = Transition('transition1') t2 = Transition('transition2') t3 = Transition('transition3') p1 = Place('place1') p2 = Place('place2') p3 = Place('place3')
t1.inputs.append(p1) t1.inputs.append(p2) t1.outputs.append(p3) t2.inputs.append(p1) p2.inputs.append(t2) p3.inputs.append(t2) p1.outputs.append(t1)
objectstore.commit()
l = Place.eagermapper.select() print repr(l)
Program output:
INSERT INTO place (name) VALUES (:name) {'name': 'place1'} INSERT INTO place (name) VALUES (:name) {'name': 'place2'} INSERT INTO place (name) VALUES (:name) {'name': 'place3'} INSERT INTO transition (name) VALUES (:name) {'name': 'transition1'} INSERT INTO transition (name) VALUES (:name) {'name': 'transition2'} INSERT INTO transition (name) VALUES (:name) {'name': 'transition3'} INSERT INTO place_input (place_id, transition_id) VALUES (:place_id, :transition_id) [{'place_id': 3, 'transition_id': 1}] INSERT INTO place_output (place_id, transition_id) VALUES (:place_id, :transition_id) [{'place_id': 1, 'transition_id': 1}, {'place_id': 2, 'transition_id': 1}, {'place_id': 1, 'transition_id': 2}] INSERT INTO place_input (place_id, transition_id) VALUES (:place_id, :transition_id) [{'place_id': 1, 'transition_id': 1}] INSERT INTO place_output (place_id, transition_id) VALUES (:place_id, :transition_id) [{'place_id': 2, 'transition_id': 2}, {'place_id': 3, 'transition_id': 2}]
SELECT place.place_id AS place_place_id, place.name AS place_name, ip_alias.transition_id AS ip_alias_transition_id, ip_alias.name AS ip_alias_name, op_alias.transition_id AS op_alias_transition_id, op_alias.name AS op_alias_name FROM place LEFT OUTER JOIN place_output ON place.place_id = place_output.place_id LEFT OUTER JOIN transition AS ip_alias ON ip_alias.transition_id = place_output.transition_id LEFT OUTER JOIN place_input ON place.place_id = place_input.place_id LEFT OUTER JOIN transition AS op_alias ON op_alias.transition_id = place_input.transition_id ORDER BY place.oid, place_output.oid, place_input.oid {}
(1, u'place1', 1, u'transition1', 1, u'transition1') (1, u'place1', 2, u'transition2', 1, u'transition1') (2, u'place2', 1, u'transition1', None, None) (2, u'place2', 2, u'transition2', None, None) (3, u'place3', 2, u'transition2', 1, u'transition1') [<__main__.Place object at 0x778b10>, <__main__.Place object at 0x778950>, <__main__.Place object at 0x778bb0>]
On Dec 2, 2005, at 4:07 PM, Shuo Yang wrote:
On 12/2/05, Michael Bayer <[EMAIL PROTECTED]> wrote: I think thats going to give you a circular eager relationship. Also, a circular thing like this needs to be set up in a certain way so that you have both original mappers pointing to each other. The thing about the right relationships being on attached to original mappers is that the commit() dependencies are set up and maintained by those first mappers. The first mapper created by default will track new objects and updates to existing objects, so it needs to be aware of dependencies since it will be called upon to set those up when you say "commit".
This would work better if you defined the relationships with lazy relationships, and used mapper options to create eager-loading versions of those mappers:
Place.mapper = mapper(Place, place)
Transition.mapper = mapper(Transition, transition, properties = dict( inputs = relation(Place.mapper, place_output, lazy=True), outputs = relation( Place.mapper, place_input, lazy=True), ) )
Place.mapper.add_property('inputs', relation(Transition.mapper, place_input, lazy=True)) Place.mapper.add_property('outputs', relation(Transition.mapper , place_output, lazy=True))
Those are the circular lazy mappers. Right off, I would be curious to try commit()ing some objects which are set up that way to make sure it doesnt get confused. I am pretty sure I need to make some fixes just to support the above pattern, with regards to commit(). While having two mappers point to each other is not a problem with a one-to-many relationship, with a many-to-many relationship I dont think the above is going to work correctly right now. Added this to the test: Index: double.py =================================================================== --- double.py (revision 671) +++ double.py (working copy) @@ -71,6 +71,9 @@ ) ) + Place.mapper.add_property('inputs', relation(Transition.mapper, place_input, lazy=True)) + Place.mapper.add_property('outputs', relation(Transition.mapper, place_output, lazy=True)) + tran = Transition('transition1') tran.inputs.append(Place('place1')) tran.outputs.append(Place('place2')) And indeed, you raised a circular dependency error :): File "/Projects/sqlalchemy/lib/sqlalchemy/topological.py", line 64, in sort raise "Circular dependency detected " + repr(edges) + repr(queue) Circular dependency detected {Mapper|Place|place (idself=1079142924): {Mapper|Transition|transition (idself=1079142892): True}, Mapper|Transition|transition (idself=1079142892): {Mapper|Place|place (idself=1079142924): True}}[]
Then assuming the above works, you can make new mappers that handle the eager thing. These mappers would just deal with loads, and both of them reference the corresponding lazy mapper, so there is no circular eager relationship:
Place.eagermapper = Place.mapper.options( eagerload('inputs', selectalias='ip_eager'), eagerload('outputs', selectalias='op_eager'))
Transition.eagermapper = Transition.mapper.options( eagerload('inputs', selectalias='ip_eager'), eagerload('outputs', selectalias='op_eager'))
I like it. :D
You could also have those eagermappers be assigned to 'mapper' and have the two lazy mappers assigned to something else, like 'lazymapper', or whatever, having the mapper attached to the class is not really important in fact. The issue with the above options calls are, you cant do them yet. I need to add the 'selectalias' option to the eagerload() option, and also its not propigated even if you have 'selectalias' on the lazy option. So in the meantime, you probably have to instead create the eagermapper completely as you already did below, i.e.:
Place.eagermapper = mapper(Place, place, properties = dict( inputs = relation(Transition.mapper , place_input, lazy=False, selectalias='ip_alias_t'), outputs = relation(Transition.mapper, place_output, lazy=False, selectalias='op_alias_t'), ) )
Since you are jumping right into some pretty ambitious patterns, ill have to test all of this stuff out tonight or later in the weekend and add new logic to cope with a many-to-many circular relationship, as well as add those options to eagerloader(). Didn't mean to make you work so hard! :p Keep up the good work! Now there are two things I'll be waiting for: double eagerload for sqlalchemy, and zblog paste template.
Shuo Yang wrote: > Thanks Mike. I like this solution better. > > Now, if I want to give similar properties to Place.mapper, would I do > something like this:? > > ----------------------------- > > Place.mapper = mapper(Place, place) > > Transition.mapper = mapper(Transition, transition, properties = dict( > inputs = relation(Place.mapper, place_output, lazy=False, > selectalias='op_alias_p'), > outputs = relation(Place.mapper, place_input, lazy=False, > selectalias='ip_alias_p'), > ) > ) > > Place.mapper = mapper(Place, place, properties = dict( > inputs = relation(Transition.mapper, place_input, lazy=False, > selectalias='ip_alias_t'), > outputs = relation(Transition.mapper, place_output, lazy=False, > selectalias='op_alias_t'), > ) > ) > > ----------------------------- > > > > > On 12/2/05, Michael Bayer < [EMAIL PROTECTED]> wrote: >> >> Hey John - >> Well, this one really kicked my ass for quite awhile, as each fix to the >> issue revealed that the entire approach of using aliases to eager load >> just >> doesnt generally work. >> >> the biggest issue being, if you map the Place class against the place >> table, as well as the output_place and input_place aliases, when you >> create >> new Place objects, they are "dependency-sorted" based on the original >> mapper, i.e. the "place" table mapper, which has no idea about the >> relationships inside the Transition.mapper, so it will screw up often if >> not always. >> >> So while you can now stick an Alias object inside a relation(), you dont >> have to. A more concise syntax that addresses the issue at a more >> specific >> spot has been added: >> >> Place.mapper = mapper(Place, place) >> Transition.mapper = mapper(Transition, transition, properties > >> dict( >> inputs = relation(Place.mapper, place_output, lazy=False, >> selectalias='op_alias'), >> outputs = relation(Place.mapper, place_input, lazy=False, >> selectalias='ip_alias'), >> ) >> ) >> >> Which properly saves new Transition and Place objects and their >> relationships. Upon select it produces a query like: >> >> SELECT transition.transition_id AS transition_transition_id, >> transition.name AS transition_name, >> ip_alias.place_id AS ip_alias_place_id, ip_alias.name AS ip_alias_name, >> op_alias.place_id AS op_alias_place_id, op_alias.name AS op_alias_name >> FROM transition LEFT OUTER JOIN place_input ON transition.transition_id >> > place_input.transition_id >> LEFT OUTER JOIN place AS ip_alias ON ip_alias.place_id > >> place_input.place_id >> LEFT OUTER JOIN place_output ON transition.transition_id > >> place_output.transition_id >> LEFT OUTER JOIN place AS op_alias ON op_alias.place_id > >> place_output.place_id >> ORDER BY transition.oid, place_input.oid, place_output.oid >> >> But despite using the two alias names, the Place object is still mapped >> against the original 'place' table, and EagerLoader performs some >> translation upon the rows coming back in so that they appear to be >> selected >> against 'place' instead of 'ip_alias' or 'op_alias'. >> >> There is also a test case based on the one you gave me inside >> test/double.py . >> >> - mike >> >> >> On Dec 1, 2005, at 1:10 PM, Shuo Yang wrote: >> >> >> On 11/30/05, Michael Bayer < [EMAIL PROTECTED] > wrote: >> > >> > Michael Bayer wrote: >> > > then if that doesnt work, you can also say: >> > > >> > > Transition.mapper = mapper(Transition, transition, properties > >> > dict( >> > > inputs = relation(Place, output_place, place_output, >> > > primaryjoin=and_(output_place.c.foo==place_output.c.foo, >> > > place.c.bar==place_output.c.bar) >> > > lazy=False), >> > >> > err that should be: >> > >> > Transition.mapper = mapper(Transition, transition, properties > > >> dict( >> > inputs = relation(Place, output_place, place_output, >> > primaryjoin=place.c.bar==place_output.c.bar, >> > secondaryjoin=output_place.c.foo==place_output.c.foo, >> > ... >> > lazy=False), >> >> >> >> ... >> outputs = relation(Place, input_place, place_input, lazy=False), >> File "build/bdist.linux-i686/egg/sqlalchemy/mapper.py", line 89, in >> mapper >> File "build/bdist.linux-i686/egg/sqlalchemy/mapper.py", line 264, in >> _init_properties >> File "build/bdist.linux-i686/egg/sqlalchemy/mapper.py", line 1007, in >> init >> File "build/bdist.linux-i686/egg/sqlalchemy/mapper.py", line 682, in >> init >> File "build/bdist.linux-i686/egg/sqlalchemy/mapper.py", line 756, in >> _match_primaries >> AttributeError: 'Alias' object has no attribute 'table' >> >> Now it's this :p >> >> > > > -- > --------------------------------------------------------------------------------------------------- > John S. Yang >
-- --------------------------------------------------------------------------------------------------- John S. Yang
|