--- /dev/null
+.. change::
+ :tags: bug, orm
+ :tickets: 4026
+
+ Fixed bug in :ref:`change_3948` which prevented "selectin" and
+ "inline" settings in a multi-level class hierarchy from interacting
+ together as expected. A new example is added to the documentation.
+
+ .. seealso::
+
+ :ref:`polymorphic_selectin_and_withpoly`
\ No newline at end of file
type = Column(String(50))
__mapper_args__ = {
- 'polymorphic_identity':'employee',
- 'polymorphic_on':type
+ 'polymorphic_identity': 'employee',
+ 'polymorphic_on': type
}
class Engineer(Employee):
__mapper_args__ = {
'polymorphic_load': 'selectin',
- 'polymorphic_identity':'engineer',
+ 'polymorphic_identity': 'engineer',
}
class Manager(Employee):
__mapper_args__ = {
'polymorphic_load': 'selectin',
- 'polymorphic_identity':'manager',
+ 'polymorphic_identity': 'manager',
}
.. warning:: The selectin polymorphic loading feature should be considered
as **experimental** within early releases of the 1.2 series.
+.. _polymorphic_selectin_and_withpoly:
+
+Combining selectin and with_polymorphic
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. note:: works as of 1.2.0b3
+
+With careful planning, selectin loading can be applied against a hierarchy
+that itself uses "with_polymorphic". A particular use case is that of
+using selectin loading to load a joined-inheritance subtable, which then
+uses "with_polymorphic" to refer to further sub-classes, which may be
+joined- or single-table inheritanace. If we added a class ``VicePresident`` that
+extends ``Manager`` using single-table inheritance, we could ensure that
+a load of ``Manager`` also fully loads ``VicePresident`` subtypes at the same time::
+
+ # use "Employee" example from the enclosing section
+
+ class Manager(Employee):
+ __tablename__ = 'manager'
+ id = Column(Integer, ForeignKey('employee.id'), primary_key=True)
+ manager_name = Column(String(30))
+
+ __mapper_args__ = {
+ 'polymorphic_load': 'selectin',
+ 'polymorphic_identity': 'manager',
+ }
+
+ class VicePresident(Manager):
+ vp_info = Column(String(30))
+
+ __mapper_args__ = {
+ "polymorphic_load": "inline",
+ "polymorphic_identity": "vp"
+ }
+
+
+Above, we add a ``vp_info`` column to the ``manager`` table, local to the
+``VicePresident`` subclass. This subclass is linked to the polymorphic
+identity ``"vp"`` which refers to rows which have this data. By setting the
+load style to "inline", it means that a load of ``Manager`` objects will also
+ensure that the ``vp_info`` column is queried for in the same SELECT statement.
+A query against ``Employee`` that encounters a ``Manager`` row would emit
+similarly to the following:
+
+.. sourcecode:: sql
+
+ SELECT employee.id AS employee_id, employee.name AS employee_name,
+ employee.type AS employee_type
+ FROM employee
+ )
+
+ SELECT manager.id AS manager_id, employee.id AS employee_id,
+ employee.type AS employee_type,
+ manager.manager_name AS manager_manager_name,
+ manager.vp_info AS manager_vp_info
+ FROM employee JOIN manager ON employee.id = manager.id
+ WHERE employee.id IN (?) ORDER BY employee.id
+ (1,)
+
+Combining "selectin" polymorhic loading with query-time
+:func:`.orm.with_polymorphic` usage is also possible (though this is very
+outer-space stuff!); assuming the above mappings had no ``polymorphic_load``
+set up, we could get the same result as follows::
+
+ from sqlalchemy.orm import with_polymorphic, selectin_polymorphic
+
+ manager_poly = with_polymorphic(Manager, [VicePresident])
+
+ s.query(Employee).options(
+ selectin_polymorphic(Employee, [manager_poly])).all()
+
+
Referring to specific subtypes on relationships
-----------------------------------------------
if (
key in context.attributes and
context.attributes[key].strategy ==
- (('selectinload_polymorphic', True), ) and
- mapper in context.attributes[key].local_opts['mappers']
- ) or mapper.polymorphic_load == 'selectin':
+ (('selectinload_polymorphic', True), )
+ ):
+ selectin_load_via = mapper._should_selectin_load(
+ context.attributes[key].local_opts['entities'],
+ _polymorphic_from)
+ else:
+ selectin_load_via = mapper._should_selectin_load(
+ None, _polymorphic_from)
+ if selectin_load_via and selectin_load_via is not _polymorphic_from:
# only_load_props goes w/ refresh_state only, and in a refresh
# we are a single row query for the exact entity; polymorphic
# loading does not apply
assert only_load_props is None
- callable_ = _load_subclass_via_in(context, path, mapper)
+ callable_ = _load_subclass_via_in(context, path, selectin_load_via)
PostLoad.callable_for_path(
- context, load_path, mapper,
- callable_, mapper)
+ context, load_path, selectin_load_via,
+ callable_, selectin_load_via)
post_load = PostLoad.for_context(context, load_path, only_load_props)
return _instance
-@util.dependencies("sqlalchemy.ext.baked")
-def _load_subclass_via_in(baked, context, path, mapper):
+def _load_subclass_via_in(context, path, entity):
+ mapper = entity.mapper
zero_idx = len(mapper.base_mapper.primary_key) == 1
- q, enable_opt, disable_opt = mapper._subclass_load_via_in
+ if entity.is_aliased_class:
+ q, enable_opt, disable_opt = mapper._subclass_load_via_in(entity)
+ else:
+ q, enable_opt, disable_opt = mapper._subclass_load_via_in_mapper
def do_load(context, path, states, load_only, effective_entity):
orig_query = context.query
cols.extend(props[key].columns)
return sql.select(cols, cond, use_labels=True)
- @_memoized_configured_property
+ def _iterate_to_target_viawpoly(self, mapper):
+ if self.isa(mapper):
+ prev = self
+ for m in self.iterate_to_root():
+ yield m
+
+ if m is not prev and prev not in \
+ m._with_polymorphic_mappers:
+ break
+
+ prev = m
+ if m is mapper:
+ break
+
+ def _should_selectin_load(self, enabled_via_opt, polymorphic_from):
+ if not enabled_via_opt:
+ # common case, takes place for all polymorphic loads
+ mapper = polymorphic_from
+ for m in self._iterate_to_target_viawpoly(mapper):
+ if m.polymorphic_load == 'selectin':
+ return m
+ else:
+ # uncommon case, selectin load options were used
+ enabled_via_opt = set(enabled_via_opt)
+ enabled_via_opt_mappers = {e.mapper: e for e in enabled_via_opt}
+ for entity in enabled_via_opt.union([polymorphic_from]):
+ mapper = entity.mapper
+ for m in self._iterate_to_target_viawpoly(mapper):
+ if m.polymorphic_load == 'selectin' or \
+ m in enabled_via_opt_mappers:
+ return enabled_via_opt_mappers.get(m, m)
+
+ return None
+
@util.dependencies(
"sqlalchemy.ext.baked",
"sqlalchemy.orm.strategy_options")
- def _subclass_load_via_in(self, baked, strategy_options):
+ def _subclass_load_via_in(self, baked, strategy_options, entity):
"""Assemble a BakedQuery that can load the columns local to
this subclass as a SELECT with IN.
keep_props = set(
[polymorphic_prop] + self._identity_key_props)
- disable_opt = strategy_options.Load(self)
- enable_opt = strategy_options.Load(self)
+ disable_opt = strategy_options.Load(entity)
+ enable_opt = strategy_options.Load(entity)
for prop in self.attrs:
if prop.parent is self or prop in keep_props:
else:
in_expr = self.primary_key[0]
- q = baked.BakedQuery(
- self._compiled_cache,
- lambda session: session.query(self),
- (self, )
- )
+ if entity.is_aliased_class:
+ assert entity.mapper is self
+ q = baked.BakedQuery(
+ self._compiled_cache,
+ lambda session: session.query(entity).
+ select_entity_from(entity.selectable)._adapt_all_clauses(),
+ (self, )
+ )
+ q.spoil()
+ else:
+ q = baked.BakedQuery(
+ self._compiled_cache,
+ lambda session: session.query(self),
+ (self, )
+ )
+
q += lambda q: q.filter(
in_expr.in_(
sql.bindparam('primary_keys', expanding=True)
return q, enable_opt, disable_opt
+ @_memoized_configured_property
+ def _subclass_load_via_in_mapper(self):
+ return self._subclass_load_via_in(self)
+
def cascade_iterator(self, type_, state, halt_on=None):
"""Iterate each element and its mapper in an object graph,
for all relationships that meet the given cascade rule.
state.key = instance_key
self.identity_map.replace(state)
+ state._orphaned_outside_of_session = False
statelib.InstanceState._commit_all_states(
((state, state.dict) for state in states),
self.add(instance, _warn=False)
def _save_or_update_state(self, state):
+ state._orphaned_outside_of_session = False
self._save_or_update_impl(state)
mapper = _state_mapper(state)
proc = new.union(dirty).difference(deleted)
for state in proc:
- is_orphan = (
- _state_mapper(state)._is_orphan(state) and state.has_identity)
- _reg = flush_context.register_object(state, isdelete=is_orphan)
- assert _reg, "Failed to add object to the flush context!"
- processed.add(state)
+ is_orphan = _state_mapper(state)._is_orphan(state)
+
+ is_persistent_orphan = is_orphan and state.has_identity
+
+ if is_orphan and not is_persistent_orphan and state._orphaned_outside_of_session:
+ self._expunge_states([state])
+ else:
+ _reg = flush_context.register_object(
+ state, isdelete=is_persistent_orphan)
+ assert _reg, "Failed to add object to the flush context!"
+ processed.add(state)
# put all remaining deletes into the flush context.
if objset:
expired = False
_deleted = False
_load_pending = False
+ _orphaned_outside_of_session = False
is_instance = True
callables = ()
"""
loadopt.set_class_strategy(
{"selectinload_polymorphic": True},
- opts={"mappers": tuple(sorted((inspect(cls) for cls in classes), key=id))}
+ opts={"entities": tuple(sorted((inspect(cls) for cls in classes), key=id))}
)
return loadopt
return
sess = state.session
- if sess:
- prop = state.manager.mapper._props[key]
+ prop = state.manager.mapper._props[key]
- if sess._warn_on_events:
- sess._flush_warning(
- "collection remove"
- if prop.uselist
- else "related attribute delete")
+ if sess and sess._warn_on_events:
+ sess._flush_warning(
+ "collection remove"
+ if prop.uselist
+ else "related attribute delete")
- # expunge pending orphans
- item_state = attributes.instance_state(item)
- if prop._cascade.delete_orphan and \
- item_state in sess._new and \
- prop.mapper._is_orphan(item_state):
+ # expunge pending orphans
+ item_state = attributes.instance_state(item)
+
+ if prop._cascade.delete_orphan and \
+ prop.mapper._is_orphan(item_state):
+ if sess and item_state in sess._new:
sess.expunge(item)
+ else:
+ item_state._orphaned_outside_of_session = True
def set_(state, newvalue, oldvalue, initiator):
# process "save_update" cascade rules for when an instance
db, callable_, assertsql.CountStatements(count))
@contextlib.contextmanager
- def assert_execution(self, *rules):
- assertsql.asserter.add_rules(rules)
- try:
+ def assert_execution(self, db, *rules):
+ with self.sql_execution_asserter(db) as asserter:
yield
- assertsql.asserter.statement_complete()
- finally:
- assertsql.asserter.clear_rules()
+ asserter.assert_(*rules)
- def assert_statement_count(self, count):
- return self.assert_execution(assertsql.CountStatements(count))
+ def assert_statement_count(self, db, count):
+ return self.assert_execution(db, assertsql.CountStatements(count))
-from sqlalchemy import Integer, String, ForeignKey, func, desc, and_, or_
-from sqlalchemy.orm import interfaces, relationship, mapper, \
- clear_mappers, create_session, joinedload, joinedload_all, \
- subqueryload, subqueryload_all, polymorphic_union, aliased,\
- class_mapper
-from sqlalchemy import exc as sa_exc
-from sqlalchemy.engine import default
+from sqlalchemy import Integer, String, ForeignKey
+from sqlalchemy.orm import relationship, mapper, \
+ create_session, polymorphic_union
from sqlalchemy.testing import AssertsCompiledSQL, fixtures
-from sqlalchemy import testing
from sqlalchemy.testing.schema import Table, Column
-from sqlalchemy.testing import assert_raises, eq_
+from sqlalchemy.testing import config
class Company(fixtures.ComparableEntity):
manager_with_polymorphic = ('*', manager_join)
return person_with_polymorphic,\
manager_with_polymorphic
+
+
+class GeometryFixtureBase(fixtures.DeclarativeMappedTest):
+ """Provides arbitrary inheritance hierarchies based on a dictionary
+ structure.
+
+ e.g.::
+
+ self._fixture_from_geometry(
+ "a": {
+ "subclasses": {
+ "b": {"polymorphic_load": "selectin"},
+ "c": {
+ "subclasses": {
+ "d": {
+ "polymorphic_load": "inlne", "single": True
+ },
+ "e": {
+ "polymorphic_load": "inline", "single": True
+ },
+ },
+ "polymorphic_load": "selectin",
+ }
+ }
+ }
+ )
+
+ would provide the equivalent of::
+
+ class a(Base):
+ __tablename__ = 'a'
+
+ id = Column(Integer, primary_key=True)
+ a_data = Column(String(50))
+ type = Column(String(50))
+ __mapper_args__ = {
+ "polymorphic_on": type,
+ "polymorphic_identity": "a"
+ }
+
+ class b(a):
+ __tablename__ = 'b'
+
+ id = Column(ForeignKey('a.id'), primary_key=True)
+ b_data = Column(String(50))
+
+ __mapper_args__ = {
+ "polymorphic_identity": "b",
+ "polymorphic_load": "selectin"
+ }
+
+ # ...
+
+ class c(a):
+ __tablename__ = 'c'
+
+ class d(c):
+ # ...
+
+ class e(c):
+ # ...
+
+ Declarative is used so that we get extra behaviors of declarative,
+ such as single-inheritance column masking.
+
+ """
+
+ run_create_tables = 'each'
+ run_define_tables = 'each'
+ run_setup_classes = 'each'
+ run_setup_mappers = 'each'
+
+ def _fixture_from_geometry(self, geometry, base=None):
+ if not base:
+ is_base = True
+ base = self.DeclarativeBasic
+ else:
+ is_base = False
+
+ for key, value in geometry.items():
+ if is_base:
+ type_ = Column(String(50))
+ items = {
+ "__tablename__": key,
+ "id": Column(Integer, primary_key=True),
+ "type": type_,
+ "__mapper_args__": {
+ "polymorphic_on": type_,
+ "polymorphic_identity": key
+ }
+
+ }
+ else:
+ items = {
+ "__mapper_args__": {
+ "polymorphic_identity": key
+ }
+ }
+
+ if not value.get("single", False):
+ items["__tablename__"] = key
+ items["id"] = Column(
+ ForeignKey("%s.id" % base.__tablename__),
+ primary_key=True)
+
+ items["%s_data" % key] = Column(String(50))
+
+ # add other mapper options to be transferred here as needed.
+ for mapper_opt in ("polymorphic_load", ):
+ if mapper_opt in value:
+ items["__mapper_args__"][mapper_opt] = value[mapper_opt]
+
+ if is_base:
+ klass = type(key, (fixtures.ComparableEntity, base, ), items)
+ else:
+ klass = type(key, (base, ), items)
+
+ if "subclasses" in value:
+ self._fixture_from_geometry(value["subclasses"], klass)
+
+ if is_base and self.metadata.tables and self.run_create_tables:
+ self.tables.update(self.metadata.tables)
+ self.metadata.create_all(config.db)
+
from sqlalchemy import String, Integer, Column, ForeignKey
from sqlalchemy.orm import relationship, Session, \
- selectin_polymorphic, selectinload
+ selectin_polymorphic, selectinload, with_polymorphic
from sqlalchemy.testing import fixtures
from sqlalchemy import testing
from sqlalchemy.testing import eq_
-from sqlalchemy.testing.assertsql import AllOf, CompiledSQL, EachOf
-from ._poly_fixtures import Company, Person, Engineer, Manager, Boss, \
- Machine, Paperwork, _Polymorphic
+from sqlalchemy.testing.assertsql import AllOf, CompiledSQL, EachOf, Or
+from ._poly_fixtures import Company, Person, Engineer, Manager, \
+ _Polymorphic, GeometryFixtureBase
class BaseAndSubFixture(object):
)
eq_(result, [self.c1, self.c2])
+
+class TestGeometries(GeometryFixtureBase):
+
+ def test_threelevel_selectin_to_inline_mapped(self):
+ self._fixture_from_geometry({
+ "a": {
+ "subclasses": {
+ "b": {"polymorphic_load": "selectin"},
+ "c": {
+ "subclasses": {
+ "d": {
+ "polymorphic_load": "inline", "single": True
+ },
+ "e": {
+ "polymorphic_load": "inline", "single": True
+ },
+ },
+ "polymorphic_load": "selectin",
+ }
+ }
+ }
+ })
+
+ a, b, c, d, e = self.classes("a", "b", "c", "d", "e")
+ sess = Session()
+ sess.add_all([d(d_data="d1"), e(e_data="e1")])
+ sess.commit()
+
+ q = sess.query(a)
+
+ result = self.assert_sql_execution(
+ testing.db,
+ q.all,
+ CompiledSQL(
+ "SELECT a.type AS a_type, a.id AS a_id, "
+ "a.a_data AS a_a_data FROM a",
+ {}
+ ),
+ Or(
+ CompiledSQL(
+ "SELECT a.type AS a_type, c.id AS c_id, a.id AS a_id, "
+ "c.c_data AS c_c_data, c.e_data AS c_e_data, "
+ "c.d_data AS c_d_data "
+ "FROM a JOIN c ON a.id = c.id "
+ "WHERE a.id IN ([EXPANDING_primary_keys]) ORDER BY a.id",
+ [{'primary_keys': [1, 2]}]
+ ),
+ CompiledSQL(
+ "SELECT a.type AS a_type, c.id AS c_id, a.id AS a_id, "
+ "c.c_data AS c_c_data, "
+ "c.d_data AS c_d_data, c.e_data AS c_e_data "
+ "FROM a JOIN c ON a.id = c.id "
+ "WHERE a.id IN ([EXPANDING_primary_keys]) ORDER BY a.id",
+ [{'primary_keys': [1, 2]}]
+ )
+ )
+ )
+ with self.assert_statement_count(testing.db, 0):
+ eq_(
+ result,
+ [d(d_data="d1"), e(e_data="e1")]
+ )
+
+ def test_threelevel_selectin_to_inline_options(self):
+ self._fixture_from_geometry({
+ "a": {
+ "subclasses": {
+ "b": {},
+ "c": {
+ "subclasses": {
+ "d": {
+ "single": True
+ },
+ "e": {
+ "single": True
+ },
+ },
+ }
+ }
+ }
+ })
+
+ a, b, c, d, e = self.classes("a", "b", "c", "d", "e")
+ sess = Session()
+ sess.add_all([d(d_data="d1"), e(e_data="e1")])
+ sess.commit()
+
+ c_alias = with_polymorphic(c, (d, e))
+ q = sess.query(a).options(
+ selectin_polymorphic(a, [b, c_alias])
+ )
+
+ result = self.assert_sql_execution(
+ testing.db,
+ q.all,
+ CompiledSQL(
+ "SELECT a.type AS a_type, a.id AS a_id, "
+ "a.a_data AS a_a_data FROM a",
+ {}
+ ),
+ Or(
+ CompiledSQL(
+ "SELECT a.type AS a_type, c.id AS c_id, a.id AS a_id, "
+ "c.c_data AS c_c_data, c.e_data AS c_e_data, "
+ "c.d_data AS c_d_data "
+ "FROM a JOIN c ON a.id = c.id "
+ "WHERE a.id IN ([EXPANDING_primary_keys]) ORDER BY a.id",
+ [{'primary_keys': [1, 2]}]
+ ),
+ CompiledSQL(
+ "SELECT a.type AS a_type, c.id AS c_id, a.id AS a_id, "
+ "c.c_data AS c_c_data, c.d_data AS c_d_data, "
+ "c.e_data AS c_e_data "
+ "FROM a JOIN c ON a.id = c.id "
+ "WHERE a.id IN ([EXPANDING_primary_keys]) ORDER BY a.id",
+ [{'primary_keys': [1, 2]}]
+ ),
+ )
+ )
+ with self.assert_statement_count(testing.db, 0):
+ eq_(
+ result,
+ [d(d_data="d1"), e(e_data="e1")]
+ )
+
+ def test_threelevel_selectin_to_inline_awkward_alias_options(self):
+ self._fixture_from_geometry({
+ "a": {
+ "subclasses": {
+ "b": {},
+ "c": {
+ "subclasses": {
+ "d": {},
+ "e": {},
+ },
+ }
+ }
+ }
+ })
+
+ a, b, c, d, e = self.classes("a", "b", "c", "d", "e")
+ sess = Session()
+ sess.add_all([d(d_data="d1"), e(e_data="e1")])
+ sess.commit()
+
+ from sqlalchemy import select
+
+ a_table, c_table, d_table, e_table = self.tables("a", "c", "d", "e")
+
+ poly = select([
+ a_table.c.id, a_table.c.type, c_table, d_table, e_table
+ ]).select_from(
+ a_table.join(c_table).outerjoin(d_table).outerjoin(e_table)
+ ).apply_labels().alias('poly')
+
+ c_alias = with_polymorphic(c, (d, e), poly)
+ q = sess.query(a).options(
+ selectin_polymorphic(a, [b, c_alias])
+ ).order_by(a.id)
+
+ result = self.assert_sql_execution(
+ testing.db,
+ q.all,
+ CompiledSQL(
+ "SELECT a.type AS a_type, a.id AS a_id, "
+ "a.a_data AS a_a_data FROM a ORDER BY a.id",
+ {}
+ ),
+ Or(
+ # here, the test is that the adaptation of "a" takes place
+ CompiledSQL(
+ "SELECT poly.a_type AS poly_a_type, "
+ "poly.c_id AS poly_c_id, "
+ "poly.a_id AS poly_a_id, poly.c_c_data AS poly_c_c_data, "
+ "poly.e_id AS poly_e_id, poly.e_e_data AS poly_e_e_data, "
+ "poly.d_id AS poly_d_id, poly.d_d_data AS poly_d_d_data "
+ "FROM (SELECT a.id AS a_id, a.type AS a_type, "
+ "c.id AS c_id, "
+ "c.c_data AS c_c_data, d.id AS d_id, "
+ "d.d_data AS d_d_data, "
+ "e.id AS e_id, e.e_data AS e_e_data FROM a JOIN c "
+ "ON a.id = c.id LEFT OUTER JOIN d ON c.id = d.id "
+ "LEFT OUTER JOIN e ON c.id = e.id) AS poly "
+ "WHERE poly.a_id IN ([EXPANDING_primary_keys]) "
+ "ORDER BY poly.a_id",
+ [{'primary_keys': [1, 2]}]
+ ),
+ CompiledSQL(
+ "SELECT poly.a_type AS poly_a_type, "
+ "poly.c_id AS poly_c_id, "
+ "poly.a_id AS poly_a_id, poly.c_c_data AS poly_c_c_data, "
+ "poly.d_id AS poly_d_id, poly.d_d_data AS poly_d_d_data, "
+ "poly.e_id AS poly_e_id, poly.e_e_data AS poly_e_e_data "
+ "FROM (SELECT a.id AS a_id, a.type AS a_type, "
+ "c.id AS c_id, c.c_data AS c_c_data, d.id AS d_id, "
+ "d.d_data AS d_d_data, e.id AS e_id, "
+ "e.e_data AS e_e_data FROM a JOIN c ON a.id = c.id "
+ "LEFT OUTER JOIN d ON c.id = d.id "
+ "LEFT OUTER JOIN e ON c.id = e.id) AS poly "
+ "WHERE poly.a_id IN ([EXPANDING_primary_keys]) "
+ "ORDER BY poly.a_id",
+ [{'primary_keys': [1, 2]}]
+ )
+ )
+ )
+ with self.assert_statement_count(testing.db, 0):
+ eq_(
+ result,
+ [d(d_data="d1"), e(e_data="e1")]
+ )