Updating Query with Soft Delete for SQLAlchemy 1.4
When working on a database, there are times where we want to delete a row, but at the same time we still want to keep around the record that the particular row was referring to. There can be many reasons for this, however the basic idea is that we want the row to generally be hidden. The Soft Delete pattern allows for the 'delete' operation, to hide the row from the external interface, while it remains within the database. Miguel Grinberg has a great writeup of the pattern and implementation in SQLAlchemy on their blog.
This implementation is great, however the SQLAlchemy 1.4 release changed some of the internals meaning Miguel's implementation no longer works, giving the below error
AttributeError: 'QueryWithSoftDelete' object has no attribute '_mapper_zero'
The fix for this issue is to replace the _mapper_zero
method with the _entity_from_pre_ent_zero
method, giving a with_deleted
method as below.
class QueryWithSoftDelete(BaseQuery):
...
def with_deleted(self):
return self.__class__(
db.class_mapper(self._entity_from_pre_ent_zero().class_),
session=db.session(),
_with_deleted=True
)
Why does this work?
With the fix out of the way,
we can have a look at what the with_deleted
method is doing.
The entire code snippet for the QueryWithSoftDelete
is reproduced below for reference.
class QueryWithSoftDelete(BaseQuery):
def __new__(cls, *args, **kwargs):
obj = super(QueryWithSoftDelete, cls).__new__(cls)
with_deleted = kwargs.pop('_with_deleted', False)
if len(args) > 0:
super(QueryWithSoftDelete, obj).__init__(*args, **kwargs)
return obj.filter_by(deleted=False) if not with_deleted else obj
return obj
def __init__(self, *args, **kwargs):
pass
def with_deleted(self):
return self.__class__(
db.class_mapper(self._entity_from_pre_ent_zero().class_),
session=db.session(),
_with_deleted=True
)
The with_deleted
method provides a way to transform the Query
from one that filters out deleted values to one that does.
Now there isn't a straightforward method for removing filters from a query,
so the approach taken here is to create a new Query,
this time setting _with_deleted=True
.
The problem now becomes,
which model are we trying to query?
When we call the with_deleted
method,
we are doing so from an instance of the QueryWithSoftDelete
class,
so we are unable to extract the model being used through
methods like __class__
, it gives us the QueryWithSoftDelete
class.
So instead we can look to SQLAlchemy to get this information.
Within SQLAlchemy, every query from the database needs a FROM
clause,
the table that we are going to be using to initially select our data.
This table we are selecting,
is the same one that we want to query
though this time instead of filtering the deleted items,
we will be including them.
Previously the _mapper_zero
method provided that table in the FROM
clause.
However the restructuring of the Query to consolidate the core and ORM components
meant the method disappeared,
completely fine given it is a 'hidden' method.
We need the function that will give the equivalent behaviour,
which happens to be the much more verbose, _entitiy_from_pre_ent_zero
.