]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- implemented foreign_keys argument on relation() [ticket:385]
authorMike Bayer <mike_mp@zzzcomputing.com>
Sat, 10 Feb 2007 23:39:06 +0000 (23:39 +0000)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sat, 10 Feb 2007 23:39:06 +0000 (23:39 +0000)
- PropertyLoader figures out accurate remote_side collection based
on foreign_keys, legacy foreignkey, primary/secondaryjoin/polymorphic
- reworked lazyloader, sync to work straight off foreign_keys/
remote_side collections

CHANGES
lib/sqlalchemy/ext/activemapper.py
lib/sqlalchemy/orm/dependency.py
lib/sqlalchemy/orm/properties.py
lib/sqlalchemy/orm/strategies.py
lib/sqlalchemy/orm/sync.py
lib/sqlalchemy/sql.py
test/orm/inheritance5.py
test/orm/unitofwork.py

diff --git a/CHANGES b/CHANGES
index 95c22fc55b3451b19a17bb6ddbe8bb05f15a7aa8..6f19f3dd0c49b479ac4b6e9e8b8dba5bc52c54d2 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -9,11 +9,13 @@
   kinds of clauses can express if they are appropriate for executing...such as, 
   you can execute a "select", but not a "Table" or a "Join".
 - orm:
-  - further rework of the recent polymorphic relationship refactorings, as well
-  as the mechanics of relationships overall.  Allows more accurate ORM behavior
-  with relationships from/to/between polymorphic mappers, as well as their usage
-  with Query, SelectResults.  tickets include [ticket:439], [ticket:441], [ticket:448].
-  relationship mechanics are still a work in progress, more to come !
+  - another refactoring to relationship calculation.  Allows more accurate ORM behavior
+  with relationships from/to/between mappers, particularly polymorphic mappers,
+  also their usage with Query, SelectResults.  
+  tickets include [ticket:439], [ticket:441], [ticket:448].
+  - implemented foreign_keys argument to mapper [ticket:385].  use in conjunction with
+  primaryjoin/secondaryjoin arguments to specify/override foreign keys defined on the
+  Table instance.
   - eager relation to an inheriting mapper wont fail if no rows returned for
   the relationship.
   - eager loading is slightly more strict about detecting "self-referential"
index 674cc9265408efa3cdae5aa450474ef98d9432f4..bcfbac63291578c0f103117123e8100f56678296 100644 (file)
@@ -93,7 +93,7 @@ class relationship(object):
         else:
             br_fkey = None
         
-        return create_backref(self.backref, foreignkey=br_fkey)
+        return create_backref(self.backref, remote_side=br_fkey)
 
 
 class one_to_many(relationship):
index 9887025a6c12c15eba7384212b0d2124191b3af4..86bb503fddf8bbee26f804bf8b48987b0b88012d 100644 (file)
@@ -35,7 +35,7 @@ class DependencyProcessor(object):
         self.direction = prop.direction
         self.is_backref = prop.is_backref
         self.post_update = prop.post_update
-        self.foreignkey = prop.foreignkey
+        self.foreign_keys = prop.foreign_keys
         self.passive_deletes = prop.passive_deletes
         self.key = prop.key
 
@@ -88,10 +88,10 @@ class DependencyProcessor(object):
         objects are processed."""
         self.syncrules = sync.ClauseSynchronizer(self.parent, self.mapper, self.direction)
         if self.direction == sync.MANYTOMANY:
-            self.syncrules.compile(self.prop.primaryjoin, issecondary=False)
-            self.syncrules.compile(self.prop.secondaryjoin, issecondary=True)
+            self.syncrules.compile(self.prop.primaryjoin, issecondary=False, foreign_keys=self.foreign_keys)
+            self.syncrules.compile(self.prop.secondaryjoin, issecondary=True, foreign_keys=self.foreign_keys)
         else:
-            self.syncrules.compile(self.prop.primaryjoin, foreignkey=self.foreignkey)
+            self.syncrules.compile(self.prop.primaryjoin, foreign_keys=self.foreign_keys)
         
     def get_object_dependencies(self, obj, uowcommit, passive = True):
         """returns the list of objects that are dependent on the given object, as according to the relationship
