diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2018-12-29 20:54:29 -0500 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2019-01-25 09:28:28 -0500 |
| commit | 93855ed623ceedffc02dee06c9a46c37dd26286b (patch) | |
| tree | 5fdb783ea642d99693fd284dcbc04d4691423d16 /lib | |
| parent | 78e598e3a5b8df7419a600c291f90260e598c9b7 (diff) | |
| download | sqlalchemy-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.py | 10 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/descriptor_props.py | 17 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/interfaces.py | 12 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/mapper.py | 116 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/query.py | 12 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/relationships.py | 118 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/session.py | 6 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/strategies.py | 105 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/strategy_options.py | 1 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/util.py | 53 |
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: |
