Reputation: 632
I have a model, which should convert some fields on initialization. I've tried to use sqlalchemy.orm.reconstructor
, but faced a problem (or misundersanding) of model auto-refreshing.
This is example of my model:
from app import db
from sqlalchemy.orm import reconstructor
t_model = db.Table(
'models',
db.metadata,
dbc.Column('id', db.Integer, primary_key=True),
dbc.Column('value', db.String)
)
class Model(db.Model):
__table__ = t_order_data
def __init__(self, value)
self.value = value
@reconstructor
def init_on_load(self)
"""cast to None if value is empty string"""
self.value = None if not self.value else self.value
def run_db_function(self)
try:
result = db.session.execute(db.func.somefunc('some_param')).fetchone()
except Exception:
db.session.rollback()
raise
else:
db.session.commit()
return result
What does my code do:
model = Model.query.get(1) # in table this row is id=1, value=''
which begins implicit transaction and selects model data.
model.value # returns None -- OK
Then i perform function
model.run_db_function()
which runs DB's routine and then commits transaction.
Then, when I try to access value
attribute, sqlalchemy begins new implicit transaction, in which re-fetches this model again.
But this time value won't be converted
model.value # returns '' -- NOT OK
Official documentation of sqlalchqmy ORM says about reconstructor
decorator:
Designates a method as the “reconstructor”, an
__init__
-like method that will be called by the ORM after the instance has been loaded from the database or otherwise reconstituted.
From this description I suppose that init_on_load
should be called after re-fetch too, but it is'nt happened.
Is this bug or by-design behavior? And how should I proceed if it's normal behavior?
Upvotes: 3
Views: 2558
Reputation: 52937
Quoting the official documentation:
reconstructor()
is a shortcut into a larger system of “instance level” events, which can be subscribed to using the event API - seeInstanceEvents
for the full API description of these events.
In essence reconstructor()
is a shortcut to InstanceEvents.load
:
Receive an object instance after it has been created via
__new__
, and after initial attribute population has occurred. ... This typically occurs when the instance is created based on incoming result rows, and is only called once for that instance’s lifetime.
When you commit or rollback the default is to just expire all ORM controlled attributes of the instances found in the session. The instances themselves stay alive. When you refetch a row that matches an instance found in the session, it is refreshed. An example:
In [2]: from sqlalchemy.events import event
In [3]: class Foo(Base):
...: id = Column(Integer, primary_key=True, autoincrement=True)
...: value = Column(Unicode)
...: __tablename__ = 'foo'
...:
Add event listeners so we can track what is going on
In [5]: @event.listens_for(Foo, 'load')
...: def foo_load(target, context):
...: print('Foo load')
...:
In [6]: @event.listens_for(Foo, 'refresh')
...: def foo_load(target, context, attrs):
...: print('Foo refresh')
...:
In [7]: session.add(Foo(value='test'))
In [8]: session.commit()
Foo instance is constructed
In [9]: foo = session.query(Foo).first()
Foo load
In [10]: session.rollback()
The now expired instance found in the session is refreshed and returned
In [11]: foo2 = session.query(Foo).first()
Foo refresh
Both names are bound to the same instance
In [12]: foo is foo2
Out[12]: True
In [13]: session.rollback()
Attribute access on an expired instance causes just a refresh
In [14]: foo.value
Foo refresh
Out[14]: 'test'
Upvotes: 5