index 5e8fab807e5073df921d69d1870a6018224d648a..2d18ccaa2a7ab9ba34c43ccc5c3bcf81c2fd479f 100644 (file)
@@ -73,7 +73,7 @@ mapper.ColumnProperty = ColumnProperty
 class PropertyLoader(StrategizedProperty):
     """describes an object property that holds a single item or list of items that correspond
     to a related database table."""
-    def __init__(self, argument, secondary, primaryjoin, secondaryjoin, foreignkey=None, uselist=None, private=False, association=None, order_by=False, attributeext=None, backref=None, is_backref=False, post_update=False, cascade=None, viewonly=False, lazy=True, collection_class=None, passive_deletes=False, remote_side=None):
+    def __init__(self, argument, secondary, primaryjoin, secondaryjoin, foreign_keys=None, foreignkey=None, uselist=None, private=False, association=None, order_by=False, attributeext=None, backref=None, is_backref=False, post_update=False, cascade=None, viewonly=False, lazy=True, collection_class=None, passive_deletes=False, remote_side=None):
         self.uselist = uselist
         self.argument = argument
         self.secondary = secondary
@@ -83,7 +83,8 @@ class PropertyLoader(StrategizedProperty):
         self.direction = None
         self.viewonly = viewonly
         self.lazy = lazy
-        self.foreignkey = util.to_set(foreignkey)
+        self.foreign_keys = util.to_set(foreign_keys)
+        self._legacy_foreignkey = util.to_set(foreignkey)
         self.collection_class = collection_class
         self.passive_deletes = passive_deletes
         self.remote_side = util.to_set(remote_side)
@@ -126,7 +127,7 @@ class PropertyLoader(StrategizedProperty):
             return strategies.NoLoader(self)
             
     def __str__(self):
-        return self.__class__.__name__ + " " + str(self.parent) + "->" + self.key + "->" + str(self.mapper)
+        return str(self.parent.class_.__name__) + "." + self.key + " (" + str(self.mapper.class_.__name__)  + ")"
 
     def merge(self, session, source, dest, _recursive):
         if not "merge" in self.cascade or source in _recursive:
@@ -184,6 +185,15 @@ class PropertyLoader(StrategizedProperty):
             return self.argument.class_
         
     def do_init(self):
+        self._determine_targets()
+        self._determine_joins()
+        self._determine_fks()
+        self._determine_direction()
+        self._determine_remote_side()
+        self._create_polymorphic_joins()
+        self._post_init()
+        
+    def _determine_targets(self):
         if isinstance(self.argument, type):
             self.mapper = mapper.class_mapper(self.argument, compile=False)._check_compile()
         elif isinstance(self.argument, mapper.Mapper):
@@ -205,9 +215,10 @@ class PropertyLoader(StrategizedProperty):
 
         if self.cascade.delete_orphan:
             if self.parent.class_ is self.mapper.class_:
-                raise exceptions.ArgumentError("Cant establish 'delete-orphan' cascade rule on a self-referential relationship (attribute '%s' on class '%s').  You probably want cascade='all', which includes delete cascading but not orphan detection." %(self.key, self.parent.class_.__name__))
+                raise exceptions.ArgumentError("In relationship '%s', can't establish 'delete-orphan' cascade rule on a self-referential relationship.  You probably want cascade='all', which includes delete cascading but not orphan detection." %(str(self)))
             self.mapper.primary_mapper().delete_orphans.append((self.key, self.parent.class_))
-            
+    
+    def _determine_joins(self):
         if self.secondaryjoin is not None and self.secondary is None:
             raise exceptions.ArgumentError("Property '" + self.key + "' specified with secondary join condition but no secondary argument")
         # if join conditions were not specified, figure them out based on foreign keys
@@ -221,7 +232,7 @@ class PropertyLoader(StrategizedProperty):
                 if self.primaryjoin is None:
                     self.primaryjoin = sql.join(self.parent.unjoined_table, self.target).onclause
         except exceptions.ArgumentError, e:
