"hasparent" flags on objects as they are loaded, both from lazy and eager loads,
the "orphan" check now uses an "optimistic" flag to determine the result if no
"hasparent" flag is found for a particular relationship on an instance. if the
instance has an _instance_key and therefore was loaded from the database, it is
assumed to not be an orphan unless a "False" hasparent flag has been set. if the
instance does not have an _instance_key and is therefore transient/pending, it is
assumed to be an orphan unless a "True" hasparent flag has been set.
- unit-of-work does a better check for "orphaned" objects that are
part of a "delete-orphan" cascade, for certain conditions where the
parent isnt available to cascade from.
+- mappers can tell if one of their objects is an "orphan" based
+on interactions with the attribute package. this check is based
+on a status flag maintained for each relationship
+when objects are attached and detached from each other. if the
+status flag is not present, its assumed to be "False" for a
+transient instance and assumed to be "True" for a persisted/detached
+ instance.
- it is now invalid to declare a self-referential relationship with
"delete-orphan" (as the abovementioned check would make them impossible
to save)
return self
return self.get(obj)
- def hasparent(self, item):
- """returns True if the given item is attached to a parent object
- via the attribute represented by this InstrumentedAttribute."""
- return item._state.get(('hasparent', id(self)))
+ def hasparent(self, item, optimistic=False):
+ """return True if the given item is attached to a parent object
+ via the attribute represented by this InstrumentedAttribute.
+
+ optimistic indicates what we should return if the given item has no "hasparent"
+ record at all for the given attribute."""
+ return item._state.get(('hasparent', id(self)), optimistic)
def sethasparent(self, item, value):
"""sets a boolean flag on the given item corresponding to whether or not it is
return InstrumentedAttribute.PASSIVE_NORESULT
values = callable_()
l = InstrumentedList(self, obj, self._adapt_list(values), init=False)
- if self.trackparent and values is not None:
- [self.sethasparent(v, True) for v in values if v is not None]
+
+ # mark loaded instances with "hasparent" status. commented out
+ # because loaded objects use "optimistic" parent-checking
+ #if self.trackparent and values is not None:
+ # [self.sethasparent(v, True) for v in values if v is not None]
+
# if a callable was executed, then its part of the "committed state"
# if any, so commit the newly loaded data
orig = state.get('original', None)
return InstrumentedAttribute.PASSIVE_NORESULT
value = callable_()
obj.__dict__[self.key] = value
- if self.trackparent and value is not None:
- self.sethasparent(value, True)
+
+ # mark loaded instances with "hasparent" status. commented out
+ # because loaded objects use "optimistic" parent-checking
+ #if self.trackparent and value is not None:
+ # self.sethasparent(value, True)
+
# if a callable was executed, then its part of the "committed state"
# if any, so commit the newly loaded data
orig = state.get('original', None)
#self.compile()
def _is_orphan(self, obj):
+ optimistic = hasattr(obj, '_instance_key')
for (key,klass) in self.delete_orphans:
- if not getattr(klass, key).hasparent(obj):
+ if not getattr(klass, key).hasparent(obj, optimistic=optimistic):
return True
else:
return False
assert p1.blog is b
assert p1 in b.posts
- # no orphans
- assert getattr(Blog, 'posts').hasparent(p1)
- assert getattr(Post, 'blog').hasparent(b)
+ # no orphans (but we are using optimistic checks)
+ assert getattr(Blog, 'posts').hasparent(p1, optimistic=True)
+ assert getattr(Post, 'blog').hasparent(b, optimistic=True)
+ # lazy loads currently not processed for "hasparent" status, so without
+ # optimistic, it returns false
+ assert not getattr(Blog, 'posts').hasparent(p1, optimistic=False)
+ assert not getattr(Post, 'blog').hasparent(b, optimistic=False)
+
+ # ok what about non-optimistic. well, dont use lazy loaders,
+ # assign things manually, so the "hasparent" flags get set
+ b2 = Blog()
+ p2 = Post()
+ b2.posts.append(p2)
+ assert getattr(Blog, 'posts').hasparent(p2, optimistic=False)
+ assert getattr(Post, 'blog').hasparent(b2, optimistic=False)
def testinheritance(self):
"""tests that attributes are polymorphic"""
{'user_id' : 9, 'addresses' : (Address, [])},
)
+ def testorphanstate(self):
+ """test that a lazily loaded child object is not marked as an orphan"""
+ m = mapper(User, users, properties={
+ 'addresses':relation(Address, cascade="all,delete-orphan", lazy=True)
+ })
+ mapper(Address, addresses)
+
+ q = create_session().query(m)
+ user = q.get(7)
+ assert getattr(User, 'addresses').hasparent(user.addresses[0], optimistic=True)
+ assert not class_mapper(Address)._is_orphan(user.addresses[0])
+
def testlimit(self):
ordermapper = mapper(Order, orders, properties = dict(
items = relation(mapper(Item, orderitems), lazy = True)
},
)
+ def testorphanstate(self):
+ """test that an eagerly loaded child object is not marked as an orphan"""
+ m = mapper(User, users, properties={
+ 'addresses':relation(Address, cascade="all,delete-orphan", lazy=False)
+ })
+ mapper(Address, addresses)
+
+ s = create_session()
+ q = s.query(m)
+ user = q.get(7)
+ assert getattr(User, 'addresses').hasparent(user.addresses[0], optimistic=True)
+ assert not class_mapper(Address)._is_orphan(user.addresses[0])
+
def testwithrepeat(self):
"""tests a one-to-many eager load where we also query on joined criterion, where the joined
criterion is using the same tables that are used within the eager load. the mapper must insure that the