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