-            raise exceptions.ArgumentError("Error determining primary and/or secondary join for relationship '%s' between mappers '%s' and '%s'.  If the underlying error cannot be corrected, you should specify the 'primaryjoin' (and 'secondaryjoin', if there is an association table present) keyword arguments to the relation() function (or for backrefs, by specifying the backref using the backref() function with keyword arguments) to explicitly specify the join conditions.  Nested error is \"%s\"" % (self.key, self.parent, self.mapper, str(e)))
+            raise exceptions.ArgumentError("Error determining primary and/or secondary join for relationship '%s'.  If the underlying error cannot be corrected, you should specify the 'primaryjoin' (and 'secondaryjoin', if there is an association table present) keyword arguments to the relation() function (or for backrefs, by specifying the backref using the backref() function with keyword arguments) to explicitly specify the join conditions.  Nested error is \"%s\"" % (str(self), str(e)))
 
         # if using polymorphic mapping, the join conditions must be agasint the base tables of the mappers,
         # as the loader strategies expect to be working with those now (they will adapt the join conditions
@@ -233,25 +244,93 @@ class PropertyLoader(StrategizedProperty):
             if self.secondaryjoin:
                 self.secondaryjoin.accept_visitor(vis)
             if vis.result:
-                raise exceptions.ArgumentError("In relationship '%s' between mappers '%s' and '%s', primary and secondary join conditions must not include columns from the polymorphic 'select_table' argument as of SA release 0.3.4.  Construct join conditions using the base tables of the related mappers." % (self.key, self.parent, self.mapper))
+                raise exceptions.ArgumentError("In relationship '%s', primary and secondary join conditions must not include columns from the polymorphic 'select_table' argument as of SA release 0.3.4.  Construct join conditions using the base tables of the related mappers." % (str(self)))
+
+    def _determine_fks(self):
+        if len(self._legacy_foreignkey) and not self._is_self_referential():
+            self.foreign_keys = self._legacy_foreignkey
+        if len(self.foreign_keys):
+            self._opposite_side = util.Set()
+            def visit_binary(binary):
+                if binary.operator != '=' or not isinstance(binary.left, schema.Column) or not isinstance(binary.right, schema.Column):
+                    return
+                if binary.left in self.foreign_keys:
+                    self._opposite_side.add(binary.right)
+                if binary.right in self.foreign_keys:
+                    self._opposite_side.add(binary.left)
+            self.primaryjoin.accept_visitor(mapperutil.BinaryVisitor(visit_binary))
+            if self.secondaryjoin is not None:
+                self.secondaryjoin.accept_visitor(mapperutil.BinaryVisitor(visit_binary))
+        else:
+            self.foreign_keys = util.Set()
+            self._opposite_side = util.Set()
+            def visit_binary(binary):
+                if binary.operator != '=' or not isinstance(binary.left, schema.Column) or not isinstance(binary.right, schema.Column):
+                    return
+                for f in binary.left.foreign_keys:
+                    if f.references(binary.right.table):
+                        self.foreign_keys.add(binary.left)
+                        self._opposite_side.add(binary.right)
+                for f in binary.right.foreign_keys:
+                    if f.references(binary.left.table):
+                        self.foreign_keys.add(binary.right)
+                        self._opposite_side.add(binary.left)
+            self.primaryjoin.accept_visitor(mapperutil.BinaryVisitor(visit_binary))
 
+            if len(self.foreign_keys) == 0:
+                raise exceptions.ArgumentError("Cant locate any foreign key columns in primary join condition '%s' for relationship '%s'.  Specify 'foreign_keys' argument to indicate which columns in the join condition are foreign." %(str(self.primaryjoin), str(self)))
+            if self.secondaryjoin is not None:
+                self.secondaryjoin.accept_visitor(mapperutil.BinaryVisitor(visit_binary))
             
-        # if the foreign key wasnt specified and theres no assocaition table, try to figure
-        # out who is dependent on who. we dont need all the foreign keys represented in the join,
-        # just one of them.
-        if not len(self.foreignkey) and self.secondaryjoin is None:
-            # else we usually will have a one-to-many where the secondary depends on the primary
-            # but its possible that its reversed
-            self._find_dependent()
+    def _determine_direction(self):
+        """determines our 'direction', i.e. do we represent one to many, many to many, etc."""
+        if self.secondaryjoin is not None:
+            self.direction = sync.MANYTOMANY
+        elif self._is_self_referential():
+            # for a self referential mapper, if the "foreignkey" is a single or composite primary key,
+            # then we are "many to one", since the remote site of the relationship identifies a singular entity.
+            # otherwise we are "one to many".
+            if len(self._legacy_foreignkey):
+                for f in self._legacy_foreignkey:
+                    if not f.primary_key:
+                        self.direction = sync.ONETOMANY
+                    else:
+                        self.direction = sync.MANYTOONE
+                
+            elif len(self.remote_side):
+                for f in self.foreign_keys:
+                    if f in self.remote_side:
+                        self.direction = sync.ONETOMANY
+                        return
+                else:
+                    self.direction = sync.MANYTOONE
+            else:
+                self.direction = sync.ONETOMANY
+        else:
+            onetomany = len([c for c in self.foreign_keys if self.mapper.unjoined_table.corresponding_column(c, False) is not None])
+            manytoone = len([c for c in self.foreign_keys if self.parent.unjoined_table.corresponding_column(c, False) is not None])
+            if not onetomany and not manytoone:
+                raise exceptions.ArgumentError("Cant determine relation direction for relationship '%s' - foreign key columns are not present in neither the parent nor the child's mapped tables" %(str(self)))
+            elif onetomany and manytoone:
+                raise exceptions.ArgumentError("Cant determine relation direction for relationship '%s' - foreign key columns are present in both the parent and the child's mapped tables.  Specify 'foreign_keys' argument." %(str(self)))
+            elif onetomany:
+                self.direction = sync.ONETOMANY
+            elif manytoone:
+                self.direction = sync.MANYTOONE
 
-        # if we are re-initializing, as in a copy made for an inheriting 
-        # mapper, dont re-evaluate the direction.
-        if self.direction is None:
-            self.direction = self._get_direction()
-        
-        #print "DIRECTION IS ", self.direction, sync.ONETOMANY, sync.MANYTOONE
-        #print "FKEY IS", self.foreignkey
+    def _determine_remote_side(self):
+        if len(self.remote_side):
+            return
+        self.remote_side = util.Set()
+            
+        if self.direction is sync.MANYTOONE:
+            for c in self._opposite_side:
+                self.remote_side.add(c)
+        elif self.direction is sync.ONETOMANY or self.direction is sync.MANYTOMANY:
+            for c in self.foreign_keys:
+                self.remote_side.add(c)
 
+    def _create_polymorphic_joins(self):        
         # get ready to create "polymorphic" primary/secondary join clauses.
         # these clauses represent the same join between parent/child tables that the primary
         # and secondary join clauses represent, except they reference ColumnElements that are specifically
@@ -272,21 +351,27 @@ class PropertyLoader(StrategizedProperty):
             else:
                 self.polymorphic_primaryjoin = self.primaryjoin.copy_container()
                 if self.direction is sync.ONETOMANY:
-                    self.polymorphic_primaryjoin.accept_visitor(sql_util.ClauseAdapter(self.mapper.select_table, include=self.foreignkey, equivalents=target_equivalents))
+                    self.polymorphic_primaryjoin.accept_visitor(sql_util.ClauseAdapter(self.mapper.select_table, include=self.foreign_keys, equivalents=target_equivalents))
                 elif self.direction is sync.MANYTOONE:
-                    self.polymorphic_primaryjoin.accept_visitor(sql_util.ClauseAdapter(self.mapper.select_table, exclude=self.foreignkey, equivalents=target_equivalents))
-                    
+                    self.polymorphic_primaryjoin.accept_visitor(sql_util.ClauseAdapter(self.mapper.select_table, exclude=self.foreign_keys, equivalents=target_equivalents))
                 self.polymorphic_secondaryjoin = None
+            for c in list(self.remote_side):
+                corr = self.mapper.select_table.corresponding_column(c, raiseerr=False)
+                if corr:
+                    self.remote_side.add(corr)
         else:
             self.polymorphic_primaryjoin = self.primaryjoin.copy_container()
             self.polymorphic_secondaryjoin = self.secondaryjoin and self.secondaryjoin.copy_container() or None
 
