From ca92a81191a8ac4627e16838c8f6c1a8300291dc Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 15 Jan 2007 21:54:16 +0000 Subject: [PATCH] - added optional constructor to sql.ColumnCollection - mapper sets its "primary_key" attribute to be the ultimately decided primary_key column collection post-compilation - added compare() method to MapperProperty, defines a comparison operation of the columns represented by the property to some value - all the above combines into today's controversial feature: saying query.select_by(somerelationname=someinstance) will create the join of the primary key columns represented by "somerelationname"'s mapper to the actual primary key in "someinstance". - docs for the above --- CHANGES | 3 +++ doc/build/content/datamapping.txt | 12 ++++++++++++ lib/sqlalchemy/orm/interfaces.py | 4 ++++ lib/sqlalchemy/orm/mapper.py | 4 ++-- lib/sqlalchemy/orm/properties.py | 7 ++++++- lib/sqlalchemy/orm/query.py | 2 +- lib/sqlalchemy/sql.py | 3 +++ test/orm/mapper.py | 15 ++++++++++++++- 8 files changed, 45 insertions(+), 5 deletions(-) diff --git a/CHANGES b/CHANGES index 892d28e26a..97710b56cf 100644 --- a/CHANGES +++ b/CHANGES @@ -42,6 +42,9 @@ - Firebird fix to autoload multifield foreign keys [ticket:409] - Firebird NUMERIC type properly handles a type without precision [ticket:409] - orm: + - poked the first hole in the can of worms: saying query.select_by(somerelationname=someinstance) + will create the join of the primary key columns represented by "somerelationname"'s mapper to the + actual primary key in "someinstance". - added a mutex to the mapper compilation step. ive been reluctant to add any kind of threading anything to SA but this is one spot that its its really needed since mappers are typically "global", and while their state does not change during normal operation, the diff --git a/doc/build/content/datamapping.txt b/doc/build/content/datamapping.txt index 428c72d750..68a17bea90 100644 --- a/doc/build/content/datamapping.txt +++ b/doc/build/content/datamapping.txt @@ -474,6 +474,18 @@ All keyword arguments sent to `select_by` are used to create query criterion. T Note that the `select_by` method, while it primarily uses keyword arguments, also can accomodate `ClauseElement` objects positionally; recall that a `ClauseElement` is genearated when producing a comparison off of a `Column` expression, such as `users.c.name=='ed'`. When using `ClauseElements` with `select_by`, these clauses are passed directly to the generated SQL and are **not** used to further locate join criterion. If criterion is being constructed with these kinds of expressions, consider using the `select()` method which is better designed to accomodate these expressions. +As of SA 0.3.4, `select_by()` and related functions can compare not only column-based attributes to column-based values, but also relations to object instances: + + {python} + # get an instance of Address + someaddress = session.query(Address).get_by(street='123 Green Street') + + # look for User instances which have the + # "someaddress" instance in their "addresses" collection + l = session.query(User).select_by(addresses=someaddress) + +Where above, the comparison denoted by `addresses=someaddress` is constructed by comparing all the primary key columns in the `Address` mapper to each corresponding primary key value in the `someaddress` entity. In other words, its equivalent to saying `select_by(address_id=someaddress.address_id)` ([Alpha API][alpha_api]). + #### Generating Join Criterion Using join\_to, join\_via {@name=jointo} Feature Status: [Alpha API][alpha_api] diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py index 4e9fe55f43..834f18e281 100644 --- a/lib/sqlalchemy/orm/interfaces.py +++ b/lib/sqlalchemy/orm/interfaces.py @@ -58,6 +58,10 @@ class MapperProperty(object): def merge(self, session, source, dest): """merges the attribute represented by this MapperProperty from source to destination object""" raise NotImplementedError() + def compare(self, value): + """returns a compare operation for the columns represented by this MapperProperty to the given value, + which may be a column value or an instance.""" + raise NotImplementedError() class StrategizedProperty(MapperProperty): """a MapperProperty which uses selectable strategies to affect loading behavior. diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 3b1bb1dad9..872f3d1212 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -441,7 +441,7 @@ class Mapper(object): if len(self.pks_by_table[self.mapped_table]) == 0: raise exceptions.ArgumentError("Could not assemble any primary key columns for mapped table '%s'" % (self.mapped_table.name)) - + self.primary_key = self.pks_by_table[self.mapped_table] def _compile_properties(self): """inspects the properties dictionary sent to the Mapper's constructor as well as the mapped_table, and creates @@ -799,7 +799,7 @@ class Mapper(object): prop = self._getpropbycolumn(column, raiseerror) if prop is None: return NO_ATTRIBUTE - #self.__log_debug("get column attribute '%s' from instance %s" % (column.key, mapperutil.instance_str(obj))) + #print "get column attribute '%s' from instance %s" % (column.key, mapperutil.instance_str(obj)) return prop.getattr(obj) def set_attr_by_column(self, obj, column, value): diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index 980424c4e7..fc945fc723 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -57,13 +57,15 @@ class ColumnProperty(StrategizedProperty): else: return strategies.ColumnLoader(self) def getattr(self, object): - return getattr(object, self.key, None) + return getattr(object, self.key) def setattr(self, object, value): setattr(object, self.key, value) def get_history(self, obj, passive=False): return sessionlib.attribute_manager.get_history(obj, self.key, passive=passive) def merge(self, session, source, dest): setattr(dest, self.key, getattr(source, self.key, None)) + def compare(self, value): + return self.columns[0] == value ColumnProperty.logger = logging.class_logger(ColumnProperty) @@ -109,6 +111,9 @@ class PropertyLoader(StrategizedProperty): else: self.backref = backref self.is_backref = is_backref + + def compare(self, value): + return sql.and_(*[x==y for (x, y) in zip(self.mapper.primary_key, self.mapper.primary_key_from_instance(value))]) private = property(lambda s:s.cascade.delete_orphan) diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index da2da61d89..5d82b594f0 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -126,7 +126,7 @@ class Query(object): for key, value in params.iteritems(): (keys, prop) = self._locate_prop(key) - c = (prop.columns[0]==value) & self.join_via(keys) + c = prop.compare(value) & self.join_via(keys) if clause is None: clause = c else: diff --git a/lib/sqlalchemy/sql.py b/lib/sqlalchemy/sql.py index f6e23d583a..323173cc56 100644 --- a/lib/sqlalchemy/sql.py +++ b/lib/sqlalchemy/sql.py @@ -676,6 +676,9 @@ class ColumnCollection(util.OrderedProperties): overrides the __eq__() method to produce SQL clauses between sets of correlated columns.""" + def __init__(self, *cols): + super(ColumnCollection, self).__init__() + [self.add(c) for c in cols] def add(self, column): """add a column to this collection. diff --git a/test/orm/mapper.py b/test/orm/mapper.py index f8dc7e7a44..519d236401 100644 --- a/test/orm/mapper.py +++ b/test/orm/mapper.py @@ -285,7 +285,8 @@ class MapperTest(MapperSuperTest): })) }) - q = create_session().query(m) + sess = create_session() + q = sess.query(m) l = q.select((orderitems.c.item_name=='item 4') & q.join_via(['orders', 'items'])) self.assert_result(l, User, user_result[0]) @@ -298,7 +299,19 @@ class MapperTest(MapperSuperTest): l = q.select((orderitems.c.item_name=='item 4') & q.join_to('items')) self.assert_result(l, User, user_result[0]) + + # test comparing to an object instance + item = sess.query(Item).get_by(item_name='item 4') + l = q.select_by(items=item) + self.assert_result(l, User, user_result[0]) + try: + # this should raise AttributeError + l = q.select_by(items=5) + assert False + except AttributeError: + assert True + def testcustomjoin(self): """test that the from_obj parameter to query.select() can be used to totally replace the FROM parameters of the generated query.""" -- 2.47.2