14
from sqlalchemy import sql, util, log, exc as sa_exc
15
from sqlalchemy.sql.util import ClauseAdapter, criterion_as_pairs, \
16
join_condition, _shallow_annotate
17
from sqlalchemy.sql import operators, expression
18
from sqlalchemy.orm import attributes, dependency, mapper, \
19
object_mapper, strategies, configure_mappers
20
from sqlalchemy.orm.util import CascadeOptions, _class_to_mapper, \
21
_orm_annotate, _orm_deannotate
23
from sqlalchemy.orm.interfaces import MANYTOMANY, MANYTOONE, \
24
MapperProperty, ONETOMANY, PropComparator, StrategizedProperty
14
from .. import sql, util, log, exc as sa_exc, inspect
15
from ..sql import operators, expression
18
strategies, configure_mappers, relationships,
21
from .util import CascadeOptions, \
22
_orm_annotate, _orm_deannotate, _orm_full_deannotate
24
from .interfaces import MANYTOMANY, MANYTOONE, ONETOMANY,\
25
PropComparator, StrategizedProperty
25
27
mapperlib = util.importlater("sqlalchemy.orm", "mapperlib")
26
28
NoneType = type(None)
28
__all__ = ('ColumnProperty', 'CompositeProperty', 'SynonymProperty',
29
'ComparableProperty', 'RelationshipProperty', 'RelationProperty')
31
30
from descriptor_props import CompositeProperty, SynonymProperty, \
32
ComparableProperty,ConcreteInheritedProperty
31
ComparableProperty, ConcreteInheritedProperty
33
__all__ = ['ColumnProperty', 'CompositeProperty', 'SynonymProperty',
34
'ComparableProperty', 'RelationshipProperty', 'RelationProperty']
34
37
class ColumnProperty(StrategizedProperty):
35
38
"""Describes an object attribute that corresponds to a table column.
998
1082
self.target = self.mapper.mapped_table
1000
if self.cascade.delete_orphan:
1001
self.mapper.primary_mapper().delete_orphans.append(
1085
def _setup_join_conditions(self):
1086
self._join_condition = jc = relationships.JoinCondition(
1087
parent_selectable=self.parent.mapped_table,
1088
child_selectable=self.mapper.mapped_table,
1089
parent_local_selectable=self.parent.local_table,
1090
child_local_selectable=self.mapper.local_table,
1091
primaryjoin=self.primaryjoin,
1092
secondary=self.secondary,
1093
secondaryjoin=self.secondaryjoin,
1094
parent_equivalents=self.parent._equivalent_columns,
1095
child_equivalents=self.mapper._equivalent_columns,
1096
consider_as_foreign_keys=self._user_defined_foreign_keys,
1097
local_remote_pairs=self.local_remote_pairs,
1098
remote_side=self.remote_side,
1099
self_referential=self._is_self_referential,
1101
support_sync=not self.viewonly,
1102
can_be_synced_fn=self._columns_are_mapped
1104
self.primaryjoin = jc.deannotated_primaryjoin
1105
self.secondaryjoin = jc.deannotated_secondaryjoin
1106
self.direction = jc.direction
1107
self.local_remote_pairs = jc.local_remote_pairs
1108
self.remote_side = jc.remote_columns
1109
self.local_columns = jc.local_columns
1110
self.synchronize_pairs = jc.synchronize_pairs
1111
self._calculated_foreign_keys = jc.foreign_key_columns
1112
self.secondary_synchronize_pairs = jc.secondary_synchronize_pairs
1114
def _check_conflicts(self):
1115
"""Test that this relationship is legal, warn about
1116
inheritance conflicts."""
1118
if not self.is_primary() \
1119
and not mapper.class_mapper(
1121
configure=False).has_property(self.key):
1122
raise sa_exc.ArgumentError("Attempting to assign a new "
1123
"relationship '%s' to a non-primary mapper on "
1124
"class '%s'. New relationships can only be added "
1125
"to the primary mapper, i.e. the very first mapper "
1126
"created for class '%s' " % (self.key,
1127
self.parent.class_.__name__,
1128
self.parent.class_.__name__))
1130
# check for conflicting relationship() on superclass
1131
if not self.parent.concrete:
1132
for inheriting in self.parent.iterate_to_root():
1133
if inheriting is not self.parent \
1134
and inheriting.has_property(self.key):
1135
util.warn("Warning: relationship '%s' on mapper "
1136
"'%s' supersedes the same relationship "
1137
"on inherited mapper '%s'; this can "
1138
"cause dependency issues during flush"
1139
% (self.key, self.parent, inheriting))
1141
def _get_cascade(self):
1142
"""Return the current cascade setting for this
1143
:class:`.RelationshipProperty`.
1145
return self._cascade
1147
def _set_cascade(self, cascade):
1148
cascade = CascadeOptions(cascade)
1149
if 'mapper' in self.__dict__:
1150
self._check_cascade_settings(cascade)
1151
self._cascade = cascade
1153
if self._dependency_processor:
1154
self._dependency_processor.cascade = cascade
1156
cascade = property(_get_cascade, _set_cascade)
1158
def _check_cascade_settings(self, cascade):
1159
if cascade.delete_orphan and not self.single_parent \
1160
and (self.direction is MANYTOMANY or self.direction
1162
raise sa_exc.ArgumentError(
1163
'On %s, delete-orphan cascade is not supported '
1164
'on a many-to-many or many-to-one relationship '
1165
'when single_parent is not set. Set '
1166
'single_parent=True on the relationship().'
1168
if self.direction is MANYTOONE and self.passive_deletes:
1169
util.warn("On %s, 'passive_deletes' is normally configured "
1170
"on one-to-many, one-to-one, many-to-many "
1171
"relationships only."
1174
if self.passive_deletes == 'all' and \
1175
("delete" in cascade or
1176
"delete-orphan" in cascade):
1177
raise sa_exc.ArgumentError(
1178
"On %s, can't set passive_deletes='all' in conjunction "
1179
"with 'delete' or 'delete-orphan' cascade" % self)
1181
if cascade.delete_orphan:
1182
self.mapper.primary_mapper()._delete_orphans.append(
1002
1183
(self.key, self.parent.class_)
1005
def _determine_joins(self):
1006
"""Determine the 'primaryjoin' and 'secondaryjoin' attributes,
1007
if not passed to the constructor already.
1009
This is based on analysis of the foreign key relationships
1010
between the parent and target mapped selectables.
1013
if self.secondaryjoin is not None and self.secondary is None:
1014
raise sa_exc.ArgumentError("Property '" + self.key
1015
+ "' specified with secondary join condition but "
1016
"no secondary argument")
1018
# if join conditions were not specified, figure them out based
1021
def _search_for_join(mapper, table):
1022
# find a join between the given mapper's mapped table and
1023
# the given table. will try the mapper's local table first
1024
# for more specificity, then if not found will try the more
1025
# general mapped table, which in the case of inheritance is
1027
return join_condition(mapper.mapped_table, table,
1028
a_subset=mapper.local_table)
1031
if self.secondary is not None:
1032
if self.secondaryjoin is None:
1033
self.secondaryjoin = _search_for_join(self.mapper,
1035
if self.primaryjoin is None:
1036
self.primaryjoin = _search_for_join(self.parent,
1039
if self.primaryjoin is None:
1040
self.primaryjoin = _search_for_join(self.parent,
1042
except sa_exc.ArgumentError, e:
1043
raise sa_exc.ArgumentError("Could not determine join "
1044
"condition between parent/child tables on "
1045
"relationship %s. Specify a 'primaryjoin' "
1046
"expression. If 'secondary' is present, "
1047
"'secondaryjoin' is needed as well."
1050
1186
def _columns_are_mapped(self, *cols):
1051
1187
"""Return True if all columns in the given collection are
1052
1188
mapped by the tables referenced by this :class:`.Relationship`.
1056
1192
if self.secondary is not None \
1057
and self.secondary.c.contains_column(c):
1193
and self.secondary.c.contains_column(c):
1059
1195
if not self.parent.mapped_table.c.contains_column(c) and \
1060
not self.target.c.contains_column(c):
1196
not self.target.c.contains_column(c):
1064
def _sync_pairs_from_join(self, join_condition, primary):
1065
"""Determine a list of "source"/"destination" column pairs
1066
based on the given join condition, as well as the
1067
foreign keys argument.
1069
"source" would be a column referenced by a foreign key,
1070
and "destination" would be the column who has a foreign key
1071
reference to "source".
1075
fks = self._user_defined_foreign_keys
1077
eq_pairs = criterion_as_pairs(join_condition,
1078
consider_as_foreign_keys=fks,
1079
any_operator=self.viewonly)
1081
# couldn't find any fks, but we have
1082
# "secondary" - assume the "secondary" columns
1084
if not eq_pairs and \
1085
self.secondary is not None and \
1087
fks = set(self.secondary.c)
1088
eq_pairs = criterion_as_pairs(join_condition,
1089
consider_as_foreign_keys=fks,
1090
any_operator=self.viewonly)
1093
util.warn("No ForeignKey objects were present "
1094
"in secondary table '%s'. Assumed referenced "
1095
"foreign key columns %s for join condition '%s' "
1096
"on relationship %s" % (
1097
self.secondary.description,
1098
", ".join(sorted(["'%s'" % col for col in fks])),
1103
# Filter out just to columns that are mapped.
1104
# If viewonly, allow pairs where the FK col
1105
# was part of "foreign keys" - the column it references
1106
# may be in an un-mapped table - see
1107
# test.orm.test_relationships.ViewOnlyComplexJoin.test_basic
1108
# for an example of this.
1109
eq_pairs = [(l, r) for (l, r) in eq_pairs
1110
if self._columns_are_mapped(l, r)
1111
or self.viewonly and
1117
# from here below is just determining the best error message
1118
# to report. Check for a join condition using any operator
1119
# (not just ==), perhaps they need to turn on "viewonly=True".
1120
if not self.viewonly and criterion_as_pairs(join_condition,
1121
consider_as_foreign_keys=self._user_defined_foreign_keys,
1124
err = "Could not locate any "\
1125
"foreign-key-equated, locally mapped column "\
1127
"condition '%s' on relationship %s." % (
1128
primary and 'primaryjoin' or 'secondaryjoin',
1133
if not self._user_defined_foreign_keys:
1134
err += " Ensure that the "\
1135
"referencing Column objects have a "\
1136
"ForeignKey present, or are otherwise part "\
1137
"of a ForeignKeyConstraint on their parent "\
1138
"Table, or specify the foreign_keys parameter "\
1139
"to this relationship."
1141
err += " For more "\
1142
"relaxed rules on join conditions, the "\
1143
"relationship may be marked as viewonly=True."
1145
raise sa_exc.ArgumentError(err)
1147
if self._user_defined_foreign_keys:
1148
raise sa_exc.ArgumentError("Could not determine "
1149
"relationship direction for %s condition "
1150
"'%s', on relationship %s, using manual "
1151
"'foreign_keys' setting. Do the columns "
1152
"in 'foreign_keys' represent all, and "
1153
"only, the 'foreign' columns in this join "
1154
"condition? Does the %s Table already "
1155
"have adequate ForeignKey and/or "
1156
"ForeignKeyConstraint objects established "
1157
"(in which case 'foreign_keys' is usually "
1160
primary and 'primaryjoin' or 'secondaryjoin',
1163
primary and 'mapped' or 'secondary'
1166
raise sa_exc.ArgumentError("Could not determine "
1167
"relationship direction for %s condition "
1168
"'%s', on relationship %s. Ensure that the "
1169
"referencing Column objects have a "
1170
"ForeignKey present, or are otherwise part "
1171
"of a ForeignKeyConstraint on their parent "
1172
"Table, or specify the foreign_keys parameter "
1173
"to this relationship."
1175
primary and 'primaryjoin' or 'secondaryjoin',
1180
def _determine_synchronize_pairs(self):
1181
"""Resolve 'primary'/foreign' column pairs from the primaryjoin
1182
and secondaryjoin arguments.
1185
if self.local_remote_pairs:
1186
if not self._user_defined_foreign_keys:
1187
raise sa_exc.ArgumentError(
1188
"foreign_keys argument is "
1189
"required with _local_remote_pairs argument")
1190
self.synchronize_pairs = []
1191
for l, r in self.local_remote_pairs:
1192
if r in self._user_defined_foreign_keys:
1193
self.synchronize_pairs.append((l, r))
1194
elif l in self._user_defined_foreign_keys:
1195
self.synchronize_pairs.append((r, l))
1197
self.synchronize_pairs = self._sync_pairs_from_join(
1201
self._calculated_foreign_keys = util.column_set(
1203
self.synchronize_pairs)
1205
if self.secondaryjoin is not None:
1206
self.secondary_synchronize_pairs = self._sync_pairs_from_join(
1209
self._calculated_foreign_keys.update(
1211
self.secondary_synchronize_pairs)
1213
self.secondary_synchronize_pairs = None
1215
def _determine_direction(self):
1216
"""Determine if this relationship is one to many, many to one,
1219
This is derived from the primaryjoin, presence of "secondary",
1220
and in the case of self-referential the "remote side".
1223
if self.secondaryjoin is not None:
1224
self.direction = MANYTOMANY
1225
elif self._refers_to_parent_table():
1227
# self referential defaults to ONETOMANY unless the "remote"
1228
# side is present and does not reference any foreign key
1231
if self.local_remote_pairs:
1232
remote = [r for (l, r) in self.local_remote_pairs]
1233
elif self.remote_side:
1234
remote = self.remote_side
1237
if not remote or self._calculated_foreign_keys.difference(l for (l,
1238
r) in self.synchronize_pairs).intersection(remote):
1239
self.direction = ONETOMANY
1241
self.direction = MANYTOONE
1243
parentcols = util.column_set(self.parent.mapped_table.c)
1244
targetcols = util.column_set(self.mapper.mapped_table.c)
1246
# fk collection which suggests ONETOMANY.
1247
onetomany_fk = targetcols.intersection(
1248
self._calculated_foreign_keys)
1250
# fk collection which suggests MANYTOONE.
1252
manytoone_fk = parentcols.intersection(
1253
self._calculated_foreign_keys)
1255
if onetomany_fk and manytoone_fk:
1256
# fks on both sides. do the same test only based on the
1258
referents = [c for (c, f) in self.synchronize_pairs]
1259
onetomany_local = parentcols.intersection(referents)
1260
manytoone_local = targetcols.intersection(referents)
1262
if onetomany_local and not manytoone_local:
1263
self.direction = ONETOMANY
1264
elif manytoone_local and not onetomany_local:
1265
self.direction = MANYTOONE
1267
raise sa_exc.ArgumentError(
1268
"Can't determine relationship"
1269
" direction for relationship '%s' - foreign "
1270
"key columns are present in both the parent "
1271
"and the child's mapped tables. Specify "
1272
"'foreign_keys' argument." % self)
1274
self.direction = ONETOMANY
1276
self.direction = MANYTOONE
1278
raise sa_exc.ArgumentError("Can't determine relationship "
1279
"direction for relationship '%s' - foreign "
1280
"key columns are present in neither the parent "
1281
"nor the child's mapped tables" % self)
1283
if self.cascade.delete_orphan and not self.single_parent \
1284
and (self.direction is MANYTOMANY or self.direction
1286
util.warn('On %s, delete-orphan cascade is not supported '
1287
'on a many-to-many or many-to-one relationship '
1288
'when single_parent is not set. Set '
1289
'single_parent=True on the relationship().'
1291
if self.direction is MANYTOONE and self.passive_deletes:
1292
util.warn("On %s, 'passive_deletes' is normally configured "
1293
"on one-to-many, one-to-one, many-to-many "
1294
"relationships only."
1297
def _determine_local_remote_pairs(self):
1298
"""Determine pairs of columns representing "local" to
1299
"remote", where "local" columns are on the parent mapper,
1300
"remote" are on the target mapper.
1302
These pairs are used on the load side only to generate
1303
lazy loading clauses.
1306
if not self.local_remote_pairs and not self.remote_side:
1307
# the most common, trivial case. Derive
1308
# local/remote pairs from the synchronize pairs.
1309
eq_pairs = util.unique_list(
1310
self.synchronize_pairs +
1311
(self.secondary_synchronize_pairs or []))
1312
if self.direction is MANYTOONE:
1313
self.local_remote_pairs = [(r, l) for l, r in eq_pairs]
1315
self.local_remote_pairs = eq_pairs
1317
# "remote_side" specified, derive from the primaryjoin
1318
# plus remote_side, similarly to how synchronize_pairs
1320
elif self.remote_side:
1321
if self.local_remote_pairs:
1322
raise sa_exc.ArgumentError('remote_side argument is '
1323
'redundant against more detailed '
1324
'_local_remote_side argument.')
1325
if self.direction is MANYTOONE:
1326
self.local_remote_pairs = [(r, l) for (l, r) in
1327
criterion_as_pairs(self.primaryjoin,
1328
consider_as_referenced_keys=self.remote_side,
1332
self.local_remote_pairs = \
1333
criterion_as_pairs(self.primaryjoin,
1334
consider_as_foreign_keys=self.remote_side,
1336
if not self.local_remote_pairs:
1337
raise sa_exc.ArgumentError('Relationship %s could '
1338
'not determine any local/remote column '
1339
'pairs from remote side argument %r'
1340
% (self, self.remote_side))
1341
# else local_remote_pairs were sent explcitly via
1342
# ._local_remote_pairs.
1344
# create local_side/remote_side accessors
1345
self.local_side = util.ordered_column_set(
1346
l for l, r in self.local_remote_pairs)
1347
self.remote_side = util.ordered_column_set(
1348
r for l, r in self.local_remote_pairs)
1350
# check that the non-foreign key column in the local/remote
1351
# collection is mapped. The foreign key
1352
# which the individual mapped column references directly may
1353
# itself be in a non-mapped table; see
1354
# test.orm.test_relationships.ViewOnlyComplexJoin.test_basic
1355
# for an example of this.
1356
if self.direction is ONETOMANY:
1357
for col in self.local_side:
1358
if not self._columns_are_mapped(col):
1359
raise sa_exc.ArgumentError(
1360
"Local column '%s' is not "
1361
"part of mapping %s. Specify remote_side "
1362
"argument to indicate which column lazy join "
1363
"condition should compare against." % (col,
1365
elif self.direction is MANYTOONE:
1366
for col in self.remote_side:
1367
if not self._columns_are_mapped(col):
1368
raise sa_exc.ArgumentError(
1369
"Remote column '%s' is not "
1370
"part of mapping %s. Specify remote_side "
1371
"argument to indicate which column lazy join "
1372
"condition should bind." % (col, self.mapper))
1374
1200
def _generate_backref(self):
1201
"""Interpret the 'backref' instruction to create a
1202
:func:`.relationship` complementary to this one."""
1375
1204
if not self.is_primary():
1377
1206
if self.backref is not None and not self.back_populates:
1493
# place a barrier on the destination such that
1494
# replacement traversals won't ever dig into it.
1495
# its internal structure remains fixed
1496
# regardless of context.
1497
dest_selectable = _shallow_annotate(
1499
{'no_replacement_traverse':True})
1501
aliased = aliased or (source_selectable is not None)
1503
primaryjoin, secondaryjoin, secondary = self.primaryjoin, \
1504
self.secondaryjoin, self.secondary
1506
# adjust the join condition for single table inheritance,
1507
# in the case that the join is to a subclass
1508
# this is analogous to the "_adjust_for_single_table_inheritance()"
1511
1302
dest_mapper = of_type or self.mapper
1513
1304
single_crit = dest_mapper._single_table_criterion
1514
if single_crit is not None:
1515
if secondaryjoin is not None:
1516
secondaryjoin = secondaryjoin & single_crit
1518
primaryjoin = primaryjoin & single_crit
1305
aliased = aliased or (source_selectable is not None)
1521
if secondary is not None:
1522
secondary = secondary.alias()
1523
primary_aliasizer = ClauseAdapter(secondary)
1524
secondary_aliasizer = \
1525
ClauseAdapter(dest_selectable,
1526
equivalents=self.mapper._equivalent_columns).\
1527
chain(primary_aliasizer)
1528
if source_selectable is not None:
1529
primary_aliasizer = \
1530
ClauseAdapter(secondary).\
1531
chain(ClauseAdapter(source_selectable,
1532
equivalents=self.parent._equivalent_columns))
1534
secondary_aliasizer.traverse(secondaryjoin)
1536
primary_aliasizer = ClauseAdapter(dest_selectable,
1537
exclude=self.local_side,
1538
equivalents=self.mapper._equivalent_columns)
1539
if source_selectable is not None:
1540
primary_aliasizer.chain(
1541
ClauseAdapter(source_selectable,
1542
exclude=self.remote_side,
1543
equivalents=self.parent._equivalent_columns))
1544
secondary_aliasizer = None
1545
primaryjoin = primary_aliasizer.traverse(primaryjoin)
1546
target_adapter = secondary_aliasizer or primary_aliasizer
1547
target_adapter.include = target_adapter.exclude = None
1549
target_adapter = None
1307
primaryjoin, secondaryjoin, secondary, target_adapter, dest_selectable = \
1308
self._join_condition.join_targets(
1309
source_selectable, dest_selectable, aliased, single_crit
1550
1311
if source_selectable is None:
1551
1312
source_selectable = self.parent.local_table
1552
1313
if dest_selectable is None:
1553
1314
dest_selectable = self.mapper.local_table
1315
return (primaryjoin, secondaryjoin, source_selectable,
1316
dest_selectable, secondary, target_adapter)
1563
1319
PropertyLoader = RelationProperty = RelationshipProperty
1564
1320
log.class_logger(RelationshipProperty)