Maxim
Maxim

Reputation: 632

Sqlalchemy doesn't call model's reconstructor on object refresh

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

Answers (1)

Ilja Everilä
Ilja Everilä

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 - see InstanceEvents 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

Related Questions