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
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)
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:
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):
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
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
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
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:
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]
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
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)
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()
# 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
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,
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)