-            
-        #print "KEY", self.key, "PARENT", str(self.parent)
-        #print "KEY", self.key, "REG PRIMARY JOIN", str(self.primaryjoin)
-        #print "KEY", self.key, "POLY PRIMARY JOIN", str(self.polymorphic_primaryjoin)
-                
-        if self.uselist is None and self.direction == sync.MANYTOONE:
+    def _post_init(self):
+        if logging.is_info_enabled(self.logger):
+            self.logger.info(str(self) + " setup primary join " + str(self.primaryjoin))
+            self.logger.info(str(self) + " setup polymorphic primary join " + str(self.polymorphic_primaryjoin))
+            self.logger.info(str(self) + " foreign keys " + str([c.key for c in self.foreign_keys]))
+            self.logger.info(str(self) + " remote columns " + str([c.key for c in self.remote_side]))
+            self.logger.info(str(self) + " relation direction " + (self.direction is sync.ONETOMANY and "one-to-many" or (self.direction is sync.MANYTOONE and "many-to-one" or "many-to-many")))
+        
+        if self.uselist is None and self.direction is sync.MANYTOONE:
             self.uselist = False
 
         if self.uselist is None:
@@ -312,58 +397,6 @@ class PropertyLoader(StrategizedProperty):
     def _is_self_referential(self):
         return self.parent.mapped_table is self.target or self.parent.select_table is self.target
         
-    def _get_direction(self):
-        """determines our 'direction', i.e. do we represent one to many, many to many, etc."""
-        if self.secondaryjoin is not None:
-            return sync.MANYTOMANY
-        elif self._is_self_referential():
-            # for a self referential mapper, if the "foreignkey" is a single or composite primary key,
-            # then we are "many to one", since the remote site of the relationship identifies a singular entity.
-            # otherwise we are "one to many".
-            if self.remote_side is not None and len(self.remote_side):
-                for f in self.foreignkey:
-                    if f in self.remote_side:
-                        return sync.ONETOMANY
-                else:
-                    return sync.MANYTOONE
-            else:
-                for f in self.foreignkey:
-                    if not f.primary_key:
-                        return sync.ONETOMANY
-                else:
-                    return sync.MANYTOONE
-        else:
-            onetomany = len([c for c in self.foreignkey if self.mapper.unjoined_table.corresponding_column(c, False) is not None])
-            manytoone = len([c for c in self.foreignkey if self.parent.unjoined_table.corresponding_column(c, False) is not None])
-            if not onetomany and not manytoone:
-                raise exceptions.ArgumentError("Cant determine relation direction for '%s' on mapper '%s' with primary join '%s' - foreign key columns are not present in neither the parent nor the child's mapped tables" %(self.key, str(self.parent), str(self.primaryjoin)) +  str(self.foreignkey))
-            elif onetomany and manytoone:
-                raise exceptions.ArgumentError("Cant determine relation direction for '%s' on mapper '%s' with primary join '%s' - foreign key columns are present in both the parent and the child's mapped tables.  Specify 'foreignkey' argument." %(self.key, str(self.parent), str(self.primaryjoin)))
-            elif onetomany:
-                return sync.ONETOMANY
-            elif manytoone:
-                return sync.MANYTOONE
-            
-    def _find_dependent(self):
-        """searches through the primary join condition to determine which side
-        has the foreign key - from this we return 
-        the "foreign key" for this property which helps determine one-to-many/many-to-one."""
-        foreignkeys = util.Set()
-        def foo(binary):
-            if binary.operator != '=' or not isinstance(binary.left, schema.Column) or not isinstance(binary.right, schema.Column):
-                return
-            for f in binary.left.foreign_keys:
-                if f.references(binary.right.table):
-                    foreignkeys.add(binary.left)
-            for f in binary.right.foreign_keys:
-                if f.references(binary.left.table):
-                    foreignkeys.add(binary.right)
-        visitor = mapperutil.BinaryVisitor(foo)
-        self.primaryjoin.accept_visitor(visitor)
-        if len(foreignkeys) == 0:
-            raise exceptions.ArgumentError("Cant determine relation direction for '%s' on mapper '%s' with primary join '%s' - no foreign key relationship is expressed within the join condition.  Specify 'foreignkey' argument." %(self.key, str(self.parent), str(self.primaryjoin)))
-        self.foreignkey = foreignkeys
-        
     def get_join(self, parent):
         try:
             return self._parent_join_cache[parent]
