pythonsqlalchemysoft-delete

SQLAlchemy: give some execution_options parameter to all session's queries at once


Original question

I have set up a soft deletion on SQLAlchemy 1.4 based on this example in the official doc here. The _add_filtering_criteria filters out the soft-deleted objects whenever the execute_state.execution_options "include_deleted" is False.

At some point, I would like that some queries are able to search in the soft-deleted objects. I am able to do it per queries, i.e. specifying query.execution_options(include_deleted=True) but I would like to make all queries of a particular session include soft-deleted objects without having to specify it for each query.

I have tried to declare the execution_options(include_deleted=True) on the engine's creation but it does not work.

from sqlalchemy import orm

class SoftDeleted:
    deletion_date = Column(DateTime, nullable=True)

@event.listens_for(orm.Session, "do_orm_execute")
def _add_filtering_criteria(execute_state: orm.ORMExecuteState) -> None:
    """Intercepts all ORM queries. Adds a with_loader_criteria option to all
    of them.
    This option applies to SELECT queries and adds a global WHERE criteria
    (or as appropriate ON CLAUSE criteria for join targets)
    to all objects of a certain class or superclass.
    """
    if (not execute_state.is_column_load  
            and not execute_state.is_relationship_load
            and not execute_state.execution_options.get("include_deleted", False)):
        execute_state.statement = execute_state.statement.options(
            orm.with_loader_criteria(
                SoftDeleted,
                lambda cls: cls.deletion_date.is_(None),
                include_aliases=True,
            )
        )
engine = sa.create_engine(url, echo=False).execution_options(include_deleted=True)
session_factory = orm.sessionmaker(bind=engine)
session = session_factory()

# let SomeClass be a class that inherits from the SoftDeleted mixin

myquery = session.query(SomeClass).get(1) 
# will not retrieve SomeClass with uid=1 if it is soft-deleted

myquery2 = session.query(SomeClass).execution_options(include_deleted=True).get(1) 
# will retrieve SomeClass with uid=1 even if it is soft-deleted

As I said, I would like that all queries of the session are able to include soft-deleted objects. Would someone know how I can do?

Solution, thanks to snakecharmerb's answer

After snakecharmerb answers, I modified the following and I got the wanted behaviour

@event.listens_for(orm.Session, "do_orm_execute")
def _add_filtering_criteria(execute_state):
if (not execute_state.is_column_load
        and not execute_state.is_relationship_load
        and not execute_state.execution_options.get("include_deleted", False)
        and not execute_state.session.info.get("include_deleted", False)):
[...]
engine = sa.create_engine(url, echo=False) \
        .execution_options(include_deleted=include_deleted)

session_factory = orm.sessionmaker(
    bind=engine,
    info={'include_deleted':
          engine.get_execution_options().get('include_deleted', False)})
session = session_factory()
[...]

Solution

  • You have set your flag in the engine's execution options, so you must retrieve it from there. The engine can be accessed through the session's bind attribute:

    (Pdb) execute_state.session.bind.get_execution_options()
    immutabledict({'include_deleted': True})
    

    Your code does not error because execute_state has an execution_options attribute, but it contains other things:

    (Pdb) execute_state.execution_options
    immutabledict({'_sa_orm_load_options': <class 'sqlalchemy.orm.context.QueryContext.default_load_options'>, '_result_disable_adapt_to_context': True, 'future_result': True})
    

    Session objects (and sessionsmakers) have an info attribute, a dictionary that you can populate as you wish. This could used to pass the flag if you wanted to set the flag per session rather than per engine.