1. idank
  2. sqlalchemy

Commits

Mike Bayer  committed 2c1787a

- added session.is_modified(obj) method; performs the same "history" comparison operation
as occurs within a flush operation; setting include_collections=False gives the same
result as is used when the flush determines whether or not to issue an UPDATE for the
instance's row.

  • Participants
  • Parent commits f22ae85
  • Branches default

Comments (0)

Files changed (5)

File CHANGES

View file
  • Ignore whitespace
   adequate __eq__() implementation can be set up with "PickleType(comparator=operator.eq)"
   [ticket:560]
 
+- added session.is_modified(obj) method; performs the same "history" comparison operation
+  as occurs within a flush operation; setting include_collections=False gives the same
+  result as is used when the flush determines whether or not to issue an UPDATE for the
+  instance's row.
+
 - added "schema" argument to Sequence; use this with Postgres /Oracle when the sequence is
   located in an alternate schema.  Implements part of [ticket:584], should fix [ticket:761].
   

File lib/sqlalchemy/orm/attributes.py

View file
  • Ignore whitespace
             return self
         return self.impl.get(obj._state)
 
+    def get_history(self, obj, **kwargs):
+        return self.impl.get_history(obj._state, **kwargs)
+        
     def clause_element(self):
         return self.comparator.clause_element()
 

File lib/sqlalchemy/orm/scoping.py

View file
  • Ignore whitespace
     def do(self, *args, **kwargs):
         return getattr(self.registry(), name)(*args, **kwargs)
     return do
-for meth in ('get', 'load', 'close', 'save', 'commit', 'update', 'flush', 'query', 'delete', 'merge', 'clear', 'refresh', 'expire', 'expunge', 'rollback', 'begin', 'begin_nested', 'connection', 'execute', 'scalar', 'get_bind'):
+for meth in ('get', 'load', 'close', 'save', 'commit', 'update', 'flush', 'query', 'delete', 'merge', 'clear', 'refresh', 'expire', 'expunge', 'rollback', 'begin', 'begin_nested', 'connection', 'execute', 'scalar', 'get_bind', 'is_modified'):
     setattr(ScopedSession, meth, instrument(meth))
 
 def makeprop(name):

File lib/sqlalchemy/orm/session.py

View file
  • Ignore whitespace
         
         return iter(list(self.uow.new) + self.uow.identity_map.values())
 
+    def is_modified(self, obj, include_collections=True, passive=False):
+        """return True if the given object has modified attributes.
+        
+        This method retrieves a history instance for each instrumented attribute
+        on the instance and performs a comparison of the current value to its
+        previously committed value.  Note that instances present in the 'dirty'
+        collection may result in a value of ``False`` when tested with this method.
+        
+        'include_collections' indicates if multivalued collections should be included
+        in the operation.  Setting this to False is a way to detect only local-column
+        based properties (i.e. scalar columns or many-to-one foreign keys) that would
+        result in an UPDATE for this instance upon flush.
+        
+        the 'passive' flag indicates if unloaded attributes and collections should
+        not be loaded in the course of performing this test.
+        """
+
+        for attr in attribute_manager.managed_attributes(obj.__class__):
+            if not include_collections and hasattr(attr.impl, 'get_collection'):
+                continue
+            if attr.get_history(obj).is_modified():
+                return True
+        return False
+        
     dirty = property(lambda s:s.uow.locate_dirty(),
-                     doc="A ``Set`` of all objects marked as 'dirty' within this ``Session``")
+                     doc="""A ``Set`` of all objects marked as 'dirty' within this ``Session``.  
+                     
+                     Note that the 'dirty' state here is 'optimistic'; most attribute-setting or collection
+                     modification operations will mark an instance as 'dirty' and place it in this set,
+                     even if there is no net change to the attribute's value.  At flush time, the value 
+                     of each attribute is compared to its previously saved value,
+                     and if there's no net change, no SQL operation will occur (this is a more expensive
+                     operation so it's only done at flush time).
+                     
+                     To check if an instance has actionable net changes to its attributes, use the
+                     is_modified() method.
+                     """)
 
     deleted = property(lambda s:s.uow.deleted,
                        doc="A ``Set`` of all objects marked as 'deleted' within this ``Session``")

File test/orm/session.py

View file
  • Ignore whitespace
         assert user in s
         assert user not in s.dirty
     
+    def test_is_modified(self):
+        s = create_session()
+        class User(object):pass
+        class Address(object):pass
+        
+        mapper(User, users, properties={'addresses':relation(Address)})
+        mapper(Address, addresses)
+        
+        # save user
+        u = User()
+        u.user_name = 'fred'
+        s.save(u)
+        s.flush()
+        s.clear()
+        
+        user = s.query(User).one()
+        assert user not in s.dirty
+        assert not s.is_modified(user)
+        user.user_name = 'fred'
+        assert user in s.dirty
+        assert not s.is_modified(user)
+        user.user_name = 'ed'
+        assert user in s.dirty
+        assert s.is_modified(user)
+        s.flush()
+        assert user not in s.dirty
+        assert not s.is_modified(user)
+        
+        a = Address()
+        user.addresses.append(a)
+        assert user in s.dirty
+        assert s.is_modified(user)
+        assert not s.is_modified(user, include_collections=False)
+        
+        
     def test_weak_ref(self):
         """test the weak-referencing identity map, which strongly-references modified items."""