From b2d1c5aa8737532bd6b1544a99da54dd44355e2f Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 14 Jan 2008 04:20:26 +0000 Subject: [PATCH] - query.join() can now accept class-mapped attributes as arguments, which can be used in place or in any combination with strings. In particular this allows construction of joins to subclasses on a polymorphic relation, i.e. query(Company).join(['employees', Engineer.name]), etc. --- CHANGES | 10 ++++++- lib/sqlalchemy/orm/query.py | 45 ++++++++++++++++++++++++-------- lib/sqlalchemy/sql/expression.py | 4 +-- test/orm/inheritance/query.py | 38 ++++++++++++++++++++++++--- 4 files changed, 80 insertions(+), 17 deletions(-) diff --git a/CHANGES b/CHANGES index 3f3f178aea..a940248185 100644 --- a/CHANGES +++ b/CHANGES @@ -18,7 +18,15 @@ CHANGES UPDATE operation are post-fetched immediately, instead of being deferred until later. This mimics the old 0.3 behavior. - + + - query.join() can now accept class-mapped attributes + as arguments, which can be used in place or in any + combination with strings. In particular this allows + construction of joins to subclasses on a polymorphic + relation, i.e. + query(Company).join(['employees', Engineer.name]), + etc. + - general improvements to the behavior of join() in conjunction with polymorphic mappers, i.e. joining from/to polymorphic mappers and properly applying diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index b3678f1aa0..bec6f1c3eb 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -24,6 +24,7 @@ from sqlalchemy.sql import expression, visitors, operators from sqlalchemy.orm import mapper, object_mapper from sqlalchemy.orm.mapper import _state_mapper from sqlalchemy.orm import util as mapperutil +from sqlalchemy.orm import interfaces __all__ = ['Query', 'QueryContext'] @@ -422,7 +423,11 @@ class Query(object): mapper = start alias = self._aliases for key in util.to_list(keys): - prop = mapper.get_property(key, resolve_synonyms=True) + if isinstance(key, interfaces.PropComparator): + prop = key.property + else: + prop = mapper.get_property(key, resolve_synonyms=True) + if prop._is_self_referential() and not create_aliases: raise exceptions.InvalidRequestError("Self-referential query on '%s' property requires create_aliases=True argument." % str(prop)) @@ -583,21 +588,39 @@ class Query(object): return q def join(self, prop, id=None, aliased=False, from_joinpoint=False): - """create a join of this ``Query`` object's criterion - to a relationship and return the newly resulting ``Query``. - - 'prop' may be a string property name or a list of string - property names. + """Create a join against this ``Query`` object's criterion + and apply generatively, retunring the newly resulting ``Query``. + + 'prop' may be one of: + * a string property name, i.e. "rooms" + * a class-mapped attribute, i.e. Houses.rooms + * a list containing a combination of any of the above. + + e.g.:: + + session.query(Company).join('employees') + session.query(Company).join(['employees', 'tasks']) + session.query(Houses).join([Colonials.rooms, Room.closets]) + """ return self._join(prop, id=id, outerjoin=False, aliased=aliased, from_joinpoint=from_joinpoint) def outerjoin(self, prop, id=None, aliased=False, from_joinpoint=False): - """create a left outer join of this ``Query`` object's criterion - to a relationship and return the newly resulting ``Query``. - - 'prop' may be a string property name or a list of string - property names. + """Create a left outer join against this ``Query`` object's criterion + and apply generatively, retunring the newly resulting ``Query``. + + 'prop' may be one of: + * a string property name, i.e. "rooms" + * a class-mapped attribute, i.e. Houses.rooms + * a list containing a combination of any of the above. + + e.g.:: + + session.query(Company).outerjoin('employees') + session.query(Company).outerjoin(['employees', 'tasks']) + session.query(Houses).outerjoin([Colonials.rooms, Room.closets]) + """ return self._join(prop, id=id, outerjoin=True, aliased=aliased, from_joinpoint=from_joinpoint) diff --git a/lib/sqlalchemy/sql/expression.py b/lib/sqlalchemy/sql/expression.py index 187b38a2c1..4eb555d4d4 100644 --- a/lib/sqlalchemy/sql/expression.py +++ b/lib/sqlalchemy/sql/expression.py @@ -26,7 +26,7 @@ to stay the same in future releases. """ import datetime, re -from itertools import chain +import itertools from sqlalchemy import util, exceptions from sqlalchemy.sql import operators, visitors from sqlalchemy import types as sqltypes @@ -2349,7 +2349,7 @@ class Join(FromClause): return self.select(use_labels=True, correlate=False).alias(name) def _hide_froms(self): - return chain(*[x.left._get_from_objects() + x.right._get_from_objects() for x in self._cloned_set]) + return itertools.chain(*[x.left._get_from_objects() + x.right._get_from_objects() for x in self._cloned_set]) _hide_froms = property(_hide_froms) def _get_from_objects(self, **modifiers): diff --git a/test/orm/inheritance/query.py b/test/orm/inheritance/query.py index 2a15ae1b01..5537f94a5d 100644 --- a/test/orm/inheritance/query.py +++ b/test/orm/inheritance/query.py @@ -22,6 +22,9 @@ class Manager(Person): class Boss(Manager): pass +class Machine(fixtures.Base): + pass + class Paperwork(fixtures.Base): pass @@ -30,7 +33,7 @@ class PolymorphicQueryTest(ORMTest): keep_mappers = True def define_tables(self, metadata): - global companies, people, engineers, managers, boss, paperwork + global companies, people, engineers, managers, boss, paperwork, machines companies = Table('companies', metadata, Column('company_id', Integer, Sequence('company_id_seq', optional=True), primary_key=True), @@ -48,7 +51,12 @@ class PolymorphicQueryTest(ORMTest): Column('engineer_name', String(50)), Column('primary_language', String(50)), ) - + + machines = Table('machines', metadata, + Column('machine_id', Integer, primary_key=True), + Column('name', String(50)), + Column('engineer_id', Integer, ForeignKey('engineers.person_id'))) + managers = Table('managers', metadata, Column('person_id', Integer, ForeignKey('people.person_id'), primary_key=True), Column('status', String(30)), @@ -82,12 +90,16 @@ class PolymorphicQueryTest(ORMTest): 'employees':relation(Person) }) + mapper(Machine, machines) + # testing a order_by here as well; the surrogate mapper has to adapt it mapper(Person, people, select_table=person_join, polymorphic_on=people.c.type, polymorphic_identity='person', order_by=people.c.person_id, properties={ 'paperwork':relation(Paperwork) }) - mapper(Engineer, engineers, inherits=Person, polymorphic_identity='engineer') + mapper(Engineer, engineers, inherits=Person, polymorphic_identity='engineer', properties={ + 'machines':relation(Machine) + }) mapper(Manager, managers, select_table=manager_join, inherits=Person, polymorphic_identity='manager') mapper(Boss, boss, inherits=Manager, polymorphic_identity='boss') mapper(Paperwork, paperwork) @@ -100,10 +112,15 @@ class PolymorphicQueryTest(ORMTest): e1 = Engineer(name="dilbert", engineer_name="dilbert", primary_language="java", status="regular engineer", paperwork=[ Paperwork(description="tps report #1"), Paperwork(description="tps report #2") + ], machines=[ + Machine(name='IBM ThinkPad'), + Machine(name='IPhone'), ]) e2 = Engineer(name="wally", engineer_name="wally", primary_language="c++", status="regular engineer", paperwork=[ Paperwork(description="tps report #3"), Paperwork(description="tps report #4") + ], machines=[ + Machine(name="Commodore 64") ]) b1 = Boss(name="pointy haired boss", golf_swing="fore", manager_name="pointy", status="da boss", paperwork=[ Paperwork(description="review #1"), @@ -116,7 +133,11 @@ class PolymorphicQueryTest(ORMTest): e3 = Engineer(name="vlad", engineer_name="vlad", primary_language="cobol", status="elbonian engineer", paperwork=[ Paperwork(description='elbonian missive #3') + ], machines=[ + Machine(name="Commodore 64"), + Machine(name="IBM 3270") ]) + c2.employees = [e3] sess = create_session() sess.save(c1) @@ -160,6 +181,17 @@ class PolymorphicQueryTest(ORMTest): self.assertEquals(sess.query(Company).join('employees', aliased=True).filter(Person.name=='vlad').one(), c2) + def test_join_to_subclass(self): + sess = create_session() + + self.assertEquals(sess.query(Person).join(Engineer.machines).all(), [e1, e2, e3]) + + self.assertEquals(sess.query(Person).join(Engineer.machines).filter(Machine.name.ilike("%ibm%")).all(), [e1, e3]) + + self.assertEquals(sess.query(Company).join(['employees', Engineer.machines]).all(), [c1, c2]) + + self.assertEquals(sess.query(Company).join(['employees', Engineer.machines]).filter(Machine.name.ilike("%thinkpad%")).all(), [c1]) + def test_join_through_polymorphic(self): sess = create_session() -- 2.47.3