From: Mike Bayer Date: Thu, 26 Aug 2010 15:32:50 +0000 (-0400) Subject: - An object that's been deleted now gets a flag X-Git-Tag: rel_0_6_4~29 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=af9fd453c08aac4f4e45f6f6ba94da89b42afe54;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - An object that's been deleted now gets a flag 'deleted', which prohibits the object from being re-add()ed to the session, as previously the object would live in the identity map silently until its attributes were accessed. The make_transient() function now resets this flag along with the "key" flag. - make_transient() can be safely called on an already transient instance. --- diff --git a/CHANGES b/CHANGES index 8fe4c49f56..6ac7acd74c 100644 --- a/CHANGES +++ b/CHANGES @@ -15,6 +15,17 @@ CHANGES ConcurrentModificationError in an "except:" clause. + - An object that's been deleted now gets a flag + 'deleted', which prohibits the object from + being re-add()ed to the session, as previously + the object would live in the identity map + silently until its attributes were accessed. + The make_transient() function now resets this + flag along with the "key" flag. + + - make_transient() can be safely called on an + already transient instance. + - a warning is emitted in mapper() if the polymorphic_on column is not present either in direct or derived form in the mapped selectable or in the diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index 86d5dd7730..06d5b89a14 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -278,8 +278,11 @@ class SessionTransaction(object): for s in set(self._new).union(self.session._new): self.session._expunge_state(s) - + for s in set(self._deleted).union(self.session._deleted): + if s.deleted: + # assert s in self._deleted + del s.deleted self.session._update_impl(s) assert not self.session._deleted @@ -1102,6 +1105,7 @@ class Session(object): self.identity_map.discard(state) self._deleted.pop(state, None) + state.deleted = True def _save_without_cascade(self, instance): """Used by scoping.py to save on init without cascade.""" @@ -1309,7 +1313,13 @@ class Session(object): raise sa_exc.InvalidRequestError( "Instance '%s' is not persisted" % mapperutil.state_str(state)) - + + if state.deleted: + raise sa_exc.InvalidRequestError( + "Instance '%s' has been deleted. Use the make_transient() " + "function to send this object back to the transient state." % + mapperutil.state_str(state) + ) self._attach(state) self._deleted.pop(state, None) self.identity_map.add(state) @@ -1655,7 +1665,9 @@ def make_transient(instance): This will remove its association with any session and additionally will remove its "identity key", such that it's as though the object were newly constructed, - except retaining its values. + except retaining its values. It also resets the + "deleted" flag on the state if this object + had been explicitly deleted by its session. Attributes which were "expired" or deferred at the instance level are reverted to undefined, and @@ -1670,8 +1682,10 @@ def make_transient(instance): # remove expired state and # deferred callables state.callables.clear() - del state.key - + if state.key: + del state.key + if state.deleted: + del state.deleted def object_session(instance): """Return the ``Session`` to which instance belongs. diff --git a/lib/sqlalchemy/orm/state.py b/lib/sqlalchemy/orm/state.py index 82e7e91301..f6828f5a9a 100644 --- a/lib/sqlalchemy/orm/state.py +++ b/lib/sqlalchemy/orm/state.py @@ -22,6 +22,7 @@ class InstanceState(object): _strong_obj = None modified = False expired = False + deleted = False def __init__(self, obj, manager): self.class_ = obj.__class__ diff --git a/test/orm/test_expire.py b/test/orm/test_expire.py index c0c96f8730..c8bdf1719e 100644 --- a/test/orm/test_expire.py +++ b/test/orm/test_expire.py @@ -89,6 +89,10 @@ class ExpireTest(_fixtures.FixtureTest): assert s.query(User).get(10) is None assert u not in s # and expunges + # trick the "deleted" flag so we can re-add for the sake + # of this test + del attributes.instance_state(u).deleted + # add it back s.add(u) # nope, raises ObjectDeletedError diff --git a/test/orm/test_session.py b/test/orm/test_session.py index 779db304e2..4976db1311 100644 --- a/test/orm/test_session.py +++ b/test/orm/test_session.py @@ -280,6 +280,52 @@ class SessionTest(_fixtures.FixtureTest): assert u1.id is None assert u1.name is None + # works twice + make_transient(u1) + + sess.close() + + u1.name = 'test2' + sess.add(u1) + sess.flush() + assert u1 in sess + sess.delete(u1) + sess.flush() + assert u1 not in sess + + assert_raises(sa.exc.InvalidRequestError, sess.add, u1) + make_transient(u1) + sess.add(u1) + sess.flush() + assert u1 in sess + + @testing.resolve_artifact_names + def test_deleted_flag(self): + mapper(User, users) + + sess = sessionmaker()() + + u1 = User(name='u1') + sess.add(u1) + sess.commit() + + sess.delete(u1) + sess.flush() + assert u1 not in sess + assert_raises(sa.exc.InvalidRequestError, sess.add, u1) + sess.rollback() + assert u1 in sess + + sess.delete(u1) + sess.commit() + assert u1 not in sess + assert_raises(sa.exc.InvalidRequestError, sess.add, u1) + + make_transient(u1) + sess.add(u1) + sess.commit() + + eq_(sess.query(User).count(), 1) @testing.resolve_artifact_names def test_autoflush_expressions(self):