@@ -375,11 +408,11 @@ class PropertyLoader(StrategizedProperty):
             else:
                 secondaryjoin = None
             if self.direction is sync.ONETOMANY:
-                primaryjoin.accept_visitor(sql_util.ClauseAdapter(parent.select_table, exclude=self.foreignkey, equivalents=parent_equivalents))
+                primaryjoin.accept_visitor(sql_util.ClauseAdapter(parent.select_table, exclude=self.foreign_keys, equivalents=parent_equivalents))
             elif self.direction is sync.MANYTOONE:
-                primaryjoin.accept_visitor(sql_util.ClauseAdapter(parent.select_table, include=self.foreignkey, equivalents=parent_equivalents))
+                primaryjoin.accept_visitor(sql_util.ClauseAdapter(parent.select_table, include=self.foreign_keys, equivalents=parent_equivalents))
             elif self.secondaryjoin:
-                primaryjoin.accept_visitor(sql_util.ClauseAdapter(parent.select_table, exclude=self.foreignkey, equivalents=parent_equivalents))
+                primaryjoin.accept_visitor(sql_util.ClauseAdapter(parent.select_table, exclude=self.foreign_keys, equivalents=parent_equivalents))
 
             if secondaryjoin is not None:
                 j = primaryjoin & secondaryjoin
index 19868ba132f198a5ddfce391685359e2a3ced607..390b83867b2fb43459eb999158b90f37989c3b2c 100644 (file)
@@ -129,7 +129,7 @@ class DeferredOption(StrategizedOption):
 class AbstractRelationLoader(LoaderStrategy):
     def init(self):
         super(AbstractRelationLoader, self).init()
-        for attr in ['primaryjoin', 'secondaryjoin', 'secondary', 'foreignkey', 'mapper', 'select_mapper', 'target', 'select_table', 'loads_polymorphic', 'uselist', 'cascade', 'attributeext', 'order_by', 'remote_side', 'polymorphic_primaryjoin', 'polymorphic_secondaryjoin', 'direction']:
+        for attr in ['primaryjoin', 'secondaryjoin', 'secondary', 'foreign_keys', 'mapper', 'select_mapper', 'target', 'select_table', 'loads_polymorphic', 'uselist', 'cascade', 'attributeext', 'order_by', 'remote_side', 'polymorphic_primaryjoin', 'polymorphic_secondaryjoin', 'direction']:
             setattr(self, attr, getattr(self.parent_property, attr))
         self._should_log_debug = logging.is_debug_enabled(self.logger)
         
@@ -155,13 +155,7 @@ NoLoader.logger = logging.class_logger(NoLoader)
 class LazyLoader(AbstractRelationLoader):
     def init(self):
         super(LazyLoader, self).init()
-        (self.lazywhere, self.lazybinds, self.lazyreverse) = self._create_lazy_clause(
-            self.parent.mapped_table, 
-            self.mapper.select_table,
-            self.polymorphic_primaryjoin, 
-            self.polymorphic_secondaryjoin, 
-            self.foreignkey, 
-            self.remote_side)
+        (self.lazywhere, self.lazybinds, self.lazyreverse) = self._create_lazy_clause(self.polymorphic_primaryjoin, self.polymorphic_secondaryjoin, self.remote_side)
 
         # determine if our "lazywhere" clause is the same as the mapper's
         # get() clause.  then we can just use mapper.get()
@@ -246,53 +240,13 @@ class LazyLoader(AbstractRelationLoader):
                 # to load data into it.
                 sessionlib.attribute_manager.reset_instance_attribute(instance, self.key)
 
-    def _create_lazy_clause(self, parenttable, targettable, primaryjoin, secondaryjoin, foreignkey, remote_side):
+    def _create_lazy_clause(self, primaryjoin, secondaryjoin, remote_side):
         binds = {}
         reverse = {}
 
-        #print "PARENTTABLE", parenttable, "TARGETTABLE", targettable, "PJ", primaryjoin
-
         def should_bind(targetcol, othercol):
-            # determine if the given target column is part of the parent table
-            # portion of the join condition, in which case it gets converted
-            # to a bind param.
-            
-            # contains_column will return if this column is exactly in the table, with no
-            # proxying relationships.  the table can be either the column's actual parent table,
-            # or a Join object containing the table.  for a Select, Alias, or Union, the column
-            # needs to be the actual ColumnElement exported by that selectable, not the "originating" column.
-            inparent = parenttable.c.contains_column(targetcol)
-            
-            # check if its also in the target table.  if this is a many-to-many relationship, 
-            # then we dont care about target table presence
-            intarget = secondaryjoin is None and targettable.c.contains_column(targetcol)
-            
-            if inparent and not intarget:
-                # its in the parent and not the target, return true.
-                return True
-            elif inparent and intarget:
-                # its in both.  hmm.
-                if parenttable is not targettable:
-                    # the column is in both tables, but the two tables are different.  
-                    # this corresponds to a table relating to a Join which also contains that table.
-                    # such as tableA.c.col1 == tableB.c.col2, tables are tableA and tableA.join(tableB)
-                    # in which case we only accept that the parenttable is the "base" table, not the "joined" table
-                    return targetcol.table is parenttable
-                else:
-                    # parent/target are the same table, i.e. circular reference.
-                    # we have to rely on the "remote_side" argument
-                    # and/or foreignkey collection.
-                    # technically we can use this for the non-circular refs as well except that "remote_side" is usually
-                    # only calculated for self-referential relationships at the moment.
-                    # TODO: have PropertyLoader calculate remote_side completely ?  this would involve moving most of the
-                    # "should_bind()" logic to PropertyLoader.  remote_side could also then be accurately used by sync.py.
-                    if col_in_collection(othercol, remote_side):
-                        return True
-            return False
-
-        if remote_side is None or len(remote_side) == 0:
-            remote_side = foreignkey
-            
+            return othercol in remote_side
+
         def find_column_in_expr(expr):
             if not isinstance(expr, sql.ColumnElement):
                 return None
@@ -324,7 +278,7 @@ class LazyLoader(AbstractRelationLoader):
                 reverse[rightcol] = binds[col]
 
             # the "left is not right" compare is to handle part of a join clause that is "table.c.col1==table.c.col1",
