From: Mike Bayer Date: Sun, 30 Dec 2018 01:54:29 +0000 (-0500) Subject: Implement relationship to AliasedClass; deprecate non primary mappers X-Git-Tag: rel_1_3_0b2~9^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=93855ed623ceedffc02dee06c9a46c37dd26286b;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git 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 --- diff --git a/doc/build/changelog/migration_13.rst b/doc/build/changelog/migration_13.rst index 94e7c857a7..bf75252723 100644 --- a/doc/build/changelog/migration_13.rst +++ b/doc/build/changelog/migration_13.rst @@ -72,13 +72,98 @@ below. :ref:`change_4393_convertunicode` - :ref:`FIXME` - FIXME - link to non-primary mapper deprecation + :ref:`change_4423` :ticket:`4393` New Features and Improvements - ORM =================================== +.. _change_4423: + +Relationship to AliasedClass replaces the need for non primary mappers +----------------------------------------------------------------------- + +The "non primary mapper" is a :func:`.mapper` created in the +:ref:`classical_mapping` style, which acts as an additional mapper against an +already mapped class against a different kind of selectable. The non primary +mapper has its roots in the 0.1, 0.2 series of SQLAlchemy where it was +anticipated that the :func:`.mapper` object was to be the primary query +construction interface, before the :class:`.Query` object existed. + +With the advent of :class:`.Query` and later the :class:`.AliasedClass` +construct, most use cases for the non primary mapper went away. This was a +good thing since SQLAlchemy also moved away from "classical" mappings altogether +around the 0.5 series in favor of the declarative system. + +One use case remained around for non primary mappers when it was realized that +some very hard-to-define :func:`.relationship` configurations could be made +possible when a non-primary mapper with an alternative selectable was made as +the mapping target, rather than trying to construct a +:paramref:`.relationship.primaryjoin` that encompassed all the complexity of a +particular inter-object relationship. + +As this use case became more popular, its limitations became apparent, +including that the non primary mapper is difficult to configure against a +selectable that adds new columns, that the mapper does not inherit the +relationships of the original mapping, that relationships which are configured +explicitly on the non primary mapper do not function well with loader options, +and that the non primary mapper also doesn't provide a fully functional +namespace of column-based attributes which can be used in queries (which again, +in the old 0.1 - 0.4 days, one would use :class:`.Table` objects directly with +the ORM). + +The missing piece was to allow the :func:`.relationship` to refer directly +to the :class:`.AliasedClass`. The :class:`.AliasedClass` already does +everything we want the non primary mapper to do; it allows an existing mapped +class to be loaded from an alternative selectable, it inherits all the +attributes and relationships of the existing mapper, it works +extremely well with loader options, and it provides a class-like +object that can be mixed into queries just like the class itself. +With this change, the recipes that +were formerly for non primary mappers at :ref:`relationship_configure_joins` +are changed to aliased class. + +At :ref:`relationship_aliased_class`, the original non primary mapper looked +like:: + + j = join(B, D, D.b_id == B.id).join(C, C.id == D.c_id) + + B_viacd = mapper( + B, j, non_primary=True, primary_key=[j.c.b_id], + properties={ + "id": j.c.b_id, # so that 'id' looks the same as before + "c_id": j.c.c_id, # needed for disambiguation + "d_c_id": j.c.d_c_id, # needed for disambiguation + "b_id": [j.c.b_id, j.c.d_b_id], + "d_id": j.c.d_id, + } + ) + + A.b = relationship(B_viacd, primaryjoin=A.b_id == B_viacd.c.b_id) + +The properties were necessary in order to re-map the additional columns +so that they did not conflict with the existing columns mapped to ``B``, as +well as it was necessary to define a new primary key. + +With the new approach, all of this verbosity goes away, and the additional +columns are referred towards directly when making the relationship:: + + j = join(B, D, D.b_id == B.id).join(C, C.id == D.c_id) + + B_viacd = aliased(B, j, flat=True) + + A.b = relationship(B_viacd, primaryjoin=A.b_id == j.c.b_id) + +The non primary mapper is now deprecated with the eventual goal to be that +classical mappings as a feature go away entirely. The Declarative API would +become the single means of mapping which hopefully will allow internal +improvements and simplifications, as well as a clearer documentation story. + + +:ticket:`4423` + + .. _change_4340: selectin loading no longer uses JOIN for simple one-to-many diff --git a/doc/build/changelog/unreleased_13/4423.rst b/doc/build/changelog/unreleased_13/4423.rst new file mode 100644 index 0000000000..7a03fb77d5 --- /dev/null +++ b/doc/build/changelog/unreleased_13/4423.rst @@ -0,0 +1,14 @@ +.. change:: + :tags: feature, orm + :tickets: 4423 + + 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. + + .. seealso:: + + :ref:`change_4423` \ No newline at end of file diff --git a/doc/build/orm/join_conditions.rst b/doc/build/orm/join_conditions.rst index f54d2c0495..62da15aa88 100644 --- a/doc/build/orm/join_conditions.rst +++ b/doc/build/orm/join_conditions.rst @@ -634,11 +634,19 @@ complexity is kept within the middle. as well as support within declarative to specify complex conditions such as joins involving class names as targets. -.. _relationship_non_primary_mapper: +.. _relationship_aliased_class: -Relationship to Non Primary Mapper +Relationship to Aliased Class ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. versionadded:: 1.3 + The :class:`.AliasedClass` construct can now be specified as the + target of a :func:`.relationship`, replacing the previous approach + of using non-primary mappers, which had limitations such that they did + not inherit sub-relationships of the mapped entity as well as that they + required complex configuration against an alternate selectable. The + recipes in this section are now updated to use :class:`.AliasedClass`. + In the previous section, we illustrated a technique where we used :paramref:`~.relationship.secondary` in order to place additional tables within a join condition. There is one complex join case where @@ -648,15 +656,17 @@ however there are also join conditions between ``A`` and ``B`` *directly*. In this case, the join from ``A`` to ``B`` may be difficult to express with just a complex :paramref:`~.relationship.primaryjoin` condition, as the intermediary -tables may need special handling, and it is also not expressable with +tables may need special handling, and it is also not expressible with a :paramref:`~.relationship.secondary` object, since the ``A->secondary->B`` pattern does not support any references between ``A`` and ``B`` directly. When this **extremely advanced** case arises, we can resort to creating a second mapping as a target for the -relationship. This is where we use :func:`.mapper` in order to make a +relationship. This is where we use :class:`.AliasedClass` in order to make a mapping to a class that includes all the additional tables we need for this join. In order to produce this mapper as an "alternative" mapping -for our class, we use the :paramref:`~.mapper.non_primary` flag. +for our class, we use the :func:`.aliased` function to produce the new +construct, then use :func:`.relationship` against the object as though it +were a plain mapped class. Below illustrates a :func:`.relationship` with a simple join from ``A`` to ``B``, however the primaryjoin condition is augmented with two additional @@ -691,26 +701,12 @@ the rows in both ``A`` and ``B`` simultaneously:: # to it in the mapping multiple times. j = join(B, D, D.b_id == B.id).join(C, C.id == D.c_id) - # 2. Create a new mapper() to B, with non_primary=True. - # Columns in the join with the same name must be - # disambiguated within the mapping, using named properties. - # we also have to make sure the primary key of the regular "B" - # mapping is maintained. - B_viacd = mapper( - B, j, non_primary=True, primary_key=[j.c.b_id], - properties={ - "id": j.c.b_id, # so that 'id' looks the same as before - "c_id": j.c.c_id, # needed for disambiguation - "d_c_id": j.c.d_c_id, # needed for disambiguation - "b_id": [j.c.b_id, j.c.d_b_id], - "d_id": j.c.d_id, - } - ) + # 2. Create an AliasedClass to B + B_viacd = aliased(B, j, flat=True) - A.b = relationship(B_viacd, primaryjoin=A.b_id == B_viacd.c.b_id) + A.b = relationship(B_viacd, primaryjoin=A.b_id == j.c.b_id) -In the above case, our non-primary mapper for ``B`` will emit for -additional columns when we query; these can be ignored: +With the above mapping, a simple join looks like: .. sourcecode:: python+sql @@ -719,10 +715,13 @@ additional columns when we query; these can be ignored: {opensql}SELECT a.id AS a_id, a.b_id AS a_b_id FROM a JOIN (b JOIN d ON d.b_id = b.id JOIN c ON c.id = d.c_id) ON a.b_id = b.id +.. _relationship_to_window_function: + Row-Limited Relationships with Window Functions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Another interesting use case for non-primary mappers are situations where +Another interesting use case for relationships to :class:`.AliasedClass` +objects are situations where the relationship needs to join to a specialized SELECT of any form. One scenario is when the use of a window function is desired, such as to limit how many rows should be returned for a relationship. The example below @@ -747,11 +746,11 @@ ten items for each collection:: ).label('index') ]).alias() - partitioned_b = mapper(B, partition, non_primary=True) + partitioned_b = aliased(B, partition) A.partitioned_bs = relationship( partitioned_b, - primaryjoin=and_(partitioned_b.c.a_id == A.id, partitioned_b.c.index < 10) + primaryjoin=and_(partitioned_b.a_id == A.id, partition.c.index < 10) ) We can use the above ``partitioned_bs`` relationship with most of the loader diff --git a/lib/sqlalchemy/ext/declarative/base.py b/lib/sqlalchemy/ext/declarative/base.py index 269cfce008..80cd23bc8d 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 c1e5866b5f..a0ddf3a8b6 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 02eed516b2..87b4cfcde8 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 0c8ab0b10e..ff5148ed1c 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 1503479951..3bb36f1e26 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 be2093fb9a..530a9bd89e 100644 --- a/lib/sqlalchemy/orm/relationships.py +++ b/lib/sqlalchemy/orm/relationships.py @@ -943,6 +943,20 @@ class RelationshipProperty(StrategizedProperty): of_type=self._of_type, ) + @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 @@ -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 53f99b99d0..a4cc071941 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 f6862a4026..3e7372fac7 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 6f9746daaf..759efe9f50 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 7762784696..8873a6a72a 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 @@ -659,6 +679,17 @@ class AliasedInsp(InspectionAttr): else: 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: diff --git a/test/aaa_profiling/test_memusage.py b/test/aaa_profiling/test_memusage.py index 0915244f74..36f28c063f 100644 --- a/test/aaa_profiling/test_memusage.py +++ b/test/aaa_profiling/test_memusage.py @@ -321,8 +321,6 @@ class MemUsageWBackendTest(EnsureZeroed): ) m2 = mapper(B, table2) - m3 = mapper(A, table1, non_primary=True) - @profile_memory() def go(): sess = create_session() @@ -354,7 +352,7 @@ class MemUsageWBackendTest(EnsureZeroed): go() metadata.drop_all() - del m1, m2, m3 + del m1, m2 assert_no_mappers() def test_sessionmaker(self): @@ -415,8 +413,6 @@ class MemUsageWBackendTest(EnsureZeroed): ) m2 = mapper(B, table2, _compiled_cache_size=50) - m3 = mapper(A, table1, non_primary=True) - @profile_memory() def go(): engine = engines.testing_engine( @@ -458,7 +454,7 @@ class MemUsageWBackendTest(EnsureZeroed): go() metadata.drop_all() - del m1, m2, m3 + del m1, m2 assert_no_mappers() @testing.emits_warning("Compiled statement cache for.*") @@ -642,8 +638,6 @@ class MemUsageWBackendTest(EnsureZeroed): ) mapper(B, table2) - mapper(A, table1, non_primary=True) - sess = create_session() a1 = A(col2="a1") a2 = A(col2="a2") diff --git a/test/ext/test_deprecations.py b/test/ext/test_deprecations.py new file mode 100644 index 0000000000..099393cf77 --- /dev/null +++ b/test/ext/test_deprecations.py @@ -0,0 +1,37 @@ +from sqlalchemy import testing +from sqlalchemy.orm import mapper +from .test_mutable import Foo +from .test_mutable import ( + MutableAssociationScalarPickleTest as _MutableAssociationScalarPickleTest, +) +from .test_mutable import ( + MutableWithScalarJSONTest as _MutableWithScalarJSONTest, +) + + +class MutableIncludeNonPrimaryTest(_MutableWithScalarJSONTest): + @classmethod + def setup_mappers(cls): + foo = cls.tables.foo + + mapper(Foo, foo) + with testing.expect_deprecated( + "The mapper.non_primary parameter is deprecated" + ): + mapper( + Foo, foo, non_primary=True, properties={"foo_bar": foo.c.data} + ) + + +class MutableAssocIncludeNonPrimaryTest(_MutableAssociationScalarPickleTest): + @classmethod + def setup_mappers(cls): + foo = cls.tables.foo + + mapper(Foo, foo) + with testing.expect_deprecated( + "The mapper.non_primary parameter is deprecated" + ): + mapper( + Foo, foo, non_primary=True, properties={"foo_bar": foo.c.data} + ) diff --git a/test/ext/test_mutable.py b/test/ext/test_mutable.py index 9b8f761e9c..eae764d406 100644 --- a/test/ext/test_mutable.py +++ b/test/ext/test_mutable.py @@ -824,15 +824,6 @@ class MutableWithScalarJSONTest(_MutableDictTestBase, fixtures.MappedTest): self._test_non_mutable() -class MutableIncludeNonPrimaryTest(MutableWithScalarJSONTest): - @classmethod - def setup_mappers(cls): - foo = cls.tables.foo - - mapper(Foo, foo) - mapper(Foo, foo, non_primary=True, properties={"foo_bar": foo.c.data}) - - class MutableColumnCopyJSONTest(_MutableDictTestBase, fixtures.MappedTest): @classmethod def define_tables(cls, metadata): @@ -1013,15 +1004,6 @@ class MutableAssociationScalarPickleTest( ) -class MutableAssocIncludeNonPrimaryTest(MutableAssociationScalarPickleTest): - @classmethod - def setup_mappers(cls): - foo = cls.tables.foo - - mapper(Foo, foo) - mapper(Foo, foo, non_primary=True, properties={"foo_bar": foo.c.data}) - - class MutableAssociationScalarJSONTest( _MutableDictTestBase, fixtures.MappedTest ): diff --git a/test/orm/test_ac_relationships.py b/test/orm/test_ac_relationships.py new file mode 100644 index 0000000000..d5f9b013d8 --- /dev/null +++ b/test/orm/test_ac_relationships.py @@ -0,0 +1,292 @@ +from sqlalchemy import and_ +from sqlalchemy import Column +from sqlalchemy import ForeignKey +from sqlalchemy import func +from sqlalchemy import Integer +from sqlalchemy import join +from sqlalchemy import select +from sqlalchemy import testing +from sqlalchemy.orm import aliased +from sqlalchemy.orm import joinedload +from sqlalchemy.orm import noload +from sqlalchemy.orm import relationship +from sqlalchemy.orm import selectinload +from sqlalchemy.orm import Session +from sqlalchemy.testing import eq_ +from sqlalchemy.testing import fixtures +from sqlalchemy.testing.assertsql import CompiledSQL +from sqlalchemy.testing.fixtures import ComparableEntity + + +class PartitionByFixture(fixtures.DeclarativeMappedTest): + @classmethod + def setup_classes(cls): + Base = cls.DeclarativeBasic + + class A(Base): + __tablename__ = "a" + + id = Column(Integer, primary_key=True) + + class B(Base): + __tablename__ = "b" + id = Column(Integer, primary_key=True) + a_id = Column(ForeignKey("a.id")) + cs = relationship("C") + + class C(Base): + __tablename__ = "c" + id = Column(Integer, primary_key=True) + b_id = Column(ForeignKey("b.id")) + + partition = select( + [ + B, + func.row_number() + .over(order_by=B.id, partition_by=B.a_id) + .label("index"), + ] + ).alias() + + partitioned_b = aliased(B, alias=partition) + + A.partitioned_bs = relationship( + partitioned_b, + primaryjoin=and_( + partitioned_b.a_id == A.id, partition.c.index < 10 + ), + ) + + @classmethod + def insert_data(cls): + A, B, C = cls.classes("A", "B", "C") + + s = Session(testing.db) + s.add_all([A(id=i) for i in range(1, 4)]) + s.flush() + s.add_all( + [ + B(a_id=i, cs=[C(), C()]) + for i in range(1, 4) + for j in range(1, 21) + ] + ) + s.commit() + + +class AliasedClassRelationshipTest( + PartitionByFixture, testing.AssertsCompiledSQL +): + # TODO: maybe make this more backend agnostic + __requires__ = ("window_functions",) + __dialect__ = "default" + + def test_lazyload(self): + A, B, C = self.classes("A", "B", "C") + + s = Session(testing.db) + + def go(): + for a1 in s.query(A): # 1 query + eq_(len(a1.partitioned_bs), 9) # 3 queries + for b in a1.partitioned_bs: + eq_(len(b.cs), 2) # 9 * 3 = 27 queries + + self.assert_sql_count(testing.db, go, 31) + + def test_join_one(self): + A, B, C = self.classes("A", "B", "C") + + s = Session(testing.db) + + q = s.query(A).join(A.partitioned_bs) + self.assert_compile( + q, + "SELECT a.id AS a_id FROM a JOIN " + "(SELECT b.id AS id, b.a_id AS a_id, row_number() " + "OVER (PARTITION BY b.a_id ORDER BY b.id) " + "AS index FROM b) AS anon_1 " + "ON anon_1.a_id = a.id AND anon_1.index < :index_1", + ) + + def test_join_two(self): + A, B, C = self.classes("A", "B", "C") + + s = Session(testing.db) + + q = s.query(A, A.partitioned_bs.entity).join(A.partitioned_bs) + self.assert_compile( + q, + "SELECT a.id AS a_id, anon_1.id AS anon_1_id, " + "anon_1.a_id AS anon_1_a_id " + "FROM a JOIN " + "(SELECT b.id AS id, b.a_id AS a_id, row_number() " + "OVER (PARTITION BY b.a_id ORDER BY b.id) " + "AS index FROM b) AS anon_1 " + "ON anon_1.a_id = a.id AND anon_1.index < :index_1", + ) + + def test_selectinload_w_noload_after(self): + A, B, C = self.classes("A", "B", "C") + + s = Session(testing.db) + + def go(): + for a1 in s.query(A).options( + noload("*"), selectinload(A.partitioned_bs) + ): + for b in a1.partitioned_bs: + eq_(b.cs, []) + + self.assert_sql_count(testing.db, go, 2) + + def test_selectinload_w_joinedload_after(self): + A, B, C = self.classes("A", "B", "C") + + s = Session(testing.db) + + def go(): + for a1 in s.query(A).options( + selectinload(A.partitioned_bs).joinedload("cs") + ): + for b in a1.partitioned_bs: + eq_(len(b.cs), 2) + + self.assert_sql_count(testing.db, go, 2) + + +class AltSelectableTest( + fixtures.DeclarativeMappedTest, testing.AssertsCompiledSQL +): + __dialect__ = "default" + + @classmethod + def setup_classes(cls): + Base = cls.DeclarativeBasic + + class A(ComparableEntity, Base): + __tablename__ = "a" + + id = Column(Integer, primary_key=True) + b_id = Column(ForeignKey("b.id")) + + class B(ComparableEntity, Base): + __tablename__ = "b" + + id = Column(Integer, primary_key=True) + + class C(ComparableEntity, Base): + __tablename__ = "c" + + id = Column(Integer, primary_key=True) + a_id = Column(ForeignKey("a.id")) + + class D(ComparableEntity, Base): + __tablename__ = "d" + + id = Column(Integer, primary_key=True) + c_id = Column(ForeignKey("c.id")) + b_id = Column(ForeignKey("b.id")) + + # 1. set up the join() as a variable, so we can refer + # to it in the mapping multiple times. + j = join(B, D, D.b_id == B.id).join(C, C.id == D.c_id) + + # 2. Create an AliasedClass to B + B_viacd = aliased(B, j, flat=True) + + A.b = relationship(B_viacd, primaryjoin=A.b_id == j.c.b_id) + + @classmethod + def insert_data(cls): + A, B, C, D = cls.classes("A", "B", "C", "D") + sess = Session() + + for obj in [ + B(id=1), + A(id=1, b_id=1), + C(id=1, a_id=1), + D(id=1, c_id=1, b_id=1), + ]: + sess.add(obj) + sess.flush() + sess.commit() + + def test_lazyload(self): + A, B = self.classes("A", "B") + + sess = Session() + a1 = sess.query(A).first() + + with self.sql_execution_asserter() as asserter: + # note this is many-to-one. use_get is unconditionally turned + # off for relationship to aliased class for now. + eq_(a1.b, B(id=1)) + + asserter.assert_( + CompiledSQL( + "SELECT b.id AS b_id FROM b JOIN d ON d.b_id = b.id " + "JOIN c ON c.id = d.c_id WHERE :param_1 = b.id", + [{"param_1": 1}], + ) + ) + + def test_joinedload(self): + A, B = self.classes("A", "B") + + sess = Session() + + with self.sql_execution_asserter() as asserter: + # note this is many-to-one. use_get is unconditionally turned + # off for relationship to aliased class for now. + a1 = sess.query(A).options(joinedload(A.b)).first() + eq_(a1.b, B(id=1)) + + asserter.assert_( + CompiledSQL( + "SELECT a.id AS a_id, a.b_id AS a_b_id, b_1.id AS b_1_id " + "FROM a LEFT OUTER JOIN (b AS b_1 " + "JOIN d AS d_1 ON d_1.b_id = b_1.id " + "JOIN c AS c_1 ON c_1.id = d_1.c_id) ON a.b_id = b_1.id " + "LIMIT :param_1", + [{"param_1": 1}], + ) + ) + + def test_selectinload(self): + A, B = self.classes("A", "B") + + sess = Session() + + with self.sql_execution_asserter() as asserter: + # note this is many-to-one. use_get is unconditionally turned + # off for relationship to aliased class for now. + a1 = sess.query(A).options(selectinload(A.b)).first() + eq_(a1.b, B(id=1)) + + asserter.assert_( + CompiledSQL( + "SELECT a.id AS a_id, a.b_id AS a_b_id " + "FROM a LIMIT :param_1", + [{"param_1": 1}], + ), + CompiledSQL( + "SELECT a_1.id AS a_1_id, b.id AS b_id FROM a AS a_1 " + "JOIN (b JOIN d ON d.b_id = b.id JOIN c ON c.id = d.c_id) " + "ON a_1.b_id = b.id WHERE a_1.id " + "IN ([EXPANDING_primary_keys]) ORDER BY a_1.id", + [{"primary_keys": [1]}], + ), + ) + + def test_join(self): + A, B = self.classes("A", "B") + + sess = Session() + + self.assert_compile( + sess.query(A).join(A.b), + "SELECT a.id AS a_id, a.b_id AS a_b_id " + "FROM a JOIN (b JOIN d ON d.b_id = b.id " + "JOIN c ON c.id = d.c_id) ON a.b_id = b.id", + ) diff --git a/test/orm/test_deprecations.py b/test/orm/test_deprecations.py index 04dff0252b..7ce05345bb 100644 --- a/test/orm/test_deprecations.py +++ b/test/orm/test_deprecations.py @@ -2162,3 +2162,369 @@ class InstrumentationTest(fixtures.ORMTest): l2 = Collection() f1.attr = l2 eq_(canary, [adapter_1, f1.attr._sa_adapter, None]) + + +class NonPrimaryRelationshipLoaderTest(_fixtures.FixtureTest): + run_inserts = "once" + run_deletes = None + + def test_selectload(self): + """tests lazy loading with two relationships simultaneously, + from the same table, using aliases. """ + + users, orders, User, Address, Order, addresses = ( + self.tables.users, + self.tables.orders, + self.classes.User, + self.classes.Address, + self.classes.Order, + self.tables.addresses, + ) + + openorders = sa.alias(orders, "openorders") + closedorders = sa.alias(orders, "closedorders") + + mapper(Address, addresses) + + mapper(Order, orders) + + with testing.expect_deprecated( + "The mapper.non_primary parameter is deprecated" + ): + open_mapper = mapper(Order, openorders, non_primary=True) + closed_mapper = mapper(Order, closedorders, non_primary=True) + mapper( + User, + users, + properties=dict( + addresses=relationship(Address, lazy=True), + open_orders=relationship( + open_mapper, + primaryjoin=sa.and_( + openorders.c.isopen == 1, + users.c.id == openorders.c.user_id, + ), + lazy="select", + ), + closed_orders=relationship( + closed_mapper, + primaryjoin=sa.and_( + closedorders.c.isopen == 0, + users.c.id == closedorders.c.user_id, + ), + lazy="select", + ), + ), + ) + + self._run_double_test(10) + + def test_joinedload(self): + """Eager loading with two relationships simultaneously, + from the same table, using aliases.""" + + users, orders, User, Address, Order, addresses = ( + self.tables.users, + self.tables.orders, + self.classes.User, + self.classes.Address, + self.classes.Order, + self.tables.addresses, + ) + + openorders = sa.alias(orders, "openorders") + closedorders = sa.alias(orders, "closedorders") + + mapper(Address, addresses) + mapper(Order, orders) + + with testing.expect_deprecated( + "The mapper.non_primary parameter is deprecated" + ): + open_mapper = mapper(Order, openorders, non_primary=True) + closed_mapper = mapper(Order, closedorders, non_primary=True) + + mapper( + User, + users, + properties=dict( + addresses=relationship( + Address, lazy="joined", order_by=addresses.c.id + ), + open_orders=relationship( + open_mapper, + primaryjoin=sa.and_( + openorders.c.isopen == 1, + users.c.id == openorders.c.user_id, + ), + lazy="joined", + order_by=openorders.c.id, + ), + closed_orders=relationship( + closed_mapper, + primaryjoin=sa.and_( + closedorders.c.isopen == 0, + users.c.id == closedorders.c.user_id, + ), + lazy="joined", + order_by=closedorders.c.id, + ), + ), + ) + self._run_double_test(1) + + def test_selectin(self): + + users, orders, User, Address, Order, addresses = ( + self.tables.users, + self.tables.orders, + self.classes.User, + self.classes.Address, + self.classes.Order, + self.tables.addresses, + ) + + openorders = sa.alias(orders, "openorders") + closedorders = sa.alias(orders, "closedorders") + + mapper(Address, addresses) + mapper(Order, orders) + + with testing.expect_deprecated( + "The mapper.non_primary parameter is deprecated" + ): + open_mapper = mapper(Order, openorders, non_primary=True) + closed_mapper = mapper(Order, closedorders, non_primary=True) + + mapper( + User, + users, + properties=dict( + addresses=relationship( + Address, lazy="selectin", order_by=addresses.c.id + ), + open_orders=relationship( + open_mapper, + primaryjoin=sa.and_( + openorders.c.isopen == 1, + users.c.id == openorders.c.user_id, + ), + lazy="selectin", + order_by=openorders.c.id, + ), + closed_orders=relationship( + closed_mapper, + primaryjoin=sa.and_( + closedorders.c.isopen == 0, + users.c.id == closedorders.c.user_id, + ), + lazy="selectin", + order_by=closedorders.c.id, + ), + ), + ) + + self._run_double_test(4) + + def test_subqueryload(self): + + users, orders, User, Address, Order, addresses = ( + self.tables.users, + self.tables.orders, + self.classes.User, + self.classes.Address, + self.classes.Order, + self.tables.addresses, + ) + + openorders = sa.alias(orders, "openorders") + closedorders = sa.alias(orders, "closedorders") + + mapper(Address, addresses) + mapper(Order, orders) + + with testing.expect_deprecated( + "The mapper.non_primary parameter is deprecated" + ): + open_mapper = mapper(Order, openorders, non_primary=True) + closed_mapper = mapper(Order, closedorders, non_primary=True) + + mapper( + User, + users, + properties=dict( + addresses=relationship( + Address, lazy="subquery", order_by=addresses.c.id + ), + open_orders=relationship( + open_mapper, + primaryjoin=sa.and_( + openorders.c.isopen == 1, + users.c.id == openorders.c.user_id, + ), + lazy="subquery", + order_by=openorders.c.id, + ), + closed_orders=relationship( + closed_mapper, + primaryjoin=sa.and_( + closedorders.c.isopen == 0, + users.c.id == closedorders.c.user_id, + ), + lazy="subquery", + order_by=closedorders.c.id, + ), + ), + ) + + self._run_double_test(4) + + def _run_double_test(self, count): + User, Address, Order, Item = self.classes( + "User", "Address", "Order", "Item" + ) + q = create_session().query(User).order_by(User.id) + + def go(): + eq_( + [ + User( + id=7, + addresses=[Address(id=1)], + open_orders=[Order(id=3)], + closed_orders=[Order(id=1), Order(id=5)], + ), + User( + id=8, + addresses=[ + Address(id=2), + Address(id=3), + Address(id=4), + ], + open_orders=[], + closed_orders=[], + ), + User( + id=9, + addresses=[Address(id=5)], + open_orders=[Order(id=4)], + closed_orders=[Order(id=2)], + ), + User(id=10), + ], + q.all(), + ) + + self.assert_sql_count(testing.db, go, count) + + sess = create_session() + user = sess.query(User).get(7) + + closed_mapper = User.closed_orders.entity + open_mapper = User.open_orders.entity + eq_( + [Order(id=1), Order(id=5)], + create_session() + .query(closed_mapper) + .with_parent(user, property="closed_orders") + .all(), + ) + eq_( + [Order(id=3)], + create_session() + .query(open_mapper) + .with_parent(user, property="open_orders") + .all(), + ) + + +class NonPrimaryMapperTest(_fixtures.FixtureTest, AssertsCompiledSQL): + __dialect__ = "default" + + def test_non_primary_identity_class(self): + User = self.classes.User + users, addresses = self.tables.users, self.tables.addresses + + class AddressUser(User): + pass + + m1 = mapper(User, users, polymorphic_identity="user") + m2 = mapper( + AddressUser, + addresses, + inherits=User, + polymorphic_identity="address", + properties={"address_id": addresses.c.id}, + ) + with testing.expect_deprecated( + "The mapper.non_primary parameter is deprecated" + ): + m3 = mapper(AddressUser, addresses, non_primary=True) + assert m3._identity_class is m2._identity_class + eq_( + m2.identity_key_from_instance(AddressUser()), + m3.identity_key_from_instance(AddressUser()), + ) + + def test_illegal_non_primary(self): + users, Address, addresses, User = ( + self.tables.users, + self.classes.Address, + self.tables.addresses, + self.classes.User, + ) + + mapper(User, users) + mapper(Address, addresses) + with testing.expect_deprecated( + "The mapper.non_primary parameter is deprecated" + ): + mapper( + User, + users, + non_primary=True, + properties={"addresses": relationship(Address)}, + ) + assert_raises_message( + sa.exc.ArgumentError, + "Attempting to assign a new relationship 'addresses' " + "to a non-primary mapper on class 'User'", + configure_mappers, + ) + + def test_illegal_non_primary_2(self): + User, users = self.classes.User, self.tables.users + + with testing.expect_deprecated( + "The mapper.non_primary parameter is deprecated" + ): + assert_raises_message( + sa.exc.InvalidRequestError, + "Configure a primary mapper first", + mapper, + User, + users, + non_primary=True, + ) + + def test_illegal_non_primary_3(self): + users, addresses = self.tables.users, self.tables.addresses + + class Base(object): + pass + + class Sub(Base): + pass + + mapper(Base, users) + with testing.expect_deprecated( + "The mapper.non_primary parameter is deprecated" + ): + assert_raises_message( + sa.exc.InvalidRequestError, + "Configure a primary mapper first", + mapper, + Sub, + addresses, + non_primary=True, + ) diff --git a/test/orm/test_eager_relations.py b/test/orm/test_eager_relations.py index fd272b1816..71010f02b6 100644 --- a/test/orm/test_eager_relations.py +++ b/test/orm/test_eager_relations.py @@ -718,27 +718,49 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL): sess.query(User).order_by(User.id).all(), ) - def test_double(self): + def test_double_w_ac(self): """Eager loading with two relationships simultaneously, from the same table, using aliases.""" - users, orders, User, Address, Order, addresses = ( + ( + users, + orders, + User, + Address, + Order, + addresses, + Item, + items, + order_items, + ) = ( self.tables.users, self.tables.orders, self.classes.User, self.classes.Address, self.classes.Order, self.tables.addresses, + self.classes.Item, + self.tables.items, + self.tables.order_items, ) - openorders = sa.alias(orders, "openorders") - closedorders = sa.alias(orders, "closedorders") - mapper(Address, addresses) - mapper(Order, orders) + mapper( + Order, + orders, + properties={ + "items": relationship( + Item, + secondary=order_items, + lazy="joined", + order_by=items.c.id, + ) + }, + ) + mapper(Item, items) - open_mapper = mapper(Order, openorders, non_primary=True) - closed_mapper = mapper(Order, closedorders, non_primary=True) + open_mapper = aliased(Order, orders) + closed_mapper = aliased(Order, orders) mapper( User, @@ -750,57 +772,91 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL): open_orders=relationship( open_mapper, primaryjoin=sa.and_( - openorders.c.isopen == 1, - users.c.id == openorders.c.user_id, + open_mapper.isopen == 1, + users.c.id == open_mapper.user_id, ), lazy="joined", - order_by=openorders.c.id, + order_by=open_mapper.id, ), closed_orders=relationship( closed_mapper, primaryjoin=sa.and_( - closedorders.c.isopen == 0, - users.c.id == closedorders.c.user_id, + closed_mapper.isopen == 0, + users.c.id == closed_mapper.user_id, ), lazy="joined", - order_by=closedorders.c.id, + order_by=closed_mapper.id, ), ), ) - q = create_session().query(User).order_by(User.id) + self._run_double_test() - def go(): - eq_( - [ - User( - id=7, - addresses=[Address(id=1)], - open_orders=[Order(id=3)], - closed_orders=[Order(id=1), Order(id=5)], - ), - User( - id=8, - addresses=[ - Address(id=2), - Address(id=3), - Address(id=4), - ], - open_orders=[], - closed_orders=[], - ), - User( - id=9, - addresses=[Address(id=5)], - open_orders=[Order(id=4)], - closed_orders=[Order(id=2)], - ), - User(id=10), - ], - q.all(), - ) + def test_double_w_ac_against_subquery(self): + """Eager loading with two relationships simultaneously, + from the same table, using aliases.""" - self.assert_sql_count(testing.db, go, 1) + ( + users, + orders, + User, + Address, + Order, + addresses, + Item, + items, + order_items, + ) = ( + self.tables.users, + self.tables.orders, + self.classes.User, + self.classes.Address, + self.classes.Order, + self.tables.addresses, + self.classes.Item, + self.tables.items, + self.tables.order_items, + ) + + mapper(Address, addresses) + mapper( + Order, + orders, + properties={ + "items": relationship( + Item, + secondary=order_items, + lazy="joined", + order_by=items.c.id, + ) + }, + ) + mapper(Item, items) + + open_mapper = aliased( + Order, select([orders]).where(orders.c.isopen == 1).alias() + ) + closed_mapper = aliased( + Order, select([orders]).where(orders.c.isopen == 0).alias() + ) + + mapper( + User, + users, + properties=dict( + addresses=relationship( + Address, lazy="joined", order_by=addresses.c.id + ), + open_orders=relationship( + open_mapper, lazy="joined", order_by=open_mapper.id + ), + closed_orders=relationship( + closed_mapper, lazy="joined", order_by=closed_mapper.id + ), + ), + ) + + self._run_double_test() def test_double_same_mappers(self): """Eager loading with two relationships simultaneously, @@ -867,26 +923,30 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL): ), ), ) + self._run_double_test() + + def _run_double_test(self, no_items=False): + User, Address, Order, Item = self.classes( + "User", "Address", "Order", "Item" + ) q = create_session().query(User).order_by(User.id) + def items(*ids): + if no_items: + return {} + else: + return {"items": [Item(id=id_) for id_ in ids]} + def go(): eq_( [ User( id=7, addresses=[Address(id=1)], - open_orders=[ - Order( - id=3, - items=[Item(id=3), Item(id=4), Item(id=5)], - ) - ], + open_orders=[Order(id=3, **items(3, 4, 5))], closed_orders=[ - Order( - id=1, - items=[Item(id=1), Item(id=2), Item(id=3)], - ), - Order(id=5, items=[Item(id=5)]), + Order(id=1, **items(1, 2, 3)), + Order(id=5, **items(5)), ], ), User( @@ -902,15 +962,8 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL): User( id=9, addresses=[Address(id=5)], - open_orders=[ - Order(id=4, items=[Item(id=1), Item(id=5)]) - ], - closed_orders=[ - Order( - id=2, - items=[Item(id=1), Item(id=2), Item(id=3)], - ) - ], + open_orders=[Order(id=4, **items(1, 5))], + closed_orders=[Order(id=2, **items(1, 2, 3))], ), User(id=10), ], @@ -1739,9 +1792,7 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL): Order, backref="user", lazy="joined", order_by=orders.c.id ), "max_order": relationship( - mapper(Order, max_orders, non_primary=True), - lazy="joined", - uselist=False, + aliased(Order, max_orders), lazy="joined", uselist=False ), }, ) @@ -3021,7 +3072,7 @@ class InnerJoinSplicingTest(fixtures.MappedTest, testing.AssertsCompiledSQL): ) self._assert_result(q) - def test_splice_onto_np_mapper(self): + def test_splice_onto_ac(self): A = self.classes.A B = self.classes.B C1 = self.classes.C1 @@ -3032,20 +3083,7 @@ class InnerJoinSplicingTest(fixtures.MappedTest, testing.AssertsCompiledSQL): weird_selectable = b_table.outerjoin(c1_table) - b_np = mapper( - B, - weird_selectable, - non_primary=True, - properties=odict( - [ - # note we need to make this fixed with lazy=False until - # [ticket:3348] is resolved - ("c1s", relationship(C1, lazy=False, innerjoin=True)), - ("c_id", c1_table.c.id), - ("b_value", b_table.c.value), - ] - ), - ) + b_np = aliased(B, weird_selectable, flat=True) a_mapper = inspect(A) a_mapper.add_property("bs_np", relationship(b_np)) @@ -3055,14 +3093,10 @@ class InnerJoinSplicingTest(fixtures.MappedTest, testing.AssertsCompiledSQL): q = s.query(A).options(joinedload("bs_np", innerjoin=False)) self.assert_compile( q, - "SELECT a.id AS a_id, c1_1.id AS c1_1_id, c1_1.b_id AS c1_1_b_id, " - "c1_1.value AS c1_1_value, c1_2.id AS c1_2_id, " - "b_1.value AS b_1_value, b_1.id AS b_1_id, " - "b_1.a_id AS b_1_a_id, c1_2.b_id AS c1_2_b_id, " - "c1_2.value AS c1_2_value " - "FROM a LEFT OUTER JOIN " - "(b AS b_1 LEFT OUTER JOIN c1 AS c1_2 ON b_1.id = c1_2.b_id " - "JOIN c1 AS c1_1 ON b_1.id = c1_1.b_id) ON a.id = b_1.a_id", + "SELECT a.id AS a_id, b_1.id AS b_1_id, b_1.a_id AS b_1_a_id, " + "b_1.value AS b_1_value FROM a LEFT OUTER JOIN " + "(b AS b_1 LEFT OUTER JOIN c1 AS c1_1 ON b_1.id = c1_1.b_id) " + "ON a.id = b_1.a_id", ) diff --git a/test/orm/test_froms.py b/test/orm/test_froms.py index c5e1d1485c..1cccfff268 100644 --- a/test/orm/test_froms.py +++ b/test/orm/test_froms.py @@ -2399,7 +2399,7 @@ class MixedEntitiesTest(QueryTest, AssertsCompiledSQL): ]: q = s.query(crit) mzero = q._entity_zero() - is_(mzero.mapped_table, q._query_entity_zero().selectable) + is_(mzero.persist_selectable, q._query_entity_zero().selectable) q = q.join(j) self.assert_compile(q, exp) diff --git a/test/orm/test_inspect.py b/test/orm/test_inspect.py index ca127580dc..368199b96f 100644 --- a/test/orm/test_inspect.py +++ b/test/orm/test_inspect.py @@ -48,11 +48,11 @@ class TestORMInspection(_fixtures.FixtureTest): insp = inspect(User) is_(insp.local_table, user_table) - def test_mapped_table(self): + def test_persist_selectable(self): User = self.classes.User user_table = self.tables.users insp = inspect(User) - is_(insp.mapped_table, user_table) + is_(insp.persist_selectable, user_table) def test_mapper_selectable(self): User = self.classes.User diff --git a/test/orm/test_joins.py b/test/orm/test_joins.py index 046d5c3e22..bb9747cf2f 100644 --- a/test/orm/test_joins.py +++ b/test/orm/test_joins.py @@ -2912,7 +2912,7 @@ class JoinToNonPolyAliasesTest(fixtures.MappedTest, AssertsCompiledSQL): mapper(Child, child) derived = select([child]).alias() - npc = mapper(Child, derived, non_primary=True) + npc = aliased(Child, derived) cls.npc = npc cls.derived = derived mp.add_property("npc", relationship(npc)) diff --git a/test/orm/test_lazy_relations.py b/test/orm/test_lazy_relations.py index 4d9e460f9a..78680701e0 100644 --- a/test/orm/test_lazy_relations.py +++ b/test/orm/test_lazy_relations.py @@ -10,9 +10,11 @@ from sqlalchemy import ForeignKey from sqlalchemy import ForeignKeyConstraint from sqlalchemy import Integer from sqlalchemy import orm +from sqlalchemy import select from sqlalchemy import SmallInteger from sqlalchemy import String from sqlalchemy import testing +from sqlalchemy.orm import aliased from sqlalchemy.orm import attributes from sqlalchemy.orm import configure_mappers from sqlalchemy.orm import create_session @@ -530,28 +532,107 @@ class LazyTest(_fixtures.FixtureTest): list(q), ) - def test_double(self): - """tests lazy loading with two relationships simultaneously, - from the same table, using aliases. """ + def test_double_w_ac_against_subquery(self): - users, orders, User, Address, Order, addresses = ( + ( + users, + orders, + User, + Address, + Order, + addresses, + Item, + items, + order_items, + ) = ( self.tables.users, self.tables.orders, self.classes.User, self.classes.Address, self.classes.Order, self.tables.addresses, + self.classes.Item, + self.tables.items, + self.tables.order_items, + ) + + mapper(Address, addresses) + + mapper( + Order, + orders, + properties={ + "items": relationship( + Item, + secondary=order_items, + lazy="select", + order_by=items.c.id, + ) + }, + ) + mapper(Item, items) + + open_mapper = aliased( + Order, select([orders]).where(orders.c.isopen == 1).alias() + ) + closed_mapper = aliased( + Order, select([orders]).where(orders.c.isopen == 0).alias() + ) + + mapper( + User, + users, + properties=dict( + addresses=relationship(Address, lazy=True), + open_orders=relationship(open_mapper, lazy="select"), + closed_orders=relationship(closed_mapper, lazy="select"), + ), ) - openorders = sa.alias(orders, "openorders") - closedorders = sa.alias(orders, "closedorders") + self._run_double_test() + + def test_double_w_ac(self): + + ( + users, + orders, + User, + Address, + Order, + addresses, + Item, + items, + order_items, + ) = ( + self.tables.users, + self.tables.orders, + self.classes.User, + self.classes.Address, + self.classes.Order, + self.tables.addresses, + self.classes.Item, + self.tables.items, + self.tables.order_items, + ) mapper(Address, addresses) - mapper(Order, orders) + mapper( + Order, + orders, + properties={ + "items": relationship( + Item, + secondary=order_items, + lazy="select", + order_by=items.c.id, + ) + }, + ) + mapper(Item, items) - open_mapper = mapper(Order, openorders, non_primary=True) - closed_mapper = mapper(Order, closedorders, non_primary=True) + open_mapper = aliased(Order, orders) + closed_mapper = aliased(Order, orders) mapper( User, users, @@ -560,47 +641,79 @@ class LazyTest(_fixtures.FixtureTest): open_orders=relationship( open_mapper, primaryjoin=sa.and_( - openorders.c.isopen == 1, - users.c.id == openorders.c.user_id, + open_mapper.isopen == 1, + users.c.id == open_mapper.user_id, ), lazy="select", ), closed_orders=relationship( closed_mapper, primaryjoin=sa.and_( - closedorders.c.isopen == 0, - users.c.id == closedorders.c.user_id, + closed_mapper.isopen == 0, + users.c.id == closed_mapper.user_id, ), lazy="select", ), ), ) - q = create_session().query(User) - assert [ - User( - id=7, - addresses=[Address(id=1)], - open_orders=[Order(id=3)], - closed_orders=[Order(id=1), Order(id=5)], - ), - User( - id=8, - addresses=[Address(id=2), Address(id=3), Address(id=4)], - open_orders=[], - closed_orders=[], - ), - User( - id=9, - addresses=[Address(id=5)], - open_orders=[Order(id=4)], - closed_orders=[Order(id=2)], - ), - User(id=10), - ] == q.all() + self._run_double_test() + + def _run_double_test(self, no_items=False): + User, Address, Order, Item = self.classes( + "User", "Address", "Order", "Item" + ) + q = create_session().query(User).order_by(User.id) + + def items(*ids): + if no_items: + return {} + else: + return {"items": [Item(id=id_) for id_ in ids]} + + def go(): + eq_( + [ + User( + id=7, + addresses=[Address(id=1)], + open_orders=[Order(id=3, **items(3, 4, 5))], + closed_orders=[ + Order(id=1, **items(1, 2, 3)), + Order(id=5, **items(5)), + ], + ), + User( + id=8, + addresses=[ + Address(id=2), + Address(id=3), + Address(id=4), + ], + open_orders=[], + closed_orders=[], + ), + User( + id=9, + addresses=[Address(id=5)], + open_orders=[Order(id=4, **items(1, 5))], + closed_orders=[Order(id=2, **items(1, 2, 3))], + ), + User(id=10), + ], + q.all(), + ) + + if no_items: + self.assert_sql_count(testing.db, go, 10) + else: + self.assert_sql_count(testing.db, go, 15) sess = create_session() user = sess.query(User).get(7) + + closed_mapper = User.closed_orders.entity + open_mapper = User.open_orders.entity eq_( [Order(id=1), Order(id=5)], create_session() diff --git a/test/orm/test_mapper.py b/test/orm/test_mapper.py index 4e42e4f9aa..06929c6bc3 100644 --- a/test/orm/test_mapper.py +++ b/test/orm/test_mapper.py @@ -853,28 +853,6 @@ class MapperTest(_fixtures.FixtureTest, AssertsCompiledSQL): assert n1.children[0] is n1._children[0] is n2 eq_(str(Node.parent == n2), ":param_1 = nodes.parent_id") - def test_non_primary_identity_class(self): - User = self.classes.User - users, addresses = self.tables.users, self.tables.addresses - - class AddressUser(User): - pass - - m1 = mapper(User, users, polymorphic_identity="user") - m2 = mapper( - AddressUser, - addresses, - inherits=User, - polymorphic_identity="address", - properties={"address_id": addresses.c.id}, - ) - m3 = mapper(AddressUser, addresses, non_primary=True) - assert m3._identity_class is m2._identity_class - eq_( - m2.identity_key_from_instance(AddressUser()), - m3.identity_key_from_instance(AddressUser()), - ) - def test_reassign_polymorphic_identity_warns(self): User = self.classes.User users = self.tables.users @@ -898,60 +876,6 @@ class MapperTest(_fixtures.FixtureTest, AssertsCompiledSQL): polymorphic_identity="user", ) - def test_illegal_non_primary(self): - users, Address, addresses, User = ( - self.tables.users, - self.classes.Address, - self.tables.addresses, - self.classes.User, - ) - - mapper(User, users) - mapper(Address, addresses) - mapper( - User, - users, - non_primary=True, - properties={"addresses": relationship(Address)}, - ) - assert_raises_message( - sa.exc.ArgumentError, - "Attempting to assign a new relationship 'addresses' " - "to a non-primary mapper on class 'User'", - configure_mappers, - ) - - def test_illegal_non_primary_2(self): - User, users = self.classes.User, self.tables.users - - assert_raises_message( - sa.exc.InvalidRequestError, - "Configure a primary mapper first", - mapper, - User, - users, - non_primary=True, - ) - - def test_illegal_non_primary_3(self): - users, addresses = self.tables.users, self.tables.addresses - - class Base(object): - pass - - class Sub(Base): - pass - - mapper(Base, users) - assert_raises_message( - sa.exc.InvalidRequestError, - "Configure a primary mapper first", - mapper, - Sub, - addresses, - non_primary=True, - ) - def test_prop_filters(self): t = Table( "person", diff --git a/test/orm/test_rel_fn.py b/test/orm/test_rel_fn.py index 8fd521ae8c..5e6ac53fe8 100644 --- a/test/orm/test_rel_fn.py +++ b/test/orm/test_rel_fn.py @@ -1120,46 +1120,53 @@ class AdaptedJoinTest(_JoinFixtures, fixtures.TestBase, AssertsCompiledSQL): def test_join_targets_o2m_selfref(self): joincond = self._join_fixture_o2m_selfref() - left = select([joincond.parent_selectable]).alias("pj") + left = select([joincond.parent_persist_selectable]).alias("pj") pj, sj, sec, adapter, ds = joincond.join_targets( - left, joincond.child_selectable, True + left, joincond.child_persist_selectable, True ) self.assert_compile(pj, "pj.id = selfref.sid") + self.assert_compile(pj, "pj.id = selfref.sid") - right = select([joincond.child_selectable]).alias("pj") + right = select([joincond.child_persist_selectable]).alias("pj") pj, sj, sec, adapter, ds = joincond.join_targets( - joincond.parent_selectable, right, True + joincond.parent_persist_selectable, right, True ) self.assert_compile(pj, "selfref.id = pj.sid") + self.assert_compile(pj, "selfref.id = pj.sid") def test_join_targets_o2m_plain(self): joincond = self._join_fixture_o2m() pj, sj, sec, adapter, ds = joincond.join_targets( - joincond.parent_selectable, joincond.child_selectable, False + joincond.parent_persist_selectable, + joincond.child_persist_selectable, + False, ) self.assert_compile(pj, "lft.id = rgt.lid") + self.assert_compile(pj, "lft.id = rgt.lid") def test_join_targets_o2m_left_aliased(self): joincond = self._join_fixture_o2m() - left = select([joincond.parent_selectable]).alias("pj") + left = select([joincond.parent_persist_selectable]).alias("pj") pj, sj, sec, adapter, ds = joincond.join_targets( - left, joincond.child_selectable, True + left, joincond.child_persist_selectable, True ) self.assert_compile(pj, "pj.id = rgt.lid") + self.assert_compile(pj, "pj.id = rgt.lid") def test_join_targets_o2m_right_aliased(self): joincond = self._join_fixture_o2m() - right = select([joincond.child_selectable]).alias("pj") + right = select([joincond.child_persist_selectable]).alias("pj") pj, sj, sec, adapter, ds = joincond.join_targets( - joincond.parent_selectable, right, True + joincond.parent_persist_selectable, right, True ) self.assert_compile(pj, "lft.id = pj.lid") + self.assert_compile(pj, "lft.id = pj.lid") def test_join_targets_o2m_composite_selfref(self): joincond = self._join_fixture_o2m_composite_selfref() - right = select([joincond.child_selectable]).alias("pj") + right = select([joincond.child_persist_selectable]).alias("pj") pj, sj, sec, adapter, ds = joincond.join_targets( - joincond.parent_selectable, right, True + joincond.parent_persist_selectable, right, True ) self.assert_compile( pj, @@ -1169,9 +1176,9 @@ class AdaptedJoinTest(_JoinFixtures, fixtures.TestBase, AssertsCompiledSQL): def test_join_targets_m2o_composite_selfref(self): joincond = self._join_fixture_m2o_composite_selfref() - right = select([joincond.child_selectable]).alias("pj") + right = select([joincond.child_persist_selectable]).alias("pj") pj, sj, sec, adapter, ds = joincond.join_targets( - joincond.parent_selectable, right, True + joincond.parent_persist_selectable, right, True ) self.assert_compile( pj, diff --git a/test/orm/test_selectable.py b/test/orm/test_selectable.py index 1fc2f3f16a..35d32a444f 100644 --- a/test/orm/test_selectable.py +++ b/test/orm/test_selectable.py @@ -74,7 +74,7 @@ class SelectableNoFromsTest(fixtures.MappedTest, AssertsCompiledSQL): Subset, common = self.classes.Subset, self.tables.common subset_select = select([common.c.id, common.c.data]).alias() - subset_mapper = mapper(Subset, subset_select) + mapper(Subset, subset_select) sess = Session(bind=testing.db) sess.add(Subset(data=1)) sess.flush() @@ -84,7 +84,7 @@ class SelectableNoFromsTest(fixtures.MappedTest, AssertsCompiledSQL): eq_(sess.query(Subset).filter(Subset.data == 1).one(), Subset(data=1)) eq_(sess.query(Subset).filter(Subset.data != 1).first(), None) - subset_select = sa.orm.class_mapper(Subset).mapped_table + subset_select = sa.orm.class_mapper(Subset).persist_selectable eq_( sess.query(Subset).filter(subset_select.c.data == 1).one(), Subset(data=1), diff --git a/test/orm/test_selectin_relations.py b/test/orm/test_selectin_relations.py index b891835d1f..e95fbb0503 100644 --- a/test/orm/test_selectin_relations.py +++ b/test/orm/test_selectin_relations.py @@ -2,6 +2,7 @@ import sqlalchemy as sa from sqlalchemy import bindparam from sqlalchemy import ForeignKey from sqlalchemy import Integer +from sqlalchemy import select from sqlalchemy import String from sqlalchemy import testing from sqlalchemy.orm import aliased @@ -776,27 +777,111 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL): sess.query(User).order_by(User.id).all(), ) - def test_double(self): - """Eager loading with two relationships simultaneously, - from the same table, using aliases.""" + def test_double_w_ac_against_subquery(self): - users, orders, User, Address, Order, addresses = ( + ( + users, + orders, + User, + Address, + Order, + addresses, + Item, + items, + order_items, + ) = ( self.tables.users, self.tables.orders, self.classes.User, self.classes.Address, self.classes.Order, self.tables.addresses, + self.classes.Item, + self.tables.items, + self.tables.order_items, + ) + + mapper(Address, addresses) + mapper( + Order, + orders, + properties={ + "items": relationship( + Item, + secondary=order_items, + lazy="selectin", + order_by=items.c.id, + ) + }, + ) + mapper(Item, items) + + open_mapper = aliased( + Order, select([orders]).where(orders.c.isopen == 1).alias() ) + closed_mapper = aliased( + Order, select([orders]).where(orders.c.isopen == 0).alias() + ) + + mapper( + User, + users, + properties=dict( + addresses=relationship( + Address, lazy="selectin", order_by=addresses.c.id + ), + open_orders=relationship( + open_mapper, lazy="selectin", order_by=open_mapper.id + ), + closed_orders=relationship( + closed_mapper, lazy="selectin", order_by=closed_mapper.id + ), + ), + ) + + self._run_double_test() - openorders = sa.alias(orders, "openorders") - closedorders = sa.alias(orders, "closedorders") + def test_double_w_ac(self): + + ( + users, + orders, + User, + Address, + Order, + addresses, + Item, + items, + order_items, + ) = ( + self.tables.users, + self.tables.orders, + self.classes.User, + self.classes.Address, + self.classes.Order, + self.tables.addresses, + self.classes.Item, + self.tables.items, + self.tables.order_items, + ) mapper(Address, addresses) - mapper(Order, orders) + mapper( + Order, + orders, + properties={ + "items": relationship( + Item, + secondary=order_items, + lazy="selectin", + order_by=items.c.id, + ) + }, + ) + mapper(Item, items) - open_mapper = mapper(Order, openorders, non_primary=True) - closed_mapper = mapper(Order, closedorders, non_primary=True) + open_mapper = aliased(Order, orders) + closed_mapper = aliased(Order, orders) mapper( User, @@ -808,57 +893,25 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL): open_orders=relationship( open_mapper, primaryjoin=sa.and_( - openorders.c.isopen == 1, - users.c.id == openorders.c.user_id, + open_mapper.isopen == 1, + users.c.id == open_mapper.user_id, ), lazy="selectin", - order_by=openorders.c.id, + order_by=open_mapper.id, ), closed_orders=relationship( closed_mapper, primaryjoin=sa.and_( - closedorders.c.isopen == 0, - users.c.id == closedorders.c.user_id, + closed_mapper.isopen == 0, + users.c.id == closed_mapper.user_id, ), lazy="selectin", - order_by=closedorders.c.id, + order_by=closed_mapper.id, ), ), ) - q = create_session().query(User).order_by(User.id) - - def go(): - eq_( - [ - User( - id=7, - addresses=[Address(id=1)], - open_orders=[Order(id=3)], - closed_orders=[Order(id=1), Order(id=5)], - ), - User( - id=8, - addresses=[ - Address(id=2), - Address(id=3), - Address(id=4), - ], - open_orders=[], - closed_orders=[], - ), - User( - id=9, - addresses=[Address(id=5)], - open_orders=[Order(id=4)], - closed_orders=[Order(id=2)], - ), - User(id=10), - ], - q.all(), - ) - - self.assert_sql_count(testing.db, go, 4) + self._run_double_test() def test_double_same_mappers(self): """Eager loading with two relationships simultaneously, @@ -925,26 +978,31 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL): ), ), ) + + self._run_double_test() + + def _run_double_test(self, no_items=False): + User, Address, Order, Item = self.classes( + "User", "Address", "Order", "Item" + ) q = create_session().query(User).order_by(User.id) + def items(*ids): + if no_items: + return {} + else: + return {"items": [Item(id=id_) for id_ in ids]} + def go(): eq_( [ User( id=7, addresses=[Address(id=1)], - open_orders=[ - Order( - id=3, - items=[Item(id=3), Item(id=4), Item(id=5)], - ) - ], + open_orders=[Order(id=3, **items(3, 4, 5))], closed_orders=[ - Order( - id=1, - items=[Item(id=1), Item(id=2), Item(id=3)], - ), - Order(id=5, items=[Item(id=5)]), + Order(id=1, **items(1, 2, 3)), + Order(id=5, **items(5)), ], ), User( @@ -960,22 +1018,18 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL): User( id=9, addresses=[Address(id=5)], - open_orders=[ - Order(id=4, items=[Item(id=1), Item(id=5)]) - ], - closed_orders=[ - Order( - id=2, - items=[Item(id=1), Item(id=2), Item(id=3)], - ) - ], + open_orders=[Order(id=4, **items(1, 5))], + closed_orders=[Order(id=2, **items(1, 2, 3))], ), User(id=10), ], q.all(), ) - self.assert_sql_count(testing.db, go, 6) + if no_items: + self.assert_sql_count(testing.db, go, 4) + else: + self.assert_sql_count(testing.db, go, 6) def test_limit(self): """Limit operations combined with lazy-load relationships.""" @@ -1119,9 +1173,7 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL): order_by=orders.c.id, ), "max_order": relationship( - mapper(Order, max_orders, non_primary=True), - lazy="selectin", - uselist=False, + aliased(Order, max_orders), lazy="selectin", uselist=False ), }, ) diff --git a/test/orm/test_subquery_relations.py b/test/orm/test_subquery_relations.py index b4be6debe4..117ab5be41 100644 --- a/test/orm/test_subquery_relations.py +++ b/test/orm/test_subquery_relations.py @@ -3,6 +3,7 @@ from sqlalchemy import bindparam from sqlalchemy import ForeignKey from sqlalchemy import inspect from sqlalchemy import Integer +from sqlalchemy import select from sqlalchemy import String from sqlalchemy import testing from sqlalchemy.orm import aliased @@ -796,27 +797,111 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL): "1 FROM users", ) - def test_double(self): - """Eager loading with two relationships simultaneously, - from the same table, using aliases.""" + def test_double_w_ac_against_subquery(self): - users, orders, User, Address, Order, addresses = ( + ( + users, + orders, + User, + Address, + Order, + addresses, + Item, + items, + order_items, + ) = ( self.tables.users, self.tables.orders, self.classes.User, self.classes.Address, self.classes.Order, self.tables.addresses, + self.classes.Item, + self.tables.items, + self.tables.order_items, + ) + + mapper(Address, addresses) + mapper( + Order, + orders, + properties={ + "items": relationship( + Item, + secondary=order_items, + lazy="subquery", + order_by=items.c.id, + ) + }, + ) + mapper(Item, items) + + open_mapper = aliased( + Order, select([orders]).where(orders.c.isopen == 1).alias() ) + closed_mapper = aliased( + Order, select([orders]).where(orders.c.isopen == 0).alias() + ) + + mapper( + User, + users, + properties=dict( + addresses=relationship( + Address, lazy="subquery", order_by=addresses.c.id + ), + open_orders=relationship( + open_mapper, lazy="subquery", order_by=open_mapper.id + ), + closed_orders=relationship( + closed_mapper, lazy="subquery", order_by=closed_mapper.id + ), + ), + ) + + self._run_double_test() - openorders = sa.alias(orders, "openorders") - closedorders = sa.alias(orders, "closedorders") + def test_double_w_ac(self): + + ( + users, + orders, + User, + Address, + Order, + addresses, + Item, + items, + order_items, + ) = ( + self.tables.users, + self.tables.orders, + self.classes.User, + self.classes.Address, + self.classes.Order, + self.tables.addresses, + self.classes.Item, + self.tables.items, + self.tables.order_items, + ) mapper(Address, addresses) - mapper(Order, orders) + mapper( + Order, + orders, + properties={ + "items": relationship( + Item, + secondary=order_items, + lazy="subquery", + order_by=items.c.id, + ) + }, + ) + mapper(Item, items) - open_mapper = mapper(Order, openorders, non_primary=True) - closed_mapper = mapper(Order, closedorders, non_primary=True) + open_mapper = aliased(Order, orders) + closed_mapper = aliased(Order, orders) mapper( User, @@ -828,57 +913,25 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL): open_orders=relationship( open_mapper, primaryjoin=sa.and_( - openorders.c.isopen == 1, - users.c.id == openorders.c.user_id, + open_mapper.isopen == 1, + users.c.id == open_mapper.user_id, ), lazy="subquery", - order_by=openorders.c.id, + order_by=open_mapper.id, ), closed_orders=relationship( closed_mapper, primaryjoin=sa.and_( - closedorders.c.isopen == 0, - users.c.id == closedorders.c.user_id, + closed_mapper.isopen == 0, + users.c.id == closed_mapper.user_id, ), lazy="subquery", - order_by=closedorders.c.id, + order_by=closed_mapper.id, ), ), ) - q = create_session().query(User).order_by(User.id) - - def go(): - eq_( - [ - User( - id=7, - addresses=[Address(id=1)], - open_orders=[Order(id=3)], - closed_orders=[Order(id=1), Order(id=5)], - ), - User( - id=8, - addresses=[ - Address(id=2), - Address(id=3), - Address(id=4), - ], - open_orders=[], - closed_orders=[], - ), - User( - id=9, - addresses=[Address(id=5)], - open_orders=[Order(id=4)], - closed_orders=[Order(id=2)], - ), - User(id=10), - ], - q.all(), - ) - - self.assert_sql_count(testing.db, go, 4) + self._run_double_test() def test_double_same_mappers(self): """Eager loading with two relationships simultaneously, @@ -945,26 +998,30 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL): ), ), ) + self._run_double_test() + + def _run_double_test(self, no_items=False): + User, Address, Order, Item = self.classes( + "User", "Address", "Order", "Item" + ) q = create_session().query(User).order_by(User.id) + def items(*ids): + if no_items: + return {} + else: + return {"items": [Item(id=id_) for id_ in ids]} + def go(): eq_( [ User( id=7, addresses=[Address(id=1)], - open_orders=[ - Order( - id=3, - items=[Item(id=3), Item(id=4), Item(id=5)], - ) - ], + open_orders=[Order(id=3, **items(3, 4, 5))], closed_orders=[ - Order( - id=1, - items=[Item(id=1), Item(id=2), Item(id=3)], - ), - Order(id=5, items=[Item(id=5)]), + Order(id=1, **items(1, 2, 3)), + Order(id=5, **items(5)), ], ), User( @@ -980,22 +1037,18 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL): User( id=9, addresses=[Address(id=5)], - open_orders=[ - Order(id=4, items=[Item(id=1), Item(id=5)]) - ], - closed_orders=[ - Order( - id=2, - items=[Item(id=1), Item(id=2), Item(id=3)], - ) - ], + open_orders=[Order(id=4, **items(1, 5))], + closed_orders=[Order(id=2, **items(1, 2, 3))], ), User(id=10), ], q.all(), ) - self.assert_sql_count(testing.db, go, 6) + if no_items: + self.assert_sql_count(testing.db, go, 4) + else: + self.assert_sql_count(testing.db, go, 6) def test_limit(self): """Limit operations combined with lazy-load relationships.""" @@ -1139,9 +1192,7 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL): order_by=orders.c.id, ), "max_order": relationship( - mapper(Order, max_orders, non_primary=True), - lazy="subquery", - uselist=False, + aliased(Order, max_orders), lazy="subquery", uselist=False ), }, ) diff --git a/test/orm/test_unitofwork.py b/test/orm/test_unitofwork.py index f7a735e215..6326f5f1a5 100644 --- a/test/orm/test_unitofwork.py +++ b/test/orm/test_unitofwork.py @@ -2660,17 +2660,13 @@ class ManyToManyTest(_fixtures.FixtureTest): mapper(Keyword, keywords) - # note that we are breaking a rule here, making a second - # mapper(Keyword, keywords) the reorganization of mapper construction - # affected this, but was fixed again - mapper( IKAssociation, item_keywords, primary_key=[item_keywords.c.item_id, item_keywords.c.keyword_id], properties=dict( keyword=relationship( - mapper(Keyword, keywords, non_primary=True), + Keyword, lazy="joined", uselist=False, # note here is a valid place where diff --git a/test/requirements.py b/test/requirements.py index c265bb3c9d..b9d61ed56b 100644 --- a/test/requirements.py +++ b/test/requirements.py @@ -596,7 +596,7 @@ class DefaultRequirements(SuiteRequirements): @property def window_functions(self): return only_if( - ["postgresql>=8.4", "mssql", "oracle"], + ["postgresql>=8.4", "mssql", "oracle", "sqlite>=3.25.0"], "Backend does not support window functions", )