summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2018-12-29 20:54:29 -0500
committerMike Bayer <mike_mp@zzzcomputing.com>2019-01-25 09:28:28 -0500
commit93855ed623ceedffc02dee06c9a46c37dd26286b (patch)
tree5fdb783ea642d99693fd284dcbc04d4691423d16 /lib
parent78e598e3a5b8df7419a600c291f90260e598c9b7 (diff)
downloadsqlalchemy-93855ed623ceedffc02dee06c9a46c37dd26286b.tar.gz
Implement relationship to AliasedClass; deprecate non primary mappers
Implemented a new feature whereby the :class:`.AliasedClass` construct can now be used as the target of a :func:`.relationship`. This allows the concept of "non primary mappers" to no longer be necessary, as the :class:`.AliasedClass` is much easier to configure and automatically inherits all the relationships of the mapped class, as well as preserves the ability for loader options to work normally. - introduce new name for mapped_table, "persist_selectable". this is the selectable that selects against the local mapper and its superclasses, but does not include columns local only to subclasses. - relationship gains "entity" which is the mapper or aliasedinsp. - clarfiy name "entity" vs. "query_entity" in loader strategies. Fixes: #4423 Fixes: #4422 Fixes: #4421 Fixes: #3348 Change-Id: Ic3609b43dc4ed115006da9ad9189e574dc0c72d9
Diffstat (limited to 'lib')
-rw-r--r--lib/sqlalchemy/ext/declarative/base.py10
-rw-r--r--lib/sqlalchemy/orm/descriptor_props.py17
-rw-r--r--lib/sqlalchemy/orm/interfaces.py12
-rw-r--r--lib/sqlalchemy/orm/mapper.py116
-rw-r--r--lib/sqlalchemy/orm/query.py12
-rw-r--r--lib/sqlalchemy/orm/relationships.py118
-rw-r--r--lib/sqlalchemy/orm/session.py6
-rw-r--r--lib/sqlalchemy/orm/strategies.py105
-rw-r--r--lib/sqlalchemy/orm/strategy_options.py1
-rw-r--r--lib/sqlalchemy/orm/util.py53
10 files changed, 275 insertions, 175 deletions
diff --git a/lib/sqlalchemy/ext/declarative/base.py b/lib/sqlalchemy/ext/declarative/base.py
index 269cfce00..80cd23bc8 100644
--- a/lib/sqlalchemy/ext/declarative/base.py
+++ b/lib/sqlalchemy/ext/declarative/base.py
@@ -163,7 +163,7 @@ class _MapperConfig(object):
# dict_ will be a dictproxy, which we can't write to, and we need to!
self.dict_ = dict(dict_)
self.classname = classname
- self.mapped_table = None
+ self.persist_selectable = None
self.properties = util.OrderedDict()
self.declared_columns = set()
self.column_copies = {}
@@ -584,7 +584,7 @@ class _MapperConfig(object):
elif self.inherits:
inherited_mapper = _declared_mapping_info(self.inherits)
inherited_table = inherited_mapper.local_table
- inherited_mapped_table = inherited_mapper.mapped_table
+ inherited_persist_selectable = inherited_mapper.persist_selectable
if table is None:
# single table inheritance.
@@ -611,10 +611,10 @@ class _MapperConfig(object):
)
inherited_table.append_column(c)
if (
- inherited_mapped_table is not None
- and inherited_mapped_table is not inherited_table
+ inherited_persist_selectable is not None
+ and inherited_persist_selectable is not inherited_table
):
- inherited_mapped_table._refresh_for_new_column(c)
+ inherited_persist_selectable._refresh_for_new_column(c)
def _prepare_mapper_arguments(self):
properties = self.properties
diff --git a/lib/sqlalchemy/orm/descriptor_props.py b/lib/sqlalchemy/orm/descriptor_props.py
index c1e5866b5..a0ddf3a8b 100644
--- a/lib/sqlalchemy/orm/descriptor_props.py
+++ b/lib/sqlalchemy/orm/descriptor_props.py
@@ -673,16 +673,21 @@ class SynonymProperty(DescriptorProperty):
def set_parent(self, parent, init):
if self.map_column:
# implement the 'map_column' option.
- if self.key not in parent.mapped_table.c:
+ if self.key not in parent.persist_selectable.c:
raise sa_exc.ArgumentError(
"Can't compile synonym '%s': no column on table "
"'%s' named '%s'"
- % (self.name, parent.mapped_table.description, self.key)
+ % (
+ self.name,
+ parent.persist_selectable.description,
+ self.key,
+ )
)
elif (
- parent.mapped_table.c[self.key] in parent._columntoproperty
+ parent.persist_selectable.c[self.key]
+ in parent._columntoproperty
and parent._columntoproperty[
- parent.mapped_table.c[self.key]
+ parent.persist_selectable.c[self.key]
].key
== self.name
):
@@ -692,7 +697,9 @@ class SynonymProperty(DescriptorProperty):
"%r for column %r"
% (self.key, self.name, self.name, self.key)
)
- p = properties.ColumnProperty(parent.mapped_table.c[self.key])
+ p = properties.ColumnProperty(
+ parent.persist_selectable.c[self.key]
+ )
parent._configure_property(self.name, p, init=init, setparent=True)
p._mapped_by_synonym = self.key
diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py
index 02eed516b..87b4cfcde 100644
--- a/lib/sqlalchemy/orm/interfaces.py
+++ b/lib/sqlalchemy/orm/interfaces.py
@@ -118,7 +118,7 @@ class MapperProperty(_MappedAttribute, InspectionAttr, util.MemoizedSlots):
"""
return {}
- def setup(self, context, entity, path, adapter, **kwargs):
+ def setup(self, context, query_entity, path, adapter, **kwargs):
"""Called by Query for the purposes of constructing a SQL statement.
Each MapperProperty associated with the target mapper processes the
@@ -542,13 +542,15 @@ class StrategizedProperty(MapperProperty):
)
return strategy
- def setup(self, context, entity, path, adapter, **kwargs):
+ def setup(self, context, query_entity, path, adapter, **kwargs):
loader = self._get_context_loader(context, path)
if loader and loader.strategy:
strat = self._get_strategy(loader.strategy)
else:
strat = self.strategy
- strat.setup_query(context, entity, path, loader, adapter, **kwargs)
+ strat.setup_query(
+ context, query_entity, path, loader, adapter, **kwargs
+ )
def create_row_processor(
self, context, path, mapper, result, adapter, populators
@@ -722,7 +724,9 @@ class LoaderStrategy(object):
def init_class_attribute(self, mapper):
pass
- def setup_query(self, context, entity, path, loadopt, adapter, **kwargs):
+ def setup_query(
+ self, context, query_entity, path, loadopt, adapter, **kwargs
+ ):
"""Establish column and other state for a given QueryContext.
This method fulfills the contract specified by MapperProperty.setup().
diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py
index 0c8ab0b10..ff5148ed1 100644
--- a/lib/sqlalchemy/orm/mapper.py
+++ b/lib/sqlalchemy/orm/mapper.py
@@ -114,11 +114,19 @@ class Mapper(InspectionAttr):
),
order_by=(
"1.1",
- "The :paramref:`.Mapper.order_by` parameter "
+ "The :paramref:`.mapper.order_by` parameter "
"is deprecated, and will be removed in a future release. "
"Use :meth:`.Query.order_by` to determine the ordering of a "
"result set.",
),
+ non_primary=(
+ "1.3",
+ "The :paramref:`.mapper.non_primary` parameter is deprecated, "
+ "and will be removed in a future release. The functionality "
+ "of non primary mappers is now better suited using the "
+ ":class:`.AliasedClass` construct, which can also be used "
+ "as the target of a :func:`.relationship` in 1.3.",
+ ),
)
def __init__(
self,
@@ -755,26 +763,34 @@ class Mapper(InspectionAttr):
.. seealso::
- :attr:`~.Mapper.mapped_table`.
+ :attr:`~.Mapper.persist_selectable`.
"""
- mapped_table = None
+ persist_selectable = None
"""The :class:`.Selectable` to which this :class:`.Mapper` is mapped.
Typically an instance of :class:`.Table`, :class:`.Join`, or
:class:`.Alias`.
- The "mapped" table is the selectable that
- the mapper selects from during queries. For non-inheriting
- mappers, the mapped table is the same as the "local" table.
- For joined-table inheritance mappers, mapped_table references the
- full :class:`.Join` representing full rows for this particular
- subclass. For single-table inheritance mappers, mapped_table
- references the base table.
+ The :attr:`.Mapper.persist_selectable` is separate from
+ :attr:`.Mapper.selectable` in that the former represents columns
+ that are mapped on this class or its superclasses, whereas the
+ latter may be a "polymorphic" selectable that contains additional columns
+ which are in fact mapped on subclasses only.
+
+ "persist selectable" is the "thing the mapper writes to" and
+ "selectable" is the "thing the mapper selects from".
+
+ :attr:`.Mapper.persist_selectable` is also separate from
+ :attr:`.Mapper.local_table`, which represents the set of columns that
+ are locally mapped on this class directly.
+
.. seealso::
+ :attr:`~.Mapper.selectable`.
+
:attr:`~.Mapper.local_table`.
"""
@@ -827,8 +843,8 @@ class Mapper(InspectionAttr):
which comprise the 'primary key' of the mapped table, from the
perspective of this :class:`.Mapper`.
- This list is against the selectable in :attr:`~.Mapper.mapped_table`. In
- the case of inheriting mappers, some columns may be managed by a
+ This list is against the selectable in :attr:`~.Mapper.persist_selectable`.
+ In the case of inheriting mappers, some columns may be managed by a
superclass mapper. For example, in the case of a :class:`.Join`, the
primary key is determined by all of the primary key columns across all
tables referenced by the :class:`.Join`.
@@ -965,6 +981,11 @@ class Mapper(InspectionAttr):
c = None
"""A synonym for :attr:`~.Mapper.columns`."""
+ @property
+ @util.deprecated("1.3", "Use .persist_selectable")
+ def mapped_table(self):
+ return self.persist_selectable
+
@util.memoized_property
def _path_registry(self):
return PathRegistry.per_mapper(self)
@@ -994,11 +1015,11 @@ class Mapper(InspectionAttr):
# inherit_condition is optional.
if self.local_table is None:
self.local_table = self.inherits.local_table
- self.mapped_table = self.inherits.mapped_table
+ self.persist_selectable = self.inherits.persist_selectable
self.single = True
elif self.local_table is not self.inherits.local_table:
if self.concrete:
- self.mapped_table = self.local_table
+ self.persist_selectable = self.local_table
for mapper in self.iterate_to_root():
if mapper.polymorphic_on is not None:
mapper._requires_row_aliasing = True
@@ -1011,19 +1032,19 @@ class Mapper(InspectionAttr):
self.inherit_condition = sql_util.join_condition(
self.inherits.local_table, self.local_table
)
- self.mapped_table = sql.join(
- self.inherits.mapped_table,
+ self.persist_selectable = sql.join(
+ self.inherits.persist_selectable,
self.local_table,
self.inherit_condition,
)
fks = util.to_set(self.inherit_foreign_keys)
self._inherits_equated_pairs = sql_util.criterion_as_pairs(
- self.mapped_table.onclause,
+ self.persist_selectable.onclause,
consider_as_foreign_keys=fks,
)
else:
- self.mapped_table = self.local_table
+ self.persist_selectable = self.local_table
if self.polymorphic_identity is not None and not self.concrete:
self._identity_class = self.inherits._identity_class
@@ -1099,14 +1120,15 @@ class Mapper(InspectionAttr):
else:
self._all_tables = set()
self.base_mapper = self
- self.mapped_table = self.local_table
+ self.persist_selectable = self.local_table
if self.polymorphic_identity is not None:
self.polymorphic_map[self.polymorphic_identity] = self
self._identity_class = self.class_
- if self.mapped_table is None:
+ if self.persist_selectable is None:
raise sa_exc.ArgumentError(
- "Mapper '%s' does not have a mapped_table specified." % self
+ "Mapper '%s' does not have a persist_selectable specified."
+ % self
)
def _set_with_polymorphic(self, with_polymorphic):
@@ -1331,7 +1353,7 @@ class Mapper(InspectionAttr):
instrumentation.unregister_class(self.class_)
def _configure_pks(self):
- self.tables = sql_util.find_tables(self.mapped_table)
+ self.tables = sql_util.find_tables(self.persist_selectable)
self._pks_by_table = {}
self._cols_by_table = {}
@@ -1343,7 +1365,7 @@ class Mapper(InspectionAttr):
pk_cols = util.column_set(c for c in all_cols if c.primary_key)
# identify primary key columns which are also mapped by this mapper.
- tables = set(self.tables + [self.mapped_table])
+ tables = set(self.tables + [self.persist_selectable])
self._all_tables.update(tables)
for t in tables:
if t.primary_key and pk_cols.issuperset(t.primary_key):
@@ -1366,13 +1388,13 @@ class Mapper(InspectionAttr):
# otherwise, see that we got a full PK for the mapped table
elif (
- self.mapped_table not in self._pks_by_table
- or len(self._pks_by_table[self.mapped_table]) == 0
+ self.persist_selectable not in self._pks_by_table
+ or len(self._pks_by_table[self.persist_selectable]) == 0
):
raise sa_exc.ArgumentError(
"Mapper %s could not assemble any primary "
"key columns for mapped table '%s'"
- % (self, self.mapped_table.description)
+ % (self, self.persist_selectable.description)
)
elif self.local_table not in self._pks_by_table and isinstance(
self.local_table, schema.Table
@@ -1393,19 +1415,19 @@ class Mapper(InspectionAttr):
# that of the inheriting (unless concrete or explicit)
self.primary_key = self.inherits.primary_key
else:
- # determine primary key from argument or mapped_table pks -
+ # determine primary key from argument or persist_selectable pks -
# reduce to the minimal set of columns
if self._primary_key_argument:
primary_key = sql_util.reduce_columns(
[
- self.mapped_table.corresponding_column(c)
+ self.persist_selectable.corresponding_column(c)
for c in self._primary_key_argument
],
ignore_nonexistent_tables=True,
)
else:
primary_key = sql_util.reduce_columns(
- self._pks_by_table[self.mapped_table],
+ self._pks_by_table[self.persist_selectable],
ignore_nonexistent_tables=True,
)
@@ -1413,7 +1435,7 @@ class Mapper(InspectionAttr):
raise sa_exc.ArgumentError(
"Mapper %s could not assemble any primary "
"key columns for mapped table '%s'"
- % (self, self.mapped_table.description)
+ % (self, self.persist_selectable.description)
)
self.primary_key = tuple(primary_key)
@@ -1458,7 +1480,7 @@ class Mapper(InspectionAttr):
# create properties for each column in the mapped table,
# for those columns which don't already map to a property
- for column in self.mapped_table.columns:
+ for column in self.persist_selectable.columns:
if column in self._columntoproperty:
continue
@@ -1539,8 +1561,8 @@ class Mapper(InspectionAttr):
# doesn't appear to be mapped. this means it can be 1.
# only present in the with_polymorphic selectable or
# 2. a totally standalone SQL expression which we'd
- # hope is compatible with this mapper's mapped_table
- col = self.mapped_table.corresponding_column(
+ # hope is compatible with this mapper's persist_selectable
+ col = self.persist_selectable.corresponding_column(
self.polymorphic_on
)
if col is None:
@@ -1550,7 +1572,7 @@ class Mapper(InspectionAttr):
# for it. Just check that if it's directly a
# schema.Column and we have with_polymorphic, it's
# likely a user error if the schema.Column isn't
- # represented somehow in either mapped_table or
+ # represented somehow in either persist_selectable or
# with_polymorphic. Otherwise as of 0.7.4 we
# just go with it and assume the user wants it
# that way (i.e. a CASE statement)
@@ -1607,12 +1629,12 @@ class Mapper(InspectionAttr):
# table is the same as the parent (i.e. single table
# inheritance), we can use it
if mapper.polymorphic_on is not None:
- if self.mapped_table is mapper.mapped_table:
+ if self.persist_selectable is mapper.persist_selectable:
self.polymorphic_on = mapper.polymorphic_on
else:
self.polymorphic_on = (
- self.mapped_table.corresponding_column
- )(mapper.polymorphic_on)
+ self.persist_selectable
+ ).corresponding_column(mapper.polymorphic_on)
# we can use the parent mapper's _set_polymorphic_identity
# directly; it ensures the polymorphic_identity of the
# instance's mapper is used so is portable to subclasses.
@@ -1674,7 +1696,7 @@ class Mapper(InspectionAttr):
stack = deque([self])
while stack:
item = stack.popleft()
- if item.mapped_table is self.mapped_table:
+ if item.persist_selectable is self.persist_selectable:
identities.add(item.polymorphic_identity)
stack.extend(item._inheriting_mappers)
@@ -1717,7 +1739,7 @@ class Mapper(InspectionAttr):
prop = self._property_from_column(key, prop)
if isinstance(prop, properties.ColumnProperty):
- col = self.mapped_table.corresponding_column(prop.columns[0])
+ col = self.persist_selectable.corresponding_column(prop.columns[0])
# if the column is not present in the mapped table,
# test if a column has been added after the fact to the
@@ -1728,8 +1750,8 @@ class Mapper(InspectionAttr):
col = m.local_table.corresponding_column(prop.columns[0])
if col is not None:
for m2 in path:
- m2.mapped_table._reset_exported()
- col = self.mapped_table.corresponding_column(
+ m2.persist_selectable._reset_exported()
+ col = self.persist_selectable.corresponding_column(
prop.columns[0]
)
break
@@ -1874,7 +1896,7 @@ class Mapper(InspectionAttr):
):
mapped_column = []
for c in columns:
- mc = self.mapped_table.corresponding_column(c)
+ mc = self.persist_selectable.corresponding_column(c)
if mc is None:
mc = self.local_table.corresponding_column(c)
if mc is not None:
@@ -1882,8 +1904,8 @@ class Mapper(InspectionAttr):
# mapped table, this corresponds to adding a
# column after the fact to the local table.
# [ticket:1523]
- self.mapped_table._reset_exported()
- mc = self.mapped_table.corresponding_column(c)
+ self.persist_selectable._reset_exported()
+ mc = self.persist_selectable.corresponding_column(c)
if mc is None:
raise sa_exc.ArgumentError(
"When configuring property '%s' on %s, "
@@ -2077,7 +2099,7 @@ class Mapper(InspectionAttr):
mapped tables.
"""
- from_obj = self.mapped_table
+ from_obj = self.persist_selectable
for m in mappers:
if m is self:
continue
@@ -2118,7 +2140,7 @@ class Mapper(InspectionAttr):
@_memoized_configured_property
def _with_polymorphic_selectable(self):
if not self.with_polymorphic:
- return self.mapped_table
+ return self.persist_selectable
spec, selectable = self.with_polymorphic
if selectable is not None:
@@ -2243,7 +2265,7 @@ class Mapper(InspectionAttr):
"""The :func:`.select` construct this :class:`.Mapper` selects from
by default.
- Normally, this is equivalent to :attr:`.mapped_table`, unless
+ Normally, this is equivalent to :attr:`.persist_selectable`, unless
the ``with_polymorphic`` feature is in use, in which case the
full "polymorphic" selectable is returned.
diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py
index 150347995..3bb36f1e2 100644
--- a/lib/sqlalchemy/orm/query.py
+++ b/lib/sqlalchemy/orm/query.py
@@ -211,7 +211,7 @@ class Query(object):
and ext_info.mapper.with_polymorphic
):
if (
- ext_info.mapper.mapped_table
+ ext_info.mapper.persist_selectable
not in self._polymorphic_adapters
):
self._mapper_loads_polymorphically_with(
@@ -2304,7 +2304,7 @@ class Query(object):
if of_type:
right = of_type
else:
- right = onclause.property.mapper
+ right = onclause.property.entity
left = onclause._parententity
@@ -2610,7 +2610,7 @@ class Query(object):
# be more liberal about auto-aliasing.
if right_mapper and (
right_mapper.with_polymorphic
- or isinstance(right_mapper.mapped_table, expression.Join)
+ or isinstance(right_mapper.persist_selectable, expression.Join)
):
for from_obj in self._from_obj or [l_info.selectable]:
if sql_util.selectables_overlap(
@@ -2669,13 +2669,13 @@ class Query(object):
# as the ON clause
if not right_selectable.is_derived_from(
- right_mapper.mapped_table
+ right_mapper.persist_selectable
):
raise sa_exc.InvalidRequestError(
"Selectable '%s' is not derived from '%s'"
% (
right_selectable.description,
- right_mapper.mapped_table.description,
+ right_mapper.persist_selectable.description,
)
)
@@ -4660,7 +4660,7 @@ class AliasOption(interfaces.MapperOption):
def process_query(self, query):
if isinstance(self.alias, util.string_types):
- alias = query._mapper_zero().mapped_table.alias(self.alias)
+ alias = query._mapper_zero().persist_selectable.alias(self.alias)
else:
alias = self.alias
query._from_obj_alias = sql_util.ColumnAdapter(alias)
diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py
index be2093fb9..530a9bd89 100644
--- a/lib/sqlalchemy/orm/relationships.py
+++ b/lib/sqlalchemy/orm/relationships.py
@@ -944,6 +944,20 @@ class RelationshipProperty(StrategizedProperty):
)
@util.memoized_property
+ def entity(self):
+ """The target entity referred to by this
+ :class:`.RelationshipProperty.Comparator`.
+
+ This is either a :class:`.Mapper` or :class:`.AliasedInsp`
+ object.
+
+ This is the "target" or "remote" side of the
+ :func:`.relationship`.
+
+ """
+ return self.property.entity
+
+ @util.memoized_property
def mapper(self):
"""The target :class:`.Mapper` referred to by this
:class:`.RelationshipProperty.Comparator`.
@@ -967,9 +981,9 @@ class RelationshipProperty(StrategizedProperty):
def __clause_element__(self):
adapt_from = self._source_selectable()
if self._of_type:
- of_type = inspect(self._of_type).mapper
+ of_type_mapper = inspect(self._of_type).mapper
else:
- of_type = None
+ of_type_mapper = None
(
pj,
@@ -981,7 +995,7 @@ class RelationshipProperty(StrategizedProperty):
) = self.property._create_joins(
source_selectable=adapt_from,
source_polymorphic=True,
- of_type=of_type,
+ of_type_mapper=of_type_mapper,
)
if sj is not None:
return pj & sj
@@ -1795,11 +1809,9 @@ class RelationshipProperty(StrategizedProperty):
)
@util.memoized_property
- def mapper(self):
- """Return the targeted :class:`.Mapper` for this
- :class:`.RelationshipProperty`.
-
- This is a lazy-initializing static attribute.
+ def entity(self): # type: () -> Union[AliasedInsp, Mapper]
+ """Return the target mapped entity, which is an inspect() of the
+ class or aliased class tha is referred towards.
"""
if util.callable(self.argument) and not isinstance(
@@ -1810,25 +1822,31 @@ class RelationshipProperty(StrategizedProperty):
argument = self.argument
if isinstance(argument, type):
- mapper_ = mapperlib.class_mapper(argument, configure=False)
- elif isinstance(self.argument, mapperlib.Mapper):
- mapper_ = argument
+ return mapperlib.class_mapper(argument, configure=False)
+
+ try:
+ entity = inspect(argument)
+ except sa_exc.NoInspectionAvailable:
+ pass
else:
- raise sa_exc.ArgumentError(
- "relationship '%s' expects "
- "a class or a mapper argument (received: %s)"
- % (self.key, type(argument))
- )
- return mapper_
+ if hasattr(entity, "mapper"):
+ return entity
+
+ raise sa_exc.ArgumentError(
+ "relationship '%s' expects "
+ "a class or a mapper argument (received: %s)"
+ % (self.key, type(argument))
+ )
@util.memoized_property
- @util.deprecated("0.7", "Use .target")
- def table(self):
- """Return the selectable linked to this
- :class:`.RelationshipProperty` object's target
- :class:`.Mapper`.
+ def mapper(self):
+ """Return the targeted :class:`.Mapper` for this
+ :class:`.RelationshipProperty`.
+
+ This is a lazy-initializing static attribute.
+
"""
- return self.target
+ return self.entity.mapper
def do_init(self):
self._check_conflicts()
@@ -1894,14 +1912,14 @@ class RelationshipProperty(StrategizedProperty):
for x in util.to_column_set(self.remote_side)
)
- self.target = self.mapper.mapped_table
+ self.target = self.entity.persist_selectable
def _setup_join_conditions(self):
self._join_condition = jc = JoinCondition(
- parent_selectable=self.parent.mapped_table,
- child_selectable=self.mapper.mapped_table,
+ parent_persist_selectable=self.parent.persist_selectable,
+ child_persist_selectable=self.entity.persist_selectable,
parent_local_selectable=self.parent.local_table,
- child_local_selectable=self.mapper.local_table,
+ child_local_selectable=self.entity.local_table,
primaryjoin=self.primaryjoin,
secondary=self.secondary,
secondaryjoin=self.secondaryjoin,
@@ -2016,7 +2034,7 @@ class RelationshipProperty(StrategizedProperty):
and self.secondary.c.contains_column(c)
):
continue
- if not self.parent.mapped_table.c.contains_column(
+ if not self.parent.persist_selectable.c.contains_column(
c
) and not self.target.c.contains_column(c):
return False
@@ -2124,7 +2142,7 @@ class RelationshipProperty(StrategizedProperty):
source_selectable=None,
dest_polymorphic=False,
dest_selectable=None,
- of_type=None,
+ of_type_mapper=None,
):
if source_selectable is None:
if source_polymorphic and self.parent.with_polymorphic:
@@ -2132,11 +2150,9 @@ class RelationshipProperty(StrategizedProperty):
aliased = False
if dest_selectable is None:
+ dest_selectable = self.entity.selectable
if dest_polymorphic and self.mapper.with_polymorphic:
- dest_selectable = self.mapper._with_polymorphic_selectable
aliased = True
- else:
- dest_selectable = self.mapper.mapped_table
if self._is_self_referential and source_selectable is None:
dest_selectable = dest_selectable.alias()
@@ -2144,7 +2160,7 @@ class RelationshipProperty(StrategizedProperty):
else:
aliased = True
- dest_mapper = of_type or self.mapper
+ dest_mapper = of_type_mapper or self.mapper
single_crit = dest_mapper._single_table_criterion
aliased = aliased or (source_selectable is not None)
@@ -2161,7 +2177,7 @@ class RelationshipProperty(StrategizedProperty):
if source_selectable is None:
source_selectable = self.parent.local_table
if dest_selectable is None:
- dest_selectable = self.mapper.local_table
+ dest_selectable = self.entity.local_table
return (
primaryjoin,
secondaryjoin,
@@ -2187,8 +2203,8 @@ def _annotate_columns(element, annotations):
class JoinCondition(object):
def __init__(
self,
- parent_selectable,
- child_selectable,
+ parent_persist_selectable,
+ child_persist_selectable,
parent_local_selectable,
child_local_selectable,
primaryjoin=None,
@@ -2204,9 +2220,9 @@ class JoinCondition(object):
support_sync=True,
can_be_synced_fn=lambda *c: True,
):
- self.parent_selectable = parent_selectable
+ self.parent_persist_selectable = parent_persist_selectable
self.parent_local_selectable = parent_local_selectable
- self.child_selectable = child_selectable
+ self.child_persist_selectable = child_persist_selectable
self.child_local_selectable = child_local_selectable
self.parent_equivalents = parent_equivalents
self.child_equivalents = child_equivalents
@@ -2317,14 +2333,14 @@ class JoinCondition(object):
if self.secondary is not None:
if self.secondaryjoin is None:
self.secondaryjoin = join_condition(
- self.child_selectable,
+ self.child_persist_selectable,
self.secondary,
a_subset=self.child_local_selectable,
consider_as_foreign_keys=consider_as_foreign_keys,
)
if self.primaryjoin is None:
self.primaryjoin = join_condition(
- self.parent_selectable,
+ self.parent_persist_selectable,
self.secondary,
a_subset=self.parent_local_selectable,
consider_as_foreign_keys=consider_as_foreign_keys,
@@ -2332,8 +2348,8 @@ class JoinCondition(object):
else:
if self.primaryjoin is None:
self.primaryjoin = join_condition(
- self.parent_selectable,
- self.child_selectable,
+ self.parent_persist_selectable,
+ self.child_persist_selectable,
a_subset=self.parent_local_selectable,
consider_as_foreign_keys=consider_as_foreign_keys,
)
@@ -2519,8 +2535,8 @@ class JoinCondition(object):
comparisons where both columns are in both tables.
"""
- pt = self.parent_selectable
- mt = self.child_selectable
+ pt = self.parent_persist_selectable
+ mt = self.child_persist_selectable
result = [False]
def visit_binary(binary):
@@ -2542,7 +2558,7 @@ class JoinCondition(object):
"""Return True if parent/child tables have some overlap."""
return selectables_overlap(
- self.parent_selectable, self.child_selectable
+ self.parent_persist_selectable, self.child_persist_selectable
)
def _annotate_remote(self):
@@ -2661,9 +2677,9 @@ class JoinCondition(object):
if isinstance(left, expression.ColumnClause) and isinstance(
right, expression.ColumnClause
):
- if self.child_selectable.c.contains_column(
+ if self.child_persist_selectable.c.contains_column(
right
- ) and self.parent_selectable.c.contains_column(left):
+ ) and self.parent_persist_selectable.c.contains_column(left):
right = right._annotate({"remote": True})
elif (
check_entities
@@ -2692,7 +2708,7 @@ class JoinCondition(object):
"""
def repl(element):
- if self.child_selectable.c.contains_column(element) and (
+ if self.child_persist_selectable.c.contains_column(element) and (
not self.parent_local_selectable.c.contains_column(element)
or self.child_local_selectable.c.contains_column(element)
):
@@ -2728,7 +2744,7 @@ class JoinCondition(object):
[l for (l, r) in self._local_remote_pairs]
)
else:
- local_side = util.column_set(self.parent_selectable.c)
+ local_side = util.column_set(self.parent_persist_selectable.c)
def locals_(elem):
if "remote" not in elem._annotations and elem in local_side:
@@ -2839,8 +2855,8 @@ class JoinCondition(object):
if self.secondaryjoin is not None:
self.direction = MANYTOMANY
else:
- parentcols = util.column_set(self.parent_selectable.c)
- targetcols = util.column_set(self.child_selectable.c)
+ parentcols = util.column_set(self.parent_persist_selectable.c)
+ targetcols = util.column_set(self.child_persist_selectable.c)
# fk collection which suggests ONETOMANY.
onetomany_fk = targetcols.intersection(self.foreign_key_columns)
diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py
index 53f99b99d..a4cc07194 100644
--- a/lib/sqlalchemy/orm/session.py
+++ b/lib/sqlalchemy/orm/session.py
@@ -1507,7 +1507,7 @@ class Session(_SessionClassMethods):
if cls in self.__binds:
return self.__binds[cls]
if clause is None:
- clause = mapper.mapped_table
+ clause = mapper.persist_selectable
if clause is not None:
for t in sql_util.find_tables(clause, include_crud=True):
@@ -1520,8 +1520,8 @@ class Session(_SessionClassMethods):
if isinstance(clause, sql.expression.ClauseElement) and clause.bind:
return clause.bind
- if mapper and mapper.mapped_table.bind:
- return mapper.mapped_table.bind
+ if mapper and mapper.persist_selectable.bind:
+ return mapper.persist_selectable.bind
context = []
if mapper is not None:
diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py
index f6862a402..3e7372fac 100644
--- a/lib/sqlalchemy/orm/strategies.py
+++ b/lib/sqlalchemy/orm/strategies.py
@@ -140,7 +140,7 @@ class UninstrumentedColumnLoader(LoaderStrategy):
def setup_query(
self,
context,
- entity,
+ query_entity,
path,
loadopt,
adapter,
@@ -173,7 +173,7 @@ class ColumnLoader(LoaderStrategy):
def setup_query(
self,
context,
- entity,
+ query_entity,
path,
loadopt,
adapter,
@@ -235,7 +235,7 @@ class ExpressionColumnLoader(ColumnLoader):
def setup_query(
self,
context,
- entity,
+ query_entity,
path,
loadopt,
adapter,
@@ -335,7 +335,7 @@ class DeferredColumnLoader(LoaderStrategy):
def setup_query(
self,
context,
- entity,
+ query_entity,
path,
loadopt,
adapter,
@@ -366,7 +366,7 @@ class DeferredColumnLoader(LoaderStrategy):
(("deferred", False), ("instrument", True))
).setup_query(
context,
- entity,
+ query_entity,
path,
loadopt,
adapter,
@@ -440,11 +440,12 @@ class LoadDeferredColumns(object):
class AbstractRelationshipLoader(LoaderStrategy):
"""LoaderStratgies which deal with related objects."""
- __slots__ = "mapper", "target", "uselist"
+ __slots__ = "mapper", "target", "uselist", "entity"
def __init__(self, parent, strategy_key):
super(AbstractRelationshipLoader, self).__init__(parent, strategy_key)
self.mapper = self.parent_property.mapper
+ self.entity = self.parent_property.entity
self.target = self.parent_property.target
self.uselist = self.parent_property.uselist
@@ -510,6 +511,7 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots):
"_lazywhere",
"_rev_lazywhere",
"use_get",
+ "is_aliased_class",
"_bind_to_col",
"_equated_columns",
"_rev_bind_to_col",
@@ -525,6 +527,8 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots):
self._raise_always = self.strategy_opts["lazy"] == "raise"
self._raise_on_sql = self.strategy_opts["lazy"] == "raise_on_sql"
+ self.is_aliased_class = inspect(self.entity).is_aliased_class
+
join_condition = self.parent_property._join_condition
self._lazywhere, self._bind_to_col, self._equated_columns = (
join_condition.create_lazy_clause()
@@ -540,10 +544,14 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots):
# determine if our "lazywhere" clause is the same as the mapper's
# get() clause. then we can just use mapper.get()
- self.use_get = not self.uselist and self.mapper._get_clause[0].compare(
- self._lazywhere,
- use_proxies=True,
- equivalents=self.mapper._equivalent_columns,
+ self.use_get = (
+ not self.is_aliased_class
+ and not self.uselist
+ and self.entity._get_clause[0].compare(
+ self._lazywhere,
+ use_proxies=True,
+ equivalents=self.mapper._equivalent_columns,
+ )
)
if self.use_get:
@@ -693,7 +701,7 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots):
# does this, including how it decides what the correct
# identity_token would be for this identity.
instance = session.query()._identity_lookup(
- self.mapper,
+ self.entity,
primary_key_identity,
passive=passive,
lazy_loaded_from=state,
@@ -757,7 +765,7 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots):
# lazy loaders. Currently the LRU cache is local to the LazyLoader,
# however add ourselves to the initial cache key just to future
# proof in case it moves
- q = self._bakery(lambda session: session.query(self.mapper), self)
+ q = self._bakery(lambda session: session.query(self.entity), self)
q.add_criteria(
lambda q: q._adapt_all_clauses()._with_invoke_all_eagers(False),
@@ -992,7 +1000,7 @@ class SubqueryLoader(AbstractRelationshipLoader):
if with_poly_info is not None:
effective_entity = with_poly_info.entity
else:
- effective_entity = self.mapper
+ effective_entity = self.entity
subq_path = context.attributes.get(
("subquery_path", None), orm_util.PathRegistry.root
@@ -1422,7 +1430,7 @@ class JoinedLoader(AbstractRelationshipLoader):
def setup_query(
self,
context,
- entity,
+ query_entity,
path,
loadopt,
adapter,
@@ -1454,7 +1462,7 @@ class JoinedLoader(AbstractRelationshipLoader):
adapter,
add_to_collection,
) = self._setup_query_on_user_defined_adapter(
- context, entity, path, adapter, user_defined_adapter
+ context, query_entity, path, adapter, user_defined_adapter
)
else:
# if not via query option, check for
@@ -1473,7 +1481,7 @@ class JoinedLoader(AbstractRelationshipLoader):
chained_from_outerjoin,
) = self._generate_row_adapter(
context,
- entity,
+ query_entity,
path,
loadopt,
adapter,
@@ -1490,12 +1498,12 @@ class JoinedLoader(AbstractRelationshipLoader):
else:
with_polymorphic = None
- path = path[self.mapper]
+ path = path[self.entity]
loading._setup_entity_query(
context,
self.mapper,
- entity,
+ query_entity,
path,
clauses,
add_to_collection,
@@ -1536,11 +1544,6 @@ class JoinedLoader(AbstractRelationshipLoader):
root_mapper, prop = path[-2:]
- # from .mapper import Mapper
- # from .interfaces import MapperProperty
- # assert isinstance(root_mapper, Mapper)
- # assert isinstance(prop, MapperProperty)
-
if alias is not None:
if isinstance(alias, str):
alias = prop.target.alias(alias)
@@ -1597,6 +1600,11 @@ class JoinedLoader(AbstractRelationshipLoader):
# we need one unique AliasedClass per query per appearance of our
# entity in the query.
+ if inspect(self.entity).is_aliased_class:
+ alt_selectable = inspect(self.entity).selectable
+ else:
+ alt_selectable = None
+
key = ("joinedloader_ac", self)
if key not in context.attributes:
context.attributes[key] = idx = 0
@@ -1605,8 +1613,14 @@ class JoinedLoader(AbstractRelationshipLoader):
if idx >= len(self._aliased_class_pool):
to_adapt = orm_util.AliasedClass(
- self.mapper, flat=True, use_mapper_path=True
+ self.mapper,
+ alias=alt_selectable.alias(flat=True)
+ if alt_selectable is not None
+ else None,
+ flat=True,
+ use_mapper_path=True,
)
+
# load up the .columns collection on the Alias() before
# the object becomes shared among threads. this prevents
# races for column identities.
@@ -1683,7 +1697,7 @@ class JoinedLoader(AbstractRelationshipLoader):
def _create_eager_join(
self,
context,
- entity,
+ query_entity,
path,
adapter,
parentmapper,
@@ -1693,7 +1707,7 @@ class JoinedLoader(AbstractRelationshipLoader):
):
if parentmapper is None:
- localparent = entity.mapper
+ localparent = query_entity.mapper
else:
localparent = parentmapper
@@ -1705,24 +1719,24 @@ class JoinedLoader(AbstractRelationshipLoader):
and context.query._should_nest_selectable
)
- entity_key = None
+ query_entity_key = None
if (
- entity not in context.eager_joins
+ query_entity not in context.eager_joins
and not should_nest_selectable
and context.from_clause
):
indexes = sql_util.find_left_clause_that_matches_given(
- context.from_clause, entity.selectable
+ context.from_clause, query_entity.selectable
)
if len(indexes) > 1:
# for the eager load case, I can't reproduce this right
# now. For query.join() I can.
raise sa_exc.InvalidRequestError(
- "Can't identify which entity in which to joined eager "
- "load from. Please use an exact match when specifying "
- "the join path."
+ "Can't identify which query entity in which to joined "
+ "eager load from. Please use an exact match when "
+ "specifying the join path."
)
if indexes:
@@ -1731,12 +1745,17 @@ class JoinedLoader(AbstractRelationshipLoader):
# key it to its list index in the eager_joins dict.
# Query._compile_context will adapt as needed and
# append to the FROM clause of the select().
- entity_key, default_towrap = indexes[0], clause
+ query_entity_key, default_towrap = indexes[0], clause
- if entity_key is None:
- entity_key, default_towrap = entity, entity.selectable
+ if query_entity_key is None:
+ query_entity_key, default_towrap = (
+ query_entity,
+ query_entity.selectable,
+ )
- towrap = context.eager_joins.setdefault(entity_key, default_towrap)
+ towrap = context.eager_joins.setdefault(
+ query_entity_key, default_towrap
+ )
if adapter:
if getattr(adapter, "aliased_class", None):
@@ -1771,7 +1790,7 @@ class JoinedLoader(AbstractRelationshipLoader):
not chained_from_outerjoin
or not innerjoin
or innerjoin == "unnested"
- or entity.entity_zero.represents_outer_join
+ or query_entity.entity_zero.represents_outer_join
)
if attach_on_outside:
@@ -1781,7 +1800,7 @@ class JoinedLoader(AbstractRelationshipLoader):
clauses.aliased_class,
onclause,
isouter=not innerjoin
- or entity.entity_zero.represents_outer_join
+ or query_entity.entity_zero.represents_outer_join
or (chained_from_outerjoin and isinstance(towrap, sql.Join)),
_left_memo=self.parent,
_right_memo=self.mapper,
@@ -1792,10 +1811,10 @@ class JoinedLoader(AbstractRelationshipLoader):
path, towrap, clauses, onclause
)
- context.eager_joins[entity_key] = eagerjoin
+ context.eager_joins[query_entity_key] = eagerjoin
# send a hint to the Query as to where it may "splice" this join
- eagerjoin.stop_on = entity.selectable
+ eagerjoin.stop_on = query_entity.selectable
if not parentmapper:
# for parentclause that is the non-eager end of the join,
@@ -1808,7 +1827,7 @@ class JoinedLoader(AbstractRelationshipLoader):
for col in sql_util._find_columns(
self.parent_property.primaryjoin
):
- if localparent.mapped_table.c.contains_column(col):
+ if localparent.persist_selectable.c.contains_column(col):
if adapter:
col = adapter.columns[col]
context.primary_columns.append(col)
@@ -1938,7 +1957,7 @@ class JoinedLoader(AbstractRelationshipLoader):
self.mapper,
context,
result,
- our_path[self.mapper],
+ our_path[self.entity],
eager_adapter,
)
@@ -2145,7 +2164,7 @@ class SelectInLoader(AbstractRelationshipLoader, util.MemoizedSlots):
if with_poly_info is not None:
effective_entity = with_poly_info.entity
else:
- effective_entity = self.mapper
+ effective_entity = self.entity
if not path_w_prop.contains(context.attributes, "loader"):
if self.join_depth:
diff --git a/lib/sqlalchemy/orm/strategy_options.py b/lib/sqlalchemy/orm/strategy_options.py
index 6f9746daa..759efe9f5 100644
--- a/lib/sqlalchemy/orm/strategy_options.py
+++ b/lib/sqlalchemy/orm/strategy_options.py
@@ -211,6 +211,7 @@ class Load(Generative, MapperOption):
ent = inspect(existing_of_type)
else:
ent = path.entity
+
try:
# use getattr on the class to work around
# synonyms, hybrids, etc.
diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py
index 776278469..8873a6a72 100644
--- a/lib/sqlalchemy/orm/util.py
+++ b/lib/sqlalchemy/orm/util.py
@@ -394,21 +394,30 @@ class ORMAdapter(sql_util.ColumnAdapter):
class AliasedClass(object):
r"""Represents an "aliased" form of a mapped class for usage with Query.
- The ORM equivalent of a :func:`sqlalchemy.sql.expression.alias`
+ The ORM equivalent of a :func:`~sqlalchemy.sql.expression.alias`
construct, this object mimics the mapped class using a
- __getattr__ scheme and maintains a reference to a
+ ``__getattr__`` scheme and maintains a reference to a
real :class:`~sqlalchemy.sql.expression.Alias` object.
- Usage is via the :func:`.orm.aliased` function, or alternatively
- via the :func:`.orm.with_polymorphic` function.
-
- Usage example::
+ A primary purpose of :class:`.AliasedClass` is to serve as an alternate
+ within a SQL statement generated by the ORM, such that an existing
+ mapped entity can be used in multiple contexts. A simple example::
# find all pairs of users with the same name
user_alias = aliased(User)
session.query(User, user_alias).\
join((user_alias, User.id > user_alias.id)).\
- filter(User.name==user_alias.name)
+ filter(User.name == user_alias.name)
+
+ :class:`.AliasedClass` is also capable of mapping an existing mapped
+ class to an entirely new selectable, provided this selectable is column-
+ compatible with the existing mapped selectable, and it can also be
+ configured in a mapping as the target of a :func:`.relationship`.
+ See the links below for examples.
+
+ The :class:`.AliasedClass` object is constructed typically using the
+ :func:`.orm.aliased` function. It also is produced with additional
+ configuration when using the :func:`.orm.with_polymorphic` function.
The resulting object is an instance of :class:`.AliasedClass`.
This object implements an attribute scheme which produces the
@@ -427,8 +436,17 @@ class AliasedClass(object):
The resulting inspection object is an instance of :class:`.AliasedInsp`.
- See :func:`.aliased` and :func:`.with_polymorphic` for construction
- argument descriptions.
+
+ .. seealso::
+
+ :func:`.aliased`
+
+ :func:`.with_polymorphic`
+
+ :ref:`relationship_aliased_class`
+
+ :ref:`relationship_to_window_function`
+
"""
@@ -564,7 +582,9 @@ class AliasedInsp(InspectionAttr):
):
self.entity = entity
self.mapper = mapper
- self.selectable = selectable
+ self.selectable = (
+ self.persist_selectable
+ ) = self.local_table = selectable
self.name = name
self.with_polymorphic_mappers = with_polymorphic_mappers
self.polymorphic_on = polymorphic_on
@@ -660,6 +680,17 @@ class AliasedInsp(InspectionAttr):
assert False, "mapper %s doesn't correspond to %s" % (mapper, self)
@util.memoized_property
+ def _get_clause(self):
+ onclause, replacemap = self.mapper._get_clause
+ return (
+ self._adapter.traverse(onclause),
+ {
+ self._adapter.traverse(col): param
+ for col, param in replacemap.items()
+ },
+ )
+
+ @util.memoized_property
def _memoized_values(self):
return {}
@@ -970,7 +1001,7 @@ class _ORMJoin(expression.Join):
dest_selectable=adapt_to,
source_polymorphic=True,
dest_polymorphic=True,
- of_type=right_info.mapper,
+ of_type_mapper=right_info.mapper,
)
if sj is not None: