On Sep 27, 2010, at 6:15 PM, Ted Turocy wrote:
> Hello,
>
> I am puzzled by some behavior of is_modified() I am seeing.
>
> Here is a sample program which illustrates the situation. I'm running
> this on 0.6.4:
This is a limitation of is_modified(), and it is definitely a bug here that
this limitation is not documented (ticket 1928).
Its a serious performance hit for us to load the "old" value for a simple
scalar attribute when a set event occurs. In this test, all attributes are
expired after each commit(). In the second case, the test does not access
instance.cans ahead of time, so when "42" is assigned, the set event is
registered, the old value is not loaded, and history-wise we see a value of
"42" with a constant of NO_VALUE to compare it against, so it counts as history.
If the is_modified() method and in turn the attributes.get_history() facility
were to be enhanced such that the attribute history function could reach
upwards to the ORM's un-expiration functionality and load the "old" value of
the attribute as though it were being accessed, then the value would be present
for a true compare - however this is not functionality the ORM internals use
right now. Scalar attributes just get bundled into the UPDATE statement if we
aren't sure what the "previous" value was, rather than going back and issuing
another SELECT. This again is much more performant in the vast majority of
use cases, though certainly not all.
A workaround is to enable full history tracking for the attribute, so that the
"old" value is loaded upon a set event. This is not public API at the moment
but it would look like:
Spam.cans.impl.active_history = True
or, set expire_on_commit=False on your session.
The "active_history" feature should become public API at some point. The
alternative notion of modifying attributes.get_history() to get the "old" value
to load from a SELECT at the point of history, rather than upon attribute set,
would be doable but would require more intensive changes, as we'd have to break
out the "load the old value" behavior into a separate function that calls upon
loader callables in such a way that they know not to populate the current
instance dictionary. The feature would still not be ideal as a default within
the ORM since you'd see many more SELECTs being emitted within a typical
flush() against a set of persistent, modified objects, most of which are
unnecessary.
>
> -----
> import sqlalchemy
> import sqlalchemy.orm
> from sqlalchemy import Table, Column, Integer
>
> class Spam(object):
> def __init__(self, cans):
> self.cans = cans
>
> engine = sqlalchemy.create_engine('sqlite:///:memory:')
> Session = sqlalchemy.orm.sessionmaker(autocommit=False, bind=engine)
> session = Session()
> meta = sqlalchemy.MetaData()
> meta.bind = engine
>
> spam_table = Table('spam', meta,
> Column('id', Integer, primary_key=True),
> Column('cans', Integer)
> )
>
> meta.create_all(engine)
>
> sqlalchemy.orm.mapper(Spam, spam_table)
>
> # Setup finished. Now test things out
> instance = Spam(42)
> session.add(instance)
> session.commit()
>
> print "Cans of spam = ", instance.cans
> instance.cans = 42
> print "Has instance been modified? (Should be False): ",
> session.is_modified(instance)
> session.commit()
>
> instance.cans = 42
> print "We have made no net change to the instance..."
> print "Has instance been modified? (Should be False): ",
> session.is_modified(instance)
> -----
>
> When I run this, I get:
>
> -----
> Cans of spam = 42
> Has instance been modified? (Should be False): False
> We have made no net change to the instance...
> Has instance been modified? (Should be False): True
> -----
>
> That's surprising to me. In both cases, no net change is being made to
> the instance.
>
> Noodling around further, it appears that this is triggered by re-using
> the same instance object, and doing another __setattr__ on the column
> without having done a __getattr__ on it earlier. I suspect, without
> having dug around, that after the commit, the instance's attribute
> values are marked as needing to be re-loaded, but they are not being
> re-loaded first in order to check whether the new value is in fact
> different than the existing one.
>
> I've also noticed if I re-obtain the instance via a query using .get()
> on the primary key, the behavior is the expected one, again presumably
> because the query operation is loading in all the column values.
>
> Am I missing something here?
>
> Ted
>
> --
> You received this message because you are subscribed to the Google Groups
> "sqlalchemy" group.
> To post to this group, send email to [email protected].
> To unsubscribe from this group, send email to
> [email protected].
> For more options, visit this group at
> http://groups.google.com/group/sqlalchemy?hl=en.
>
--
You received this message because you are subscribed to the Google Groups
"sqlalchemy" group.
To post to this group, send email to [email protected].
To unsubscribe from this group, send email to
[email protected].
For more options, visit this group at
http://groups.google.com/group/sqlalchemy?hl=en.