--- /dev/null
+.. change::
+ :tags: bug, orm
+ :tickets: 5761
+
+ Added new attribute :attr:`_orm.ORMExecuteState.is_column_load` to indicate
+ that a :meth:`_orm.SessionEvents.do_orm_execute` handler that a particular
+ operation is a primary-key-directed column attribute load, such as from an
+ expiration or a deferred attribute, and that WHERE criteria or additional
+ loader options should not be added to the query. This has been added to
+ the examples which illustrate the :func:`_orm.with_loader_criteria` option.
\ No newline at end of file
--- /dev/null
+.. change::
+ :tags: bug, orm
+ :tickets: 5762
+
+ The :func:`_orm.with_loader_criteria` option has been modified so that it
+ will never apply its criteria to the SELECT statement for an ORM refresh
+ operation, such as that invoked by :meth:`_orm.Session.refresh` or whenever
+ an expired attribute is loaded. These queries are only against the
+ primary key row of the object that is already present in memory so there
+ should not be additional criteria added.
\ No newline at end of file
@event.listens_for(Session, "do_orm_execute")
def _do_orm_execute(orm_execute_state):
- if orm_execute_state.is_select:
+
+ if (
+ orm_execute_state.is_select and
+ not orm_execute_state.is_column_load and
+ not orm_execute_state.is_relationship_load
+ ):
orm_execute_state.statement = orm_execute_state.statement.options(
with_loader_criteria(MyEntity.public == True)
)
Above, an option is added to all SELECT statements that will limit all queries
against ``MyEntity`` to filter on ``public == True``. The criteria
will be applied to **all** loads of that class within the scope of the
-immediate query as well as subsequent relationship loads, which includes
+immediate query. The :func:`_orm.with_loader_criteria` option by default
+will automatically propagate to relationship loaders as well, which will
+apply to subsequent relationship loads, which includes
lazy loads, selectinloads, etc.
For a series of classes that all feature some common column structure,
@event.listens_for(Session, "do_orm_execute")
def _do_orm_execute(orm_execute_state):
- if orm_execute_state.is_select:
+ if (
+ orm_execute_state.is_select
+ and not orm_execute_state.is_column_load
+ and not orm_execute_state.is_relationship_load
+ ):
one_month_ago = datetime.datetime.today() - datetime.timedelta(months=1)
orm_execute_state.statement = orm_execute_state.statement.options(
)
)
+.. warning:: The use of a lambda inside of the call to
+ :func:`_orm.with_loader_criteria` is only invoked **once per unique class**.
+ Custom functions should not be invoked within this lambda. See
+ :ref:`engine_lambda_caching` for an overview of the "lambda SQL" feature,
+ which is for advanced use only.
+
.. seealso::
:ref:`examples_session_orm_events` - includes working examples of the
# query.
if (
- not execute_state.is_relationship_load
+ not execute_state.is_column_load
+ and not execute_state.is_relationship_load
and not execute_state.execution_options.get("include_private", False)
):
execute_state.statement = execute_state.statement.options(
propagate_to_loaders = False
"""if True, indicate this option should be carried along
- to "secondary" Query objects produced during lazy loads
- or refresh operations.
+ to "secondary" SELECT statements that occur for relationship
+ lazy loaders as well as attribute load / refresh operations.
"""
self.local_execution_options = self.local_execution_options.union(opts)
def _orm_compile_options(self):
+ if not self.is_select:
+ return None
opts = self.statement._compile_options
if isinstance(opts, context.ORMCompileState.default_compile_options):
return opts
else:
return None
+ @property
+ def is_column_load(self):
+ """Return True if the operation is refreshing column-oriented
+ attributes on an existing ORM object.
+
+ This occurs during operations such as :meth:`_orm.Session.refresh`,
+ as well as when an attribute deferred by :func:`_orm.defer` is
+ being loaded, or an attribute that was expired either directly
+ by :meth:`_orm.Session.expire` or via a commit operation is being
+ loaded.
+
+ Handlers will very likely not want to add any options to queries
+ when such an operation is occurring as the query should be a straight
+ primary key fetch which should not have any additional WHERE criteria,
+ and loader options travelling with the instance
+ will have already been added to the query.
+
+ .. versionadded:: 1.4.0b2
+
+ .. seealso::
+
+ :attr:`_orm.ORMExecuteState.is_relationship_load`
+
+ """
+ opts = self._orm_compile_options()
+ return opts is not None and opts._for_refresh_state
+
@property
def is_relationship_load(self):
"""Return True if this load is loading objects on behalf of a
SELECT statement being emitted is on behalf of a relationship
load.
+ Handlers will very likely not want to add any options to queries
+ when such an operation is occurring, as loader options are already
+ capable of being propigated to relationship loaders and should
+ be already present.
+
+ .. seealso::
+
+ :attr:`_orm.ORMExecuteState.is_column_load`
+
"""
+ opts = self._orm_compile_options()
+ if opts is None:
+ return False
path = self.loader_strategy_path
return path is not None and not path.is_root
@event.listens_for("do_orm_execute", session)
def _add_filtering_criteria(execute_state):
- execute_state.statement = execute_state.statement.options(
- with_loader_criteria(
- SecurityRole,
- lambda cls: cls.role.in_(['some_role']),
- include_aliases=True
- )
- )
- The given class will expand to include all mapped subclass and
- need not itself be a mapped class.
+ if (
+ execute_state.is_select
+ and not execute_state.is_column_load
+ and not execute_state.is_relationship_load
+ ):
+ execute_state.statement = execute_state.statement.options(
+ with_loader_criteria(
+ SecurityRole,
+ lambda cls: cls.role.in_(['some_role']),
+ include_aliases=True
+ )
+ )
+ In the above example, the :meth:`_orm.SessionEvents.do_orm_execute`
+ event will intercept all queries emitted using the
+ :class:`_orm.Session`. For those queries which are SELECT statements
+ and are not attribute or relationship loads a custom
+ :func:`_orm.with_loader_criteria` option is added to the query. The
+ :func:`_orm.with_loader_criteria` option will be used in the given
+ statement and will also be automatically propagated to all relationship
+ loads that descend from this query.
+
+ The criteria argument given is a ``lambda`` that accepts a ``cls``
+ argument. The given class will expand to include all mapped subclass
+ and need not itself be a mapped class.
+
+ .. warning:: The use of a lambda inside of the call to
+ :func:`_orm.with_loader_criteria` is only invoked **once per unique
+ class**. Custom functions should not be invoked within this lambda.
+ See :ref:`engine_lambda_caching` for an overview of the "lambda SQL"
+ feature, which is for advanced use only.
:param entity_or_base: a mapped class, or a class that is a super
class of a particular set of mapped classes, to which the rule
# if options to limit the criteria to immediate query only,
# use compile_state.attributes instead
- self.get_global_criteria(compile_state.global_attributes)
+ if not compile_state.compile_options._for_refresh_state:
+ self.get_global_criteria(compile_state.global_attributes)
def get_global_criteria(self, attributes):
for mp in self._all_mappers():
import sqlalchemy as sa
+from sqlalchemy import delete
from sqlalchemy import event
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy import select
from sqlalchemy import String
from sqlalchemy import testing
+from sqlalchemy import update
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import attributes
from sqlalchemy.orm import class_mapper
},
)
+ def test_flags(self):
+ User, Address = self.classes("User", "Address")
+
+ sess = Session(testing.db, future=True)
+
+ canary = Mock()
+
+ @event.listens_for(sess, "do_orm_execute")
+ def do_orm_execute(ctx):
+
+ if not ctx.is_select:
+ assert_raises_message(
+ sa.exc.InvalidRequestError,
+ "This ORM execution is not against a SELECT statement",
+ lambda: ctx.lazy_loaded_from,
+ )
+
+ canary.options(
+ is_select=ctx.is_select,
+ is_update=ctx.is_update,
+ is_delete=ctx.is_delete,
+ is_orm_statement=ctx.is_orm_statement,
+ is_relationship_load=ctx.is_relationship_load,
+ is_column_load=ctx.is_column_load,
+ lazy_loaded_from=ctx.lazy_loaded_from
+ if ctx.is_select
+ else None,
+ )
+
+ u1 = sess.execute(select(User).filter_by(id=7)).scalar_one()
+
+ u1.addresses
+
+ sess.expire(u1)
+
+ eq_(u1.name, "jack")
+
+ sess.execute(delete(User).filter_by(id=18))
+ sess.execute(update(User).filter_by(id=18).values(name="eighteen"))
+
+ eq_(
+ canary.mock_calls,
+ [
+ call.options(
+ is_select=True,
+ is_update=False,
+ is_delete=False,
+ is_orm_statement=True,
+ is_relationship_load=False,
+ is_column_load=False,
+ lazy_loaded_from=None,
+ ),
+ call.options(
+ is_select=True,
+ is_update=False,
+ is_delete=False,
+ is_orm_statement=True,
+ is_relationship_load=False,
+ is_column_load=False,
+ lazy_loaded_from=u1._sa_instance_state,
+ ),
+ call.options(
+ is_select=True,
+ is_update=False,
+ is_delete=False,
+ is_orm_statement=True,
+ is_relationship_load=False,
+ is_column_load=True,
+ lazy_loaded_from=None,
+ ),
+ call.options(
+ is_select=False,
+ is_update=False,
+ is_delete=True,
+ is_orm_statement=True,
+ is_relationship_load=False,
+ is_column_load=False,
+ lazy_loaded_from=None,
+ ),
+ call.options(
+ is_select=False,
+ is_update=True,
+ is_delete=False,
+ is_orm_statement=True,
+ is_relationship_load=False,
+ is_column_load=False,
+ lazy_loaded_from=None,
+ ),
+ ],
+ )
+
def test_chained_events_two(self):
sess = Session(testing.db, future=True)
from sqlalchemy import String
from sqlalchemy import testing
from sqlalchemy.orm import aliased
+from sqlalchemy.orm import defer
from sqlalchemy.orm import joinedload
from sqlalchemy.orm import lazyload
from sqlalchemy.orm import mapper
eq_(s.execute(stmt).scalars().all(), [UserWFoob(name=name)])
+ def test_never_for_refresh(self, user_address_fixture):
+ User, Address = user_address_fixture
+
+ s = Session(testing.db)
+ u1 = s.get(User, 8)
+
+ @event.listens_for(s, "do_orm_execute")
+ def add_criteria(orm_context):
+ orm_context.statement = orm_context.statement.options(
+ with_loader_criteria(User, User.id != 8)
+ )
+
+ s.refresh(u1)
+ eq_(u1.name, "ed")
+
+ def test_never_for_unexpire(self, user_address_fixture):
+ User, Address = user_address_fixture
+
+ s = Session(testing.db)
+ u1 = s.get(User, 8)
+
+ s.expire(u1)
+
+ @event.listens_for(s, "do_orm_execute")
+ def add_criteria(orm_context):
+ orm_context.statement = orm_context.statement.options(
+ with_loader_criteria(User, User.id != 8)
+ )
+
+ eq_(u1.name, "ed")
+
+ def test_never_for_undefer(self, user_address_fixture):
+ User, Address = user_address_fixture
+
+ s = Session(testing.db)
+ u1 = s.execute(
+ select(User).options(defer(User.name)).filter(User.id == 8)
+ ).scalar_one()
+
+ @event.listens_for(s, "do_orm_execute")
+ def add_criteria(orm_context):
+ orm_context.statement = orm_context.statement.options(
+ with_loader_criteria(User, User.id != 8)
+ )
+
+ eq_(u1.name, "ed")
+
class TemporalFixtureTest(testing.fixtures.DeclarativeMappedTest):
@classmethod