eagerloading on the reverse many-to-one side, since
that loading is by definition unnecessary. [ticket:1495]
+ - Session.refresh() now does an equivalent expire()
+ on the given instance first, so that the "refresh-expire"
+ cascade is propagated. Previously, refresh() was
+ not affected in any way by the presence of "refresh-expire"
+ cascade. This is a change in behavior versus that
+ of 0.6beta2, where the "lockmode" flag passed to refresh()
+ would cause a version check to occur. Since the instance
+ is first expired, refresh() always upgrades the object
+ to the most recent version.
+
+ - The 'refresh-expire' cascade, when reaching a pending object,
+ will expunge the object if the cascade also includes
+ "delete-orphan", or will simply detach it otherwise.
+ [ticket:1754]
+
0.6beta3
========
else:
instances = state.value_as_iterable(self.key, passive=passive)
+ skip_pending = type_ == 'refresh-expire' and 'delete-orphan' not in self.cascade
+
if instances:
for c in instances:
if c is not None and \
str(self.parent.class_),
str(c.__class__)
))
+ instance_state = attributes.instance_state(c)
+
+ if skip_pending and not instance_state.key:
+ continue
+
visited_instances.add(c)
# cascade using the mapper local to this
# object, so that its individual properties are located
- instance_mapper = object_mapper(c)
- yield (c, instance_mapper, attributes.instance_state(c))
+ instance_mapper = instance_state.manager.mapper
+ yield (c, instance_mapper, instance_state)
def _add_reverse_property(self, key):
other = self.mapper._get_property(key)
state.commit_all(dict_, self.identity_map)
def refresh(self, instance, attribute_names=None, lockmode=None):
- """Refresh the attributes on the given instance.
+ """Expire and refresh the attributes on the given instance.
A query will be issued to the database and all attributes will be
refreshed with their current database value.
state = attributes.instance_state(instance)
except exc.NO_STATE:
raise exc.UnmappedInstanceError(instance)
- self._validate_persistent(state)
+
+ self._expire_state(state, attribute_names)
+
if self.query(_object_mapper(instance))._get(
state.key, refresh_state=state,
lockmode=lockmode,
state = attributes.instance_state(instance)
except exc.NO_STATE:
raise exc.UnmappedInstanceError(instance)
+ self._expire_state(state, attribute_names)
+
+ def _expire_state(self, state, attribute_names):
self._validate_persistent(state)
if attribute_names:
_expire_state(state, state.dict,
- attribute_names=attribute_names, instance_dict=self.identity_map)
+ attribute_names=attribute_names,
+ instance_dict=self.identity_map)
else:
# pre-fetch the full cascade since the expire is going to
# remove associations
cascaded = list(_cascade_state_iterator('refresh-expire', state))
- _expire_state(state, state.dict, None, instance_dict=self.identity_map)
+ self._conditional_expire(state)
for (state, m, o) in cascaded:
- _expire_state(state, state.dict, None, instance_dict=self.identity_map)
-
+ self._conditional_expire(state)
+
+ def _conditional_expire(self, state):
+ """Expire a state if persistent, else expunge if pending"""
+
+ if state.key:
+ _expire_state(state, state.dict, None, instance_dict=self.identity_map)
+ elif state in self._new:
+ self._new.pop(state)
+ state.detach()
+
def prune(self):
"""Remove unreferenced instances cached in the identity map.
no_support('sybase', 'no SEQUENCE support'),
)
+def update_nowait(fn):
+ """Target database must support SELECT...FOR UPDATE NOWAIT"""
+ return _chain_decorators_on(
+ fn,
+ no_support('access', 'no FOR UPDATE NOWAIT support'),
+ no_support('firebird', 'no FOR UPDATE NOWAIT support'),
+ no_support('mssql', 'no FOR UPDATE NOWAIT support'),
+ no_support('mysql', 'no FOR UPDATE NOWAIT support'),
+ no_support('sqlite', 'no FOR UPDATE NOWAIT support'),
+ no_support('sybase', 'no FOR UPDATE NOWAIT support'),
+ )
+
def subqueries(fn):
"""Target database must support subqueries."""
return _chain_decorators_on(
from sqlalchemy.test.schema import Column
from sqlalchemy.orm import mapper, relationship, create_session, \
attributes, deferred, exc as orm_exc, defer, undefer,\
- strategies, state, lazyload
+ strategies, state, lazyload, backref
from test.orm import _base, _fixtures
u.addresses[0].email_address = 'someotheraddress'
s.expire(u)
- u.name
- print attributes.instance_state(u).dict
assert u.addresses[0].email_address == 'ed@wood.com'
+ @testing.resolve_artifact_names
+ def test_refresh_cascade(self):
+ mapper(User, users, properties={
+ 'addresses':relationship(Address, cascade="all, refresh-expire")
+ })
+ mapper(Address, addresses)
+ s = create_session()
+ u = s.query(User).get(8)
+ assert u.addresses[0].email_address == 'ed@wood.com'
+
+ u.addresses[0].email_address = 'someotheraddress'
+ s.refresh(u)
+ assert u.addresses[0].email_address == 'ed@wood.com'
+
+ def test_expire_cascade_pending_orphan(self):
+ cascade = 'save-update, refresh-expire, delete, delete-orphan'
+ self._test_cascade_to_pending(cascade, True)
+
+ def test_refresh_cascade_pending_orphan(self):
+ cascade = 'save-update, refresh-expire, delete, delete-orphan'
+ self._test_cascade_to_pending(cascade, False)
+
+ def test_expire_cascade_pending(self):
+ cascade = 'save-update, refresh-expire'
+ self._test_cascade_to_pending(cascade, True)
+
+ def test_refresh_cascade_pending(self):
+ cascade = 'save-update, refresh-expire'
+ self._test_cascade_to_pending(cascade, False)
+
+ @testing.resolve_artifact_names
+ def _test_cascade_to_pending(self, cascade, expire_or_refresh):
+ mapper(User, users, properties={
+ 'addresses':relationship(Address, cascade=cascade)
+ })
+ mapper(Address, addresses)
+ s = create_session()
+
+ u = s.query(User).get(8)
+ a = Address(email_address='foobar')
+
+ u.addresses.append(a)
+ if expire_or_refresh:
+ s.expire(u)
+ else:
+ s.refresh(u)
+ if "delete-orphan" in cascade:
+ assert a not in s
+ else:
+ assert a in s
+
+ assert a not in u.addresses
+ s.flush()
+
@testing.resolve_artifact_names
def test_expired_lazy(self):
mapper(User, users, properties={
import sqlalchemy as sa
from sqlalchemy.test import engines, testing
-from sqlalchemy import Integer, String, ForeignKey, literal_column, orm
+from sqlalchemy import Integer, String, ForeignKey, literal_column, orm, exc
from sqlalchemy.test.schema import Table, Column
from sqlalchemy.orm import mapper, relationship, create_session, column_property, sessionmaker
from sqlalchemy.test.testing import eq_, ne_, assert_raises, assert_raises_message
return _uuids.pop(0)
class VersioningTest(_base.MappedTest):
+
@classmethod
def define_tables(cls, metadata):
Table('version_table', metadata,
s1.query(Foo).with_lockmode('read').get, f1s1.id
)
- # load, version is wrong
- assert_raises(
- sa.orm.exc.ConcurrentModificationError,
- s1.refresh, f1s1, lockmode='read'
- )
-
- # reload it
- s1.query(Foo).populate_existing().get(f1s1.id)
+ # reload it - this expires the old version first
+ s1.refresh(f1s1, lockmode='read')
# now assert version OK
s1.query(Foo).with_lockmode('read').get(f1s1.id)
# assert brand new load is OK too
s1.close()
s1.query(Foo).with_lockmode('read').get(f1s1.id)
+
+
+ @testing.emits_warning(r'.*does not support updated rowcount')
+ @engines.close_open_connections
+ @testing.requires.update_nowait
+ @testing.resolve_artifact_names
+ def test_versioncheck_for_update(self):
+ """query.with_lockmode performs a 'version check' on an already loaded instance"""
+
+ s1 = create_session(autocommit=False)
+
+ mapper(Foo, version_table, version_id_col=version_table.c.version_id)
+ f1s1 = Foo(value='f1 value')
+ s1.add(f1s1)
+ s1.commit()
+
+ s2 = create_session(autocommit=False)
+ f1s2 = s2.query(Foo).get(f1s1.id)
+ s2.refresh(f1s2, lockmode='update')
+ f1s2.value='f1 new value'
+ assert_raises(
+ exc.DBAPIError,
+ s1.refresh, f1s1, lockmode='update_nowait'
+ )
+ s1.rollback()
-
+ s2.commit()
+ s1.refresh(f1s1, lockmode='update_nowait')
+ assert f1s1.version_id == f1s2.version_id
@testing.emits_warning(r'.*does not support updated rowcount')
@engines.close_open_connections