On Sep 27, 2010, at 6:53 PM, Michael Bayer wrote:

> 
> 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.

just another thought, thinking about this, if we wanted to get into "hey let's 
refactor attributes yet again", I suppose we could hold onto the "old" value 
when we expire attributes, probably in the committed_state dictionary or 
something else.   Not a change I'm planning on anytime soon, it would 
destablize everything and it still wouldn't work in all cases, but something to 
think about.   A long time ago we tried making attributes hold onto their 
history and support rollback in place...it had lots of impossible edge cases.


> 
> 
> 
>> 
>> -----
>> 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 sqlalch...@googlegroups.com.
>> To unsubscribe from this group, send email to 
>> sqlalchemy+unsubscr...@googlegroups.com.
>> 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 sqlalch...@googlegroups.com.
> To unsubscribe from this group, send email to 
> sqlalchemy+unsubscr...@googlegroups.com.
> 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 sqlalch...@googlegroups.com.
To unsubscribe from this group, send email to 
sqlalchemy+unsubscr...@googlegroups.com.
For more options, visit this group at 
http://groups.google.com/group/sqlalchemy?hl=en.

Reply via email to