From: Mike Bayer Date: Fri, 20 Jul 2007 20:02:41 +0000 (+0000) Subject: - added query.populate_existing().. - marks the query to reload X-Git-Tag: rel_0_4_6~59 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=c75ff59fc5ccad21a968fe10121a225de3b15f7c;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - added query.populate_existing().. - marks the query to reload all attributes and collections of all instances touched in the query, including eagerly-loaded entities [ticket:660] - added eagerload_all(), allows eagerload_all('x.y.z') to specify eager loading of all properties in the given path --- diff --git a/CHANGES b/CHANGES index b9f8b8bf09..ce23ba9e63 100644 --- a/CHANGES +++ b/CHANGES @@ -25,12 +25,24 @@ - AttributeExtension moved to interfaces, and .delete is now .remove The event method signature has also been swapped around. - - major interface pare-down for Query: all selectXXX methods + - major overhaul for Query: all selectXXX methods are deprecated. generative methods are now the standard way to do things, i.e. filter(), filter_by(), all(), one(), etc. Deprecated methods are docstring'ed with their new replacements. + + - Class-level properties are now usable as query elements ...no + more '.c.' ! "Class.c.propname" is now superceded by "Class.propname". + All clause operators are supported, as well as higher level operators + such as Class.prop== for scalar attributes and + Class.prop.contains() for collection-based attributes + (both are also negatable). Table-based column expressions as well as + columns mounted on mapped classes via 'c' are of course still fully available + and can be freely mixed with the new attributes. + [ticket:643] + - removed ancient query.select_by_attributename() capability. + - added "aliased joins" positional argument to the front of filter_by(). this allows auto-creation of joins that are aliased locally to the individual filter_by() call. This allows the @@ -38,6 +50,13 @@ querying divergent criteria. ClauseElements at the front of filter_by() are removed (use filter()). + - added query.populate_existing().. - marks the query to reload + all attributes and collections of all instances touched in the query, + including eagerly-loaded entities [ticket:660] + + - added eagerload_all(), allows eagerload_all('x.y.z') to specify eager + loading of all properties in the given path + - Eager loading has been enhanced to allow even more joins in more places. It now functions at any arbitrary depth along self-referential and cyclical structures. When loading cyclical structures, specify "join_depth" @@ -47,15 +66,6 @@ scheme now and are a lot easier on the eyes, as well as of course completely deterministic. [ticket:659] - - Class-level properties are now usable as query elements ...no - more '.c.' ! "Class.c.propname" is now superceded by "Class.propname". - All clause operators are supported, as well as higher level operators - such as Class.prop== for scalar attributes and - Class.prop.contains() for collection-based attributes - (both are also negatable). Table-based column expressions as well as - columns mounted on mapped classes via 'c' are of course still fully available - and can be freely mixed with the new attributes. - [ticket:643] - added composite column properties. This allows you to create a type which is represented by more than one column, when using the diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index 9e9615635c..6a790def40 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -22,7 +22,7 @@ from sqlalchemy.orm.session import Session as create_session from sqlalchemy.orm.session import object_session, attribute_manager __all__ = ['relation', 'column_property', 'composite', 'backref', 'eagerload', - 'lazyload', 'noload', 'deferred', 'defer', 'undefer', + 'eagerload_all', 'lazyload', 'noload', 'deferred', 'defer', 'undefer', 'undefer_group', 'extension', 'mapper', 'clear_mappers', 'compile_mappers', 'class_mapper', 'object_mapper', 'MapperExtension', 'Query', 'polymorphic_union', 'create_session', @@ -153,6 +153,22 @@ def eagerload(name): return strategies.EagerLazyOption(name, lazy=False) +def eagerload_all(name): + """Return a ``MapperOption`` that will convert all + properties along the given dot-separated path into an + eager load. + + e.g:: + query.options(eagerload_all('orders.items.keywords'))... + + will set all of 'orders', 'orders.items', and 'orders.items.keywords' + to load in one eager load. + + Used with ``query.options()``. + """ + + return strategies.EagerLazyOption(name, lazy=False, chained=True) + def lazyload(name): """Return a ``MapperOption`` that will convert the property of the given name into a lazy load. diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py index 4cad68e346..42ae5c8ddf 100644 --- a/lib/sqlalchemy/orm/interfaces.py +++ b/lib/sqlalchemy/orm/interfaces.py @@ -497,28 +497,30 @@ class PropertyOption(MapperOption): def __init__(self, key): self.key = key - def process_query_property(self, context, property): + def process_query_property(self, context, properties): pass - def process_selection_property(self, context, property): + def process_selection_property(self, context, properties): pass def process_query_context(self, context): - self.process_query_property(context, self._get_property(context)) + self.process_query_property(context, self._get_properties(context)) def process_selection_context(self, context): - self.process_selection_property(context, self._get_property(context)) + self.process_selection_property(context, self._get_properties(context)) - def _get_property(self, context): + def _get_properties(self, context): try: - prop = self.__prop + l = self.__prop except AttributeError: + l = [] mapper = context.mapper for token in self.key.split('.'): prop = mapper.get_property(token, resolve_synonyms=True) + l.append(prop) mapper = getattr(prop, 'mapper', None) - self.__prop = prop - return prop + self.__prop = l + return l PropertyOption.logger = logging.class_logger(PropertyOption) @@ -543,13 +545,24 @@ class StrategizedOption(PropertyOption): for an operation by a StrategizedProperty. """ - def process_query_property(self, context, property): + def is_chained(self): + return False + + def process_query_property(self, context, properties): self.logger.debug("applying option to QueryContext, property key '%s'" % self.key) - context.attributes[("loaderstrategy", property)] = self.get_strategy_class() + if self.is_chained(): + for prop in properties: + context.attributes[("loaderstrategy", prop)] = self.get_strategy_class() + else: + context.attributes[("loaderstrategy", properties[-1])] = self.get_strategy_class() - def process_selection_property(self, context, property): + def process_selection_property(self, context, properties): self.logger.debug("applying option to SelectionContext, property key '%s'" % self.key) - context.attributes[("loaderstrategy", property)] = self.get_strategy_class() + if self.is_chained(): + for prop in properties: + context.attributes[("loaderstrategy", prop)] = self.get_strategy_class() + else: + context.attributes[("loaderstrategy", properties[-1])] = self.get_strategy_class() def get_strategy_class(self): raise NotImplementedError() diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index aeae60365c..6a9ddc57ac 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -122,6 +122,22 @@ class Query(object): criterion = prop.compare(operator.eq, instance, value_is_parent=True) return Query(target, **kwargs).filter(criterion) query_from_parent = classmethod(query_from_parent) + + def populate_existing(self): + """return a Query that will refresh all instances loaded. + + this includes all entities accessed from the database, including + secondary entities, eagerly-loaded collection items. + + All changes present on entities which are already present in the session will + be reset and the entities will all be marked "clean". + + This is essentially the en-masse version of load(). + """ + + q = self._clone() + q._populate_existing = True + return q def with_parent(self, instance, property=None): """add a join criterion corresponding to a relationship to the given parent instance. diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index 473fe7729a..0ceb969559 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -740,17 +740,22 @@ class EagerLoader(AbstractRelationLoader): EagerLoader.logger = logging.class_logger(EagerLoader) class EagerLazyOption(StrategizedOption): - def __init__(self, key, lazy=True): + def __init__(self, key, lazy=True, chained=False): super(EagerLazyOption, self).__init__(key) self.lazy = lazy - - def process_query_property(self, context, prop): + self.chained = chained + + def is_chained(self): + return not self.lazy and self.chained + + def process_query_property(self, context, properties): if self.lazy: - if prop in context.eager_loaders: - context.eager_loaders.remove(prop) + if properties[-1] in context.eager_loaders: + context.eager_loaders.remove(properties[-1]) else: - context.eager_loaders.add(prop) - super(EagerLazyOption, self).process_query_property(context, prop) + for prop in properties: + context.eager_loaders.add(prop) + super(EagerLazyOption, self).process_query_property(context, properties) def get_strategy_class(self): if self.lazy: @@ -775,8 +780,8 @@ class FetchModeOption(PropertyOption): raise exceptions.ArgumentError("Fetchmode must be one of 'join' or 'select'") self.type = type - def process_selection_property(self, context, property): - context.attributes[('fetchmode', property)] = self.type + def process_selection_property(self, context, properties): + context.attributes[('fetchmode', properties[-1])] = self.type class RowDecorateOption(PropertyOption): def __init__(self, key, decorator=None, alias=None): @@ -784,17 +789,17 @@ class RowDecorateOption(PropertyOption): self.decorator = decorator self.alias = alias - def process_selection_property(self, context, property): + def process_selection_property(self, context, properties): if self.alias is not None and self.decorator is None: if isinstance(self.alias, basestring): - self.alias = property.target.alias(self.alias) + self.alias = properties[-1].target.alias(self.alias) def decorate(row): d = {} - for c in property.target.columns: + for c in properties[-1].target.columns: d[c] = row[self.alias.corresponding_column(c)] return d self.decorator = decorate - context.attributes[("eager_row_processor", property)] = self.decorator + context.attributes[("eager_row_processor", properties[-1])] = self.decorator RowDecorateOption.logger = logging.class_logger(RowDecorateOption) diff --git a/lib/sqlalchemy/sql.py b/lib/sqlalchemy/sql.py index 7677aab9c7..3ca5b0921b 100644 --- a/lib/sqlalchemy/sql.py +++ b/lib/sqlalchemy/sql.py @@ -1311,6 +1311,8 @@ class _CompareMixin(ColumnOperators): obj = self._check_literal(obj) type_ = self._compare_type(obj) + + # TODO: generalize operator overloading like this out into the types module if op == operator.add and isinstance(type_, (sqltypes.Concatenable)): op = ColumnOperators.concat_op diff --git a/test/orm/fixtures.py b/test/orm/fixtures.py index ddec0a2812..39b0da383f 100644 --- a/test/orm/fixtures.py +++ b/test/orm/fixtures.py @@ -36,7 +36,7 @@ class Base(object): continue else: if value is not None: - if value != getattr(other, attr): + if value != getattr(other, attr, None): return False else: return True diff --git a/test/orm/mapper.py b/test/orm/mapper.py index e6c03161c2..b0542cfbe8 100644 --- a/test/orm/mapper.py +++ b/test/orm/mapper.py @@ -55,13 +55,12 @@ class MapperTest(MapperSuperTest): assert not hasattr(u, 'user_name') def testrefresh(self): - mapper(User, users, properties={'addresses':relation(mapper(Address, addresses))}) + mapper(User, users, properties={'addresses':relation(mapper(Address, addresses), backref='user')}) s = create_session() u = s.get(User, 7) u.user_name = 'foo' a = Address() - import sqlalchemy.orm.session - assert sqlalchemy.orm.session.object_session(a) is None + assert object_session(a) is None u.addresses.append(a) self.assert_(a in u.addresses) @@ -87,6 +86,7 @@ class MapperTest(MapperSuperTest): # get the attribute, it refreshes self.assert_(u.user_name == 'jack') self.assert_(a not in u.addresses) + def testexpirecascade(self): mapper(User, users, properties={'addresses':relation(mapper(Address, addresses), cascade="all, refresh-expire")}) @@ -616,8 +616,8 @@ class MapperTest(MapperSuperTest): print "-------MARK----------" - # eagerload orders, orders.items, orders.items.keywords - q2 = sess.query(User).options(eagerload('orders'), eagerload('orders.items'), eagerload('orders.items.keywords')) + # eagerload orders.items.keywords; eagerload_all() implies eager load of orders, orders.items + q2 = sess.query(User).options(eagerload_all('orders.items.keywords')) u = q2.select() def go(): print u[0].orders[1].items[0].keywords[1] diff --git a/test/orm/query.py b/test/orm/query.py index 01815374da..7b809d74fb 100644 --- a/test/orm/query.py +++ b/test/orm/query.py @@ -73,7 +73,6 @@ class QueryTest(testbase.ORMTest): }) mapper(Keyword, keywords) - class GetTest(QueryTest): def test_get(self): s = create_session() @@ -85,6 +84,33 @@ class GetTest(QueryTest): u2 = s.query(User).get(7) assert u is not u2 + def test_load(self): + s = create_session() + + try: + assert s.query(User).load(19) is None + assert False + except exceptions.InvalidRequestError: + assert True + + u = s.query(User).load(7) + u2 = s.query(User).load(7) + assert u is u2 + s.clear() + u2 = s.query(User).load(7) + assert u is not u2 + + u2.name = 'some name' + a = Address(name='some other name') + u2.addresses.append(a) + assert u2 in s.dirty + assert a in u2.addresses + + s.query(User).load(7) + assert u2 not in s.dirty + assert u2.name =='jack' + assert a not in u2.addresses + def test_unicode(self): """test that Query.get properly sets up the type for the bind parameter. using unicode would normally fail on postgres, mysql and oracle unless it is converted to an encoded string""" @@ -99,6 +125,38 @@ class GetTest(QueryTest): mapper(LocalFoo, table) assert create_session().query(LocalFoo).get(ustring) == LocalFoo(id=ustring, data=ustring) + def test_populate_existing(self): + s = create_session() + + userlist = s.query(User).all() + + u = userlist[0] + u.name = 'foo' + a = Address(name='ed') + u.addresses.append(a) + + self.assert_(a in u.addresses) + + s.query(User).populate_existing().all() + + self.assert_(u not in s.dirty) + + self.assert_(u.name == 'jack') + + self.assert_(a not in u.addresses) + + u.addresses[0].email_address = 'lala' + u.orders[1].items[2].description = 'item 12' + # test that lazy load doesnt change child items + s.query(User).populate_existing().all() + assert u.addresses[0].email_address == 'lala' + assert u.orders[1].items[2].description == 'item 12' + + # eager load does + s.query(User).options(eagerload('addresses'), eagerload_all('orders.items')).populate_existing().all() + assert u.addresses[0].email_address == 'jack@bean.com' + assert u.orders[1].items[2].description == 'item 5' + class OperatorTest(QueryTest): """test sql.Comparator implementation for MapperProperties"""