From: Mike Bayer Date: Mon, 6 Sep 2010 14:55:53 +0000 (-0400) Subject: - query.with_parent() now accepts transient objects X-Git-Tag: rel_0_6_4~8 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=ccde5319358aa3eb0e075dbcf01024001230b02b;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - query.with_parent() now accepts transient objects and will use the non-persistent values of their pk/fk attributes in order to formulate the criterion. Docs are also clarified as to the purpose of with_parent(). - fix for PG test executing an alias() --- diff --git a/CHANGES b/CHANGES index 43cdb86ed9..e58eb7e19e 100644 --- a/CHANGES +++ b/CHANGES @@ -21,7 +21,12 @@ CHANGES where a one-step Session constructor is desired. Most users should stick with sessionmaker() for general use, however. - + + - query.with_parent() now accepts transient objects + and will use the non-persistent values of their pk/fk + attributes in order to formulate the criterion. + Docs are also clarified as to the purpose of with_parent(). + - The include_properties and exclude_properties arguments to mapper() now accept Column objects as members in addition to strings. This so that same-named Column diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index 263b611a57..1b8a4b5455 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -720,7 +720,10 @@ class RelationshipProperty(StrategizedProperty): self.prop.parent.compile() return self.prop - def compare(self, op, value, value_is_parent=False, alias_secondary=True): + def compare(self, op, value, + value_is_parent=False, + alias_secondary=True, + detect_transient_pending=False): if op == operators.eq: if value is None: if self.uselist: @@ -728,21 +731,26 @@ class RelationshipProperty(StrategizedProperty): else: return self._optimized_compare(None, value_is_parent=value_is_parent, + detect_transient_pending=detect_transient_pending, alias_secondary=alias_secondary) else: - return self._optimized_compare(value, - value_is_parent=value_is_parent, - alias_secondary=alias_secondary) + return self._optimized_compare(value, + value_is_parent=value_is_parent, + detect_transient_pending=detect_transient_pending, + alias_secondary=alias_secondary) else: return op(self.comparator, value) def _optimized_compare(self, value, value_is_parent=False, - adapt_source=None, alias_secondary=True): + adapt_source=None, + detect_transient_pending=False, + alias_secondary=True): if value is not None: value = attributes.instance_state(value) return self._get_strategy(strategies.LazyLoader).lazy_clause(value, reverse_direction=not value_is_parent, alias_secondary=alias_secondary, + detect_transient_pending=detect_transient_pending, adapt_source=adapt_source) def __str__(self): diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index 41e9a48903..9da7d3ea84 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -648,21 +648,35 @@ class Query(object): self._populate_existing = True def with_parent(self, instance, property=None): - """Add a join criterion corresponding to a relationship to the given - parent instance. - - instance - a persistent or detached instance which is related to class - represented by this query. - - property - string name of the property which relates this query's class to the - instance. if None, the method will attempt to find a suitable - property. - - Currently, this method only works with immediate parent relationships, - but in the future may be enhanced to work across a chain of parent - mappers. + """Add filtering criterion that relates this query's primary entity + to the given related instance, using established :func:`.relationship()` + configuration. + + The SQL rendered is the same as that rendered when a lazy loader + would fire off from the given parent on that attribute, meaning + that the appropriate state is taken from the parent object in + Python without the need to render joins to the parent table + in the rendered statement. + + As of 0.6.4, this method accepts parent instances in all + persistence states, including transient, persistent, and detached. + Only the requisite primary key/foreign key attributes need to + be populated. Previous versions didn't work with transient + instances. + + :param instance: + An instance which is related to the class represented by + this query via some :func:`.relationship`, that also + contains the appropriate attribute state that identifies + the child object or collection. + + :param property: + String property name, or class-bound attribute, which indicates + what relationship should be used to reconcile the parent/child + relationship. If None, the method will use the first relationship + that links them together - note that this is not deterministic + in the case of multiple relationships linking parent/child, + so using None is not recommended. """ from sqlalchemy.orm import properties @@ -684,7 +698,8 @@ class Query(object): prop = mapper.get_property(property, resolve_synonyms=True) return self.filter(prop.compare( operators.eq, - instance, value_is_parent=True)) + instance, value_is_parent=True, + detect_transient_pending=True)) @_generative() def add_entity(self, entity, alias=None): diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index a8c0791134..1b8cf0852a 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -404,7 +404,9 @@ class LazyLoader(AbstractRelationshipLoader): ) def lazy_clause(self, state, reverse_direction=False, - alias_secondary=False, adapt_source=None): + alias_secondary=False, + adapt_source=None, + detect_transient_pending=False): if state is None: return self._lazy_none_clause( reverse_direction, @@ -426,17 +428,29 @@ class LazyLoader(AbstractRelationshipLoader): else: mapper = self.parent_property.parent + o = state.obj() # strong ref + dict_ = attributes.instance_dict(o) + def visit_bindparam(bindparam): if bindparam.key in bind_to_col: - # use the "committed" (database) version to get - # query column values - # also its a deferred value; so that when used - # by Query, the committed value is used - # after an autoflush occurs - o = state.obj() # strong ref - bindparam.value = \ - lambda: mapper._get_committed_attr_by_column( - o, bind_to_col[bindparam.key]) + # using a flag to enable "detect transient pending" so that + # the slightly different usage paradigm of "dynamic" loaders + # continue to work as expected, i.e. that all pending objects + # should use the "post flush" attributes, and to limit this + # newer behavior to the query.with_parent() method. + # It would be nice to do away with this flag. + + if detect_transient_pending and \ + (not state.key or not state.session_id): + bindparam.value = mapper._get_state_attr_by_column( + state, dict_, bind_to_col[bindparam.key]) + else: + # send value as a lambda so that the value is + # acquired after any autoflush occurs. + bindparam.value = \ + lambda: mapper._get_committed_state_attr_by_column( + state, dict_, bind_to_col[bindparam.key]) + if self.parent_property.secondary is not None and alias_secondary: criterion = sql_util.ClauseAdapter( diff --git a/test/dialect/test_postgresql.py b/test/dialect/test_postgresql.py index a605594d44..9ad46c189c 100644 --- a/test/dialect/test_postgresql.py +++ b/test/dialect/test_postgresql.py @@ -1725,7 +1725,7 @@ class ServerSideCursorsTest(TestBase, AssertsExecutionResults): result.close() result = \ sess.query(Foo).execution_options(stream_results=True).\ - subquery().execute() + statement.execute() assert result.cursor.name result.close() finally: diff --git a/test/orm/test_dynamic.py b/test/orm/test_dynamic.py index 5d822fa3d3..c06f6918ad 100644 --- a/test/orm/test_dynamic.py +++ b/test/orm/test_dynamic.py @@ -21,6 +21,7 @@ class DynamicTest(_fixtures.FixtureTest, AssertsCompiledSQL): q = sess.query(User) u = q.filter(User.id==7).first() + eq_([User(id=7, addresses=[Address(id=1, email_address='jack@bean.com')])], q.filter(User.id==7).all()) diff --git a/test/orm/test_query.py b/test/orm/test_query.py index cc2f046cd3..d58f09565d 100644 --- a/test/orm/test_query.py +++ b/test/orm/test_query.py @@ -1396,7 +1396,9 @@ class ParentTest(QueryTest): q = sess.query(Item).with_parent(u1) assert False except sa_exc.InvalidRequestError, e: - assert str(e) == "Could not locate a property which relates instances of class 'Item' to instances of class 'User'" + assert str(e) \ + == "Could not locate a property which relates "\ + "instances of class 'Item' to instances of class 'User'" def test_m2m(self): sess = create_session() @@ -1404,6 +1406,40 @@ class ParentTest(QueryTest): k = sess.query(Keyword).with_parent(i1).all() assert [Keyword(name='red'), Keyword(name='small'), Keyword(name='square')] == k + def test_with_transient(self): + sess = Session() + + q = sess.query(User) + u1 = q.filter_by(name='jack').one() + utrans = User(id=u1.id) + o = sess.query(Order).with_parent(utrans, 'orders') + eq_( + [Order(description="order 1"), Order(description="order 3"), Order(description="order 5")], + o.all() + ) + + def test_with_pending_autoflush(self): + sess = Session() + + o1 = sess.query(Order).first() + opending = Order(id=20, user_id=o1.user_id) + sess.add(opending) + eq_( + sess.query(User).with_parent(opending, 'user').one(), + User(id=o1.user_id) + ) + + def test_with_pending_no_autoflush(self): + sess = Session(autoflush=False) + + o1 = sess.query(Order).first() + opending = Order(user_id=o1.user_id) + sess.add(opending) + eq_( + sess.query(User).with_parent(opending, 'user').one(), + User(id=o1.user_id) + ) + class InheritedJoinTest(_base.MappedTest, AssertsCompiledSQL): run_setup_mappers = 'once'