-            # which can happen in rare cases
+            # which can happen in rare cases (test/orm/relationships.py RelationTest2)
             if leftcol is not rightcol and should_bind(rightcol, leftcol):
                 col = rightcol
                 binary.right = binds.setdefault(rightcol,
@@ -339,8 +293,7 @@ class LazyLoader(AbstractRelationLoader):
             secondaryjoin = secondaryjoin.copy_container()
             lazywhere = sql.and_(lazywhere, secondaryjoin)
  
-        #print "LAZYCLAUSE", str(lazywhere)
-        LazyLoader.logger.info("create_lazy_clause " + str(lazywhere))
+        LazyLoader.logger.info(str(self.parent_property) + " lazy loading clause " + str(lazywhere))
         return (lazywhere, binds, reverse)
 
 LazyLoader.logger = logging.class_logger(LazyLoader)
index f0a56687f158b893b7a36d29e76decd60921d7f0..8d8ee4179378a02a626a9e19da8890f4e5cc2d58 100644 (file)
@@ -31,7 +31,7 @@ class ClauseSynchronizer(object):
         self.direction = direction
         self.syncrules = []
 
-    def compile(self, sqlclause, issecondary=None, foreignkey=None):
+    def compile(self, sqlclause, foreign_keys=None, issecondary=None):
         def compile_binary(binary):
             """assemble a SyncRule given a single binary condition"""
             if binary.operator != '=' or not isinstance(binary.left, schema.Column) or not isinstance(binary.right, schema.Column):
@@ -39,43 +39,24 @@ class ClauseSynchronizer(object):
 
             source_column = None
             dest_column = None
-            if foreignkey is not None:
-                # for self-referential relationships,
-                # the best we can do right now is figure out which side
-                # is the primary key
-                # TODO: need some better way for this
+            
+            if foreign_keys is None:
                 if binary.left.table == binary.right.table:
-                    if binary.left.primary_key:
-                        source_column = binary.left
-                        dest_column = binary.right
-                    elif binary.right.primary_key:
-                        source_column = binary.right
-                        dest_column = binary.left
-                    elif binary.left in foreignkey:
-                        dest_column = binary.left
-                        source_column = binary.right
-                    elif binary.right in foreignkey:
-                        dest_column = binary.right
-                        source_column = binary.left
-                    else:
-                        raise exceptions.ArgumentError("Can't figure out which column is source/dest in join clause '%s'" % str(binary))
-                # for other relationships we are more flexible
-                # and go off the 'foreignkey' property
-                elif binary.left in foreignkey:
-                    dest_column = binary.left
-                    source_column = binary.right
-                elif binary.right in foreignkey:
-                    dest_column = binary.right
-                    source_column = binary.left
-                else:
-                    return
-            else:
+                    raise exceptions.ArgumentError("need foreign_keys argument for self-referential sync")
+                    
                 if binary.left in [f.column for f in binary.right.foreign_keys]:
                     dest_column = binary.right
                     source_column = binary.left
                 elif binary.right in [f.column for f in binary.left.foreign_keys]:
                     dest_column = binary.left
                     source_column = binary.right
+            else:
+                if binary.left in foreign_keys:
+                    source_column=binary.right
+                    dest_column = binary.left
+                elif binary.right in foreign_keys:
+                    source_column = binary.left
+                    dest_column = binary.right
             
             if source_column and dest_column:    
                 if self.direction == ONETOMANY:
index 259ef00b6702f1a800aea9efcfffdea19b7526ff..275fec343392e5e225f2d69f78a5fbfd7bf80ca4 100644 (file)
@@ -642,7 +642,7 @@ class ColumnElement(Selectable, _CompareMixin):
     may correspond to several TableClause-attached columns)."""
     
     primary_key = property(lambda self:getattr(self, '_primary_key', False), doc="primary key flag.  indicates if this Column represents part or whole of a primary key.")
-    foreign_keys = property(lambda self:getattr(self, '_foreign_keys', []), doc="foreign key accessor.  points to a ForeignKey object which represents a Foreign Key placed on this column's ultimate ancestor.")
+    foreign_keys = property(lambda self:getattr(self, '_foreign_keys', []), doc="foreign key accessor.  points to a list of ForeignKey objects which represents a Foreign Key placed on this column's ultimate ancestor.")
     columns = property(lambda self:[self], doc="Columns accessor which just returns self, to provide compatibility with Selectable objects.")
     def _one_fkey(self):
         if len(self._foreign_keys):
index 6c093bf9ddcb9df67a26addcd5b553af2d256fb4..29bec2aa86f564c7ebaee00fab31a07e0475c5cd 100644 (file)
@@ -43,7 +43,7 @@ class RelationTest1(testbase.ORMTest):
         try:
             compile_mappers()
         except exceptions.ArgumentError, ar:
-            assert str(ar) == "Cant determine relation direction for 'manager' on mapper 'Mapper|Person|people' with primary join 'people.manager_id = managers.person_id' - foreign key columns are present in both the parent and the child's mapped tables.  Specify 'foreignkey' argument."
+            assert str(ar) == "Cant determine relation direction for relationship 'Person.manager (Manager)' - foreign key columns are present in both the parent and the child's mapped tables.  Specify 'foreign_keys' argument."
 
         clear_mappers()
 
index bdb5a15c9c7cdee356ea0b4aef2a24fbadc89e45..5539df6b71496c93b38044b7b5ee35e5f109897c 100644 (file)
@@ -455,7 +455,7 @@ class ForeignPKTest(UnitOfWorkTest):
               },
             )
 
-        assert list(m2.props['sites'].foreignkey) == [peoplesites.c.person]
+        assert list(m2.props['sites'].foreign_keys) == [peoplesites.c.person]
         p = Person()
         p.person = 'im the key'
         p.firstname = 'asdf'