My Flask-Restful application has a number of "objects". In the first version of the app these are simple data structures with no behaviour, implemented as Dicts, or lists of Dicts.
The attributes of these "objects" can change. I use a generator function to track the changes, and then alert web-clients via server-sent-events (SSEs). This works by maintaining an "old" copy of the object to be tracked, and comparing it to the latest state.
In the next version of the app I populate the "objects" from a SQLite DB using SQLAlchemy. The objects are now implemented as SQLAlchemy declarative classes, or lists of such classes.
To compare "old" and "new" instances based on equality of attributes only I had to add an __eq__
override to
my SQLAlchemy Objects. i.e. the instances are considered equal / unchanged when the attributes have the same values.
(I have posted example code at the bottom of this question).
Technically this works, but raises some architectural alarm bells: Am I sailing in the wrong direction?
a) If I add __eq__
and __ne__
overrides to SQAlchemy objects, could this cause SQLAlchemy a problem when I later want
to re-persist the objects back to the database?
b) How far into my application should the SQLAlchemy objects reach: is there a "pythonic best practice"? i.e. Is it ok / normal to extend SQLAlchemy objects with business logic / behaviours unconnected with DB persistence (such as tracking changes); or should they be used only as simple DTOs between the database and server, with business logic in other objects?
Note: it is clear to me that the data presented to the clients via the REST apis and the SSEs should be abstracted from the implementation details in the web-server and DB, so that is not part of this question.
sqlalchemy id equality vs reference equality https://codereview.stackexchange.com/questions/93511/data-transfer-objects-vs-entities-in-java-rest-server-application http://www.mehdi-khalili.com/orm-anti-patterns-part-4-persistence-domain-model/
class EqualityMixin(object):
# extended from the concept in :
# https://stackoverflow.com/questions/390250/elegant-ways-to-support-equivalence-equality-in-python-classes
def __eq__(self, other):
classes_match = isinstance(other, self.__class__)
a, b = deepcopy(self.__dict__), deepcopy(other.__dict__)
#compare based on equality our attributes, ignoring SQLAlchemy internal stuff
a.pop('_sa_instance_state', None)
b.pop('_sa_instance_state', None)
attrs_match = (a == b)
return classes_match and attrs_match
def __ne__(self, other):
return not self.__eq__(other)
The == operator compares the value or equality of two objects, whereas the Python is operator checks whether two variables point to the same object in memory. In the vast majority of cases, this means you should use the equality operators == and != , except when you're comparing to None .
Both “is” and “==” are used for object comparison in Python. The operator “==” compares values of two objects, while “is” checks if two objects are same (In other words two references to same object). The “==” operator does not tell us whether x1 and x2 are actually referring to the same object or not.
_sa_instance_state is a non-database-persisted value used by SQLAlchemy internally (it refers to the InstanceState for the instance.
I'll drill down into what's happening behind the Base class, to show that the __eq__
and __ne__
overrides are fine. When you instantiate your Base
class by calling declarative_base()
, it's using a metaclass behind the scenes to set it up (It might be worth reading this explanation this metaclass explanation to better understand why it is involved). It does some configurable setup, like adding a custom constructor to your Base
class and setting how it will map from the object to a table.
declarative_base()
is then going to return a new Base
class instance of a DeclarativeMeta
metaclass. The whole reason metaclasses are involved here are so that at the time when you create a class that extends your Base
, it will map it to a table. If you trace down this path a little way, you will see how it maps the Columns you declare on your object to a table.
self.cls.__mapper__ = mp_ = mapper_cls( self.cls, # cls is your model self.local_table, **self.mapper_args # the columns you have defined )
Although the actual Mapper which is doing this looks like it gets very complicated and low level, at this stage it's operating with primary keys and columns rather than actual object instances. This doesn't confirm that it's never used, however, so I looked through the usages of ==
and !=
on the source and didn't see any causes for concern.
As for your second question I can only really offer my own opinion - I've googled around this subject many times in the past and haven't found much in the way of 'gold standard' SQL Alchemy usage. I've used SQL Alchemy for a couple of projects so far and it feels like your use of the objects can extend about as far as you can still sensibly abstract away the session
life-cycle. To me it seems like enough of the Alchemy "magic" is abstracted away from the models themselves that when sessions are handled well, they are sufficiently far removed from the data layer that it doesn't feel like business logic in the classes would get in the way.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With