--- /dev/null
+.. change::
+ :tags: bug, orm
+ :tickets: 4872
+
+ Fixed regression in selectinload loader strategy caused by #4775 (released
+ in version 1.3.6) where a many-to-one attribute of None would no longer be
+ populated by the loader. While this was usually not noticeable due to the
+ lazyloader populating None upon get, it would lead to a detached instance
+ error if the object were detached.
# determine if our "lazywhere" clause is the same as the mapper's
# get() clause. then we can just use mapper.get()
+ #
+ # TODO: the "not self.uselist" can be taken out entirely; a m2o
+ # load that populates for a list (very unusual, but is possible with
+ # the API) can still set for "None" and the attribute system will
+ # populate as an empty list.
self.use_get = (
not self.is_aliased_class
and not self.uselist
if query_info.load_only_child:
our_states = collections.defaultdict(list)
+ none_states = []
mapper = self.parent
if attributes.PASSIVE_NO_RESULT in related_ident:
query_info = self._fallback_query_info
break
+
+ # organize states into lists keyed to particular foreign
+ # key values.
if None not in related_ident:
our_states[related_ident].append(
(state, state_dict, overwrite)
)
+ else:
+ # For FK values that have None, add them to a
+ # separate collection that will be populated separately
+ none_states.append((state, state_dict, overwrite))
+ # note the above conditional may have changed query_info
if not query_info.load_only_child:
our_states = [
(state.key[1], state, state.dict, overwrite)
q.add_criteria(_setup_outermost_orderby)
if query_info.load_only_child:
- self._load_via_child(our_states, query_info, q, context)
+ self._load_via_child(
+ our_states, none_states, query_info, q, context
+ )
else:
self._load_via_parent(our_states, query_info, q, context)
- def _load_via_child(self, our_states, query_info, q, context):
+ def _load_via_child(self, our_states, none_states, query_info, q, context):
uselist = self.uselist
# this sort is really for the benefit of the unit tests
while our_keys:
chunk = our_keys[0 : self._chunksize]
our_keys = our_keys[self._chunksize :]
-
data = {
k: v
for k, v in q(context.session).params(
dict_,
related_obj if not uselist else [related_obj],
)
+ # populate none states with empty value / collection
+ for state, dict_, overwrite in none_states:
+ if not overwrite and self.key in dict_:
+ continue
+
+ # note it's OK if this is a uselist=True attribute, the empty
+ # collection will be populated
+ state.get_impl(self.key).set_committed_value(state, dict_, None)
def _load_via_parent(self, our_states, query_info, q, context):
uselist = self.uselist
attributes.del_attribute(f1, "coll")
assert "coll" not in f1.__dict__
+ def test_set_commited_value_none_uselist(self):
+ """test that set_committed_value->None to a uselist generates an
+ empty list """
+
+ class Foo(object):
+ pass
+
+ class Bar(object):
+ pass
+
+ instrumentation.register_class(Foo)
+ instrumentation.register_class(Bar)
+ attributes.register_attribute(
+ Foo, "col_list", uselist=True, useobject=True
+ )
+ attributes.register_attribute(
+ Foo, "col_set", uselist=True, useobject=True, typecallable=set
+ )
+
+ f1 = Foo()
+ attributes.set_committed_value(f1, "col_list", None)
+ eq_(f1.col_list, [])
+
+ attributes.set_committed_value(f1, "col_set", None)
+ eq_(f1.col_set, set())
+
def test_initiator_arg(self):
class Foo(object):
pass
self.assert_sql_count(testing.db, go, 2)
+ def test_m2o_none_value_present(self):
+ orders, Order, addresses, Address = (
+ self.tables.orders,
+ self.classes.Order,
+ self.tables.addresses,
+ self.classes.Address,
+ )
+
+ mapper(
+ Order,
+ orders,
+ properties={"address": relationship(Address, lazy="selectin")},
+ )
+ mapper(Address, addresses)
+
+ sess = create_session()
+ q = sess.query(Order).filter(Order.id.in_([4, 5])).order_by(Order.id)
+
+ o4, o5 = q.all()
+ assert o4.__dict__["address"] is not None
+ assert o5.__dict__["address"] is None
+
+ # test overwrite
+
+ o5.address = Address()
+ sess.query(Order).filter(Order.id.in_([4, 5])).order_by(Order.id).all()
+ assert o5.__dict__["address"] is not None
+
+ o5.address = Address()
+ sess.query(Order).populate_existing().filter(
+ Order.id.in_([4, 5])
+ ).order_by(Order.id).all()
+ assert o5.__dict__["address"] is None
+
+ def test_m2o_uselist_none_value_present(self):
+ orders, Order, addresses, Address = (
+ self.tables.orders,
+ self.classes.Order,
+ self.tables.addresses,
+ self.classes.Address,
+ )
+
+ mapper(
+ Order,
+ orders,
+ properties={
+ "address": relationship(Address, lazy="selectin", uselist=True)
+ },
+ )
+ mapper(Address, addresses)
+
+ sess = create_session()
+ q = sess.query(Order).filter(Order.id.in_([4, 5])).order_by(Order.id)
+
+ o4, o5 = q.all()
+ assert len(o4.__dict__["address"])
+ eq_(o5.__dict__["address"], [])
+
+ def test_o2m_empty_list_present(self):
+ Address, addresses, users, User = (
+ self.classes.Address,
+ self.tables.addresses,
+ self.tables.users,
+ self.classes.User,
+ )
+
+ mapper(
+ User,
+ users,
+ properties=dict(
+ addresses=relationship(
+ mapper(Address, addresses), lazy="selectin"
+ )
+ ),
+ )
+ q = create_session().query(User)
+ result = q.filter(users.c.id == 10).all()
+ u1 = result[0]
+
+ eq_(u1.__dict__["addresses"], [])
+
def test_double_with_aggregate(self):
User, users, orders, Order = (
self.classes.User,
eq_(u1.id, 8)
self.assert_sql_count(testing.db, go, 2)
- assert 'addresses' in u1.__dict__
+ assert "addresses" in u1.__dict__
def test_no_query_on_deferred(self):
User, Address, sess = self._deferred_config_fixture()