* secondary - for a many-to-many relationship, specifies the intermediary table.
* primaryjoin - a ClauseElement that will be used as the primary join of this child object against the parent object, or in a many-to-many relationship the join of the primary object to the association table. By default, this value is computed based on the foreign key relationships of the parent and child tables (or association table).
* secondaryjoin - a ClauseElement that will be used as the join of an association table to the child object. By default, this value is computed based on the foreign key relationships of the association and child tables.
-* foreignkey - specifies which column in this relationship is "foreign", i.e. which column refers to the parent object. This value is automatically determined in most cases based on the primary and secondary join conditions, except in the case of a self-referential mapper, where it is needed to indicate the child object's reference back to its parent, or in the case where the join conditions do not represent any primary key columns to properly represent the direction of the relationship.
+* foreignkey - a single Column, or list of Columns, specifying which column(s) in this relationship are "foreign", i.e. which column refers to the remote object. This value is automatically determined in most cases from the primary join condition, by looking at which column in the join condition contains a `ForeignKey` pointing to the other column in an equality expression. Specifying it here can override the normal foreign key properties of the join condition, which is useful for self-referential table relationships, join conditions where a `ForeignKey` is not present, or where the same column might appear on both sides of the join condition.
* uselist - a boolean that indicates if this property should be loaded as a list or a scalar. In most cases, this value is determined based on the type and direction of the relationship - one to many forms a list, many to one forms a scalar, many to many is a list. If a scalar is desired where normally a list would be present, such as a bi-directional one-to-one relationship, set uselist to False.
* private - setting `private=True` is the equivalent of setting `cascade="all, delete-orphan"`, and indicates the lifecycle of child objects should be contained within that of the parent. See the example in [datamapping_relations_cycle](rel:datamapping_relations_lifecycle).
* backref - indicates the name of a property to be placed on the related mapper's class that will handle this relationship in the other direction, including synchronizing the object attributes on both sides of the relation. Can also point to a `backref()` construct for more configurability. See [datamapping_relations_backreferences](rel:datamapping_relations_backreferences).
}
)
-The "root" property on a TreeNode is a many-to-one relationship. By default, a self-referential mapper declares relationships as one-to-many, so the extra parameter `foreignkey`, pointing to the remote side of a relationship, is needed to indicate a "many-to-one" self-referring relationship.
+The "root" property on a TreeNode is a many-to-one relationship. By default, a self-referential mapper declares relationships as one-to-many, so the extra parameter `foreignkey`, pointing to a column or list of columns on the remote side of a relationship, is needed to indicate a "many-to-one" self-referring relationship.
Both TreeNode examples above are available in functional form in the `examples/adjacencytree` directory of the distribution.
### Result-Set Mapping {@name=resultset}
self.post_update = post_update
self.direction = None
- # would like to have foreignkey be a list.
- # however, have to figure out how to do
- # <column> in <list>, since column overrides the == operator
- # and it doesnt work
- self.foreignkey = foreignkey #util.to_set(foreignkey)
- if foreignkey:
- self.foreigntable = foreignkey.table
- else:
- self.foreigntable = None
-
+ self.foreignkey = util.to_set(foreignkey)
+
if cascade is not None:
self.cascade = mapperutil.CascadeOptions(cascade)
else:
# 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 self.foreignkey is None and self.secondaryjoin is None:
+ 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()
if self.secondaryjoin is not None:
return sync.MANYTOMANY
elif self.parent.mapped_table is self.target or self.parent.select_table is self.target:
- if self.foreignkey.primary_key:
+ if list(self.foreignkey)[0].primary_key:
return sync.MANYTOONE
else:
return sync.ONETOMANY
- elif self.foreigntable == self.mapper.unjoined_table:
+ elif len([c for c in self.foreignkey if self.mapper.unjoined_table.corresponding_column(c, False) is not None]):
return sync.ONETOMANY
- elif self.foreigntable == self.parent.unjoined_table:
+ elif len([c for c in self.foreignkey if self.parent.unjoined_table.corresponding_column(c, False) is not None]):
return sync.MANYTOONE
else:
- raise exceptions.ArgumentError("Cant determine relation direction")
+ raise exceptions.ArgumentError("Cant determine relation direction " + repr(self.foreignkey))
def _find_dependent(self):
"""searches through the primary join condition to determine which side
- has the primary key and which has the foreign key - from this we return
+ has the foreign key - from this we return
the "foreign key" for this property which helps determine one-to-many/many-to-one."""
-
- # set as a reference to allow assignment from inside a first-class function
- dependent = [None]
+ foreignkeys = sets.Set()
def foo(binary):
if binary.operator != '=' or not isinstance(binary.left, schema.Column) or not isinstance(binary.right, schema.Column):
return
- if binary.left.primary_key:
- if dependent[0] is binary.left.table:
- raise exceptions.ArgumentError("Could not determine the parent/child relationship for property '%s', based on join condition '%s' (table '%s' appears on both sides of the relationship, or in an otherwise ambiguous manner). please specify the 'foreignkey' keyword parameter to the relation() function indicating a column on the remote side of the relationship" % (self.key, str(self.primaryjoin), binary.left.table.name))
- dependent[0] = binary.right.table
- self.foreignkey= binary.right
- elif binary.right.primary_key:
- if dependent[0] is binary.right.table:
- raise exceptions.ArgumentError("Could not determine the parent/child relationship for property '%s', based on join condition '%s' (table '%s' appears on both sides of the relationship, or in an otherwise ambiguous manner). please specify the 'foreignkey' keyword parameter to the relation() function indicating a column on the remote side of the relationship" % (self.key, str(self.primaryjoin), binary.right.table.name))
- dependent[0] = binary.left.table
- self.foreignkey = binary.left
+ if binary.left.foreign_key is not None and binary.left.foreign_key.references(binary.right.table):
+ foreignkeys.add(binary.left)
+ elif binary.right.foreign_key is not None and binary.right.foreign_key.references(binary.left.table):
+ foreignkeys.add(binary.right)
visitor = BinaryVisitor(foo)
self.primaryjoin.accept_visitor(visitor)
- if dependent[0] is None:
- raise exceptions.ArgumentError("Could not determine the parent/child relationship for property '%s', based on join condition '%s' (no relationships joining tables '%s' and '%s' could be located). please specify the 'foreignkey' keyword parameter to the relation() function indicating a column on the remote side of the relationship" % (self.key, str(self.primaryjoin), str(binary.left.table), binary.right.table.name))
- else:
- self.foreigntable = dependent[0]
-
+ self.foreignkey = foreignkeys
+
def get_join(self):
if self.secondaryjoin is not None:
return self.primaryjoin & self.secondaryjoin
def visit_binary(binary):
circular = isinstance(binary.left, schema.Column) and isinstance(binary.right, schema.Column) and binary.left.table is binary.right.table
- if isinstance(binary.left, schema.Column) and isinstance(binary.right, schema.Column) and ((not circular and binary.left.table is table) or (circular and binary.right is foreignkey)):
+ if isinstance(binary.left, schema.Column) and isinstance(binary.right, schema.Column) and ((not circular and binary.left.table is table) or (circular and binary.right in foreignkey)):
col = binary.left
binary.left = binds.setdefault(binary.left,
sql.BindParamClause(bind_label(), None, shortname = binary.left.name))
reverse[binary.right] = binds[col]
- if isinstance(binary.right, schema.Column) and isinstance(binary.left, schema.Column) and ((not circular and binary.right.table is table) or (circular and binary.left is foreignkey)):
+ if isinstance(binary.right, schema.Column) and isinstance(binary.left, schema.Column) and ((not circular and binary.right.table is table) or (circular and binary.left in foreignkey)):
col = binary.right
binary.right = binds.setdefault(binary.right,
sql.BindParamClause(bind_label(), None, shortname = binary.right.name))
e.data = 'some more data'
ctx.current.flush()
+class ForeignPKTest(SessionTest):
+ """tests mapper detection of the relationship direction when parent/child tables are joined on their
+ primary keys"""
+ def setUpAll(self):
+ SessionTest.setUpAll(self)
+ global metadata, people, peoplesites
+ metadata = BoundMetaData(testbase.db)
+ people = Table("people", metadata,
+ Column('person', String(10), primary_key=True),
+ Column('firstname', String(10)),
+ Column('lastname', String(10)),
+ )
+
+ peoplesites = Table("peoplesites", metadata,
+ Column('person', String(10), ForeignKey("people.person"),
+ primary_key=True),
+ Column('site', String(10)),
+ )
+ metadata.create_all()
+ def tearDownAll(self):
+ metadata.drop_all()
+ SessionTest.tearDownAll(self)
+ def testbasic(self):
+ class PersonSite(object):pass
+ class Person(object):pass
+ m1 = mapper(PersonSite, peoplesites)
+
+ m2 = mapper(Person, people,
+ properties = {
+ 'sites' : relation(PersonSite),
+ },
+ )
+
+ assert list(m2.props['sites'].foreignkey) == [peoplesites.c.person]
+ p = Person()
+ p.person = 'im the key'
+ p.firstname = 'asdf'
+ ps = PersonSite()
+ ps.site = 'asdf'
+ p.sites.append(ps)
+ ctx.current.flush()
+ assert people.count(people.c.person=='im the key').scalar() == peoplesites.count(peoplesites.c.person=='im the key').scalar() == 1
+
class PrivateAttrTest(SessionTest):
"""tests various things to do with private=True mappers"""
def setUpAll(self):