diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2013-09-06 21:39:36 -0400 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2013-09-06 21:39:36 -0400 |
commit | 5a6895471fb6bf9afe9bdf017f1fa2c6246ae303 (patch) | |
tree | 5c14811273d5b2e328a59e323593535bfa112b86 | |
parent | e8167548429b9d4937caaa09740ffe9bdab1ef61 (diff) | |
download | sqlalchemy-5a6895471fb6bf9afe9bdf017f1fa2c6246ae303.tar.gz |
- modify what we did in [ticket:2793] so that we can also set the
version id programmatically outside of the generator. using this system,
we can also leave the version id alone.
-rw-r--r-- | doc/build/changelog/changelog_09.rst | 8 | ||||
-rw-r--r-- | doc/build/changelog/migration_09.rst | 9 | ||||
-rw-r--r-- | doc/build/orm/mapper_config.rst | 50 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/mapper.py | 5 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/persistence.py | 9 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/strategies.py | 3 | ||||
-rw-r--r-- | test/orm/test_versioning.py | 86 |
7 files changed, 156 insertions, 14 deletions
diff --git a/doc/build/changelog/changelog_09.rst b/doc/build/changelog/changelog_09.rst index 03cecff2c..27cddd9bf 100644 --- a/doc/build/changelog/changelog_09.rst +++ b/doc/build/changelog/changelog_09.rst @@ -51,9 +51,11 @@ The ``version_id_generator`` parameter of ``Mapper`` can now be specified to rely upon server generated version identifiers, using triggers - or other database-provided versioning features, by passing the value - ``False``. The ORM will use RETURNING when available to immediately - load the new version identifier, else it will emit a second SELECT. + or other database-provided versioning features, or via an optional programmatic + value, by setting ``version_id_generator=False``. + When using a server-generated version identfier, the ORM will use RETURNING when + available to immediately + load the new version value, else it will emit a second SELECT. .. change:: :tags: feature, orm diff --git a/doc/build/changelog/migration_09.rst b/doc/build/changelog/migration_09.rst index 82314cce4..cf345edbc 100644 --- a/doc/build/changelog/migration_09.rst +++ b/doc/build/changelog/migration_09.rst @@ -396,9 +396,12 @@ Server Side Version Counting The versioning feature of the ORM (now also documented at :ref:`mapper_version_counter`) can now make use of server-side version counting schemes, such as those produced -by triggers or database system columns. By providing the value ``False`` -to the ``version_id_generator`` parameter, the ORM will fetch the version identifier -from each row at the same time the INSERT or UPDATE is emitted. It is strongly +by triggers or database system columns, as well as conditional programmatic schemes outside +of the version_id_counter function itself. By providing the value ``False`` +to the ``version_id_generator`` parameter, the ORM will use the already-set version +identifier, or alternatively fetch the version identifier +from each row at the same time the INSERT or UPDATE is emitted. When using a +server-generated version identifier, it is strongly recommended that this feature be used only on a backend where RETURNING can also be used, else the additional SELECT statements will add significant performance overhead. The example provided at :ref:`server_side_version_counter` illustrates diff --git a/doc/build/orm/mapper_config.rst b/doc/build/orm/mapper_config.rst index c35e3429c..d35910745 100644 --- a/doc/build/orm/mapper_config.rst +++ b/doc/build/orm/mapper_config.rst @@ -1237,7 +1237,7 @@ subsequent value. .. _server_side_version_counter: Server Side Version Counters ------------------------------ +---------------------------- The ``version_id_generator`` can also be configured to rely upon a value that is generated by the database. In this case, the database would need @@ -1311,6 +1311,54 @@ e.g. Postgresql, Oracle, SQL Server (though SQL Server has Support for server side version identifier tracking. +Programmatic or Conditional Version Counters +--------------------------------------------- + +When ``version_id_generator`` is set to False, we can also programmatically +(and conditionally) set the version identifier on our object in the same way +we assign any other mapped attribute. Such as if we used our UUID example, but +set ``version_id_generator`` to ``False``, we can set the version identifier +at our choosing:: + + import uuid + + class User(Base): + __tablename__ = 'user' + + id = Column(Integer, primary_key=True) + version_uuid = Column(String(32)) + name = Column(String(50), nullable=False) + + __mapper_args__ = { + 'version_id_col':version_uuid, + 'version_id_generator': False + } + + u1 = User(name='u1', version_uuid=uuid.uuid4()) + + session.add(u1) + + session.commit() + + u1.name = 'u2' + u1.version_uuid = uuid.uuid4() + + session.commit() + +We can update our ``User`` object without incrementing the version counter +as well; the value of the counter will remain unchanged, and the UPDATE +statement will still check against the previous value. This may be useful +for schemes where only certain classes of UPDATE are sensitive to concurrency +issues:: + + # will leave version_uuid unchanged + u1.name = 'u3' + session.commit() + +.. versionadded:: 0.9.0 + + Support for programmatic and conditional version identifier tracking. + Class Mapping API ================= diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 30b5ffc79..4336c191d 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -459,8 +459,9 @@ class Mapper(_InspectionAttr): def generate_version(version): return next_version - Alternatively, server-side versioning functions such as triggers - may be used as well, by specifying the value ``False``. + Alternatively, server-side versioning functions such as triggers, + or programmatic versioning schemes outside of the version id generator + may be used, by specifying the value ``False``. Please see :ref:`server_side_version_counter` for a discussion of important points when using this option. diff --git a/lib/sqlalchemy/orm/persistence.py b/lib/sqlalchemy/orm/persistence.py index 042186179..ccdd6e81e 100644 --- a/lib/sqlalchemy/orm/persistence.py +++ b/lib/sqlalchemy/orm/persistence.py @@ -248,10 +248,10 @@ def _collect_insert_commands(base_mapper, uowtransaction, table, has_all_pks = True has_all_defaults = True for col in mapper._cols_by_table[table]: - if col is mapper.version_id_col: - if mapper.version_id_generator is not False: - val = mapper.version_id_generator(None) - params[col.key] = val + if col is mapper.version_id_col and \ + mapper.version_id_generator is not False: + val = mapper.version_id_generator(None) + params[col.key] = val else: # pull straight from the dict for # pending objects @@ -417,6 +417,7 @@ def _collect_post_update_commands(base_mapper, uowtransaction, table, mapper._get_state_attr_by_column( state, state_dict, col) + elif col in post_update_cols: prop = mapper._columntoproperty[col] history = attributes.get_state_history( diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index 63b7d7c0b..761e6b999 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -133,7 +133,8 @@ class ColumnLoader(LoaderStrategy): coltype = self.columns[0].type # TODO: check all columns ? check for foreign key as well? active_history = self.parent_property.active_history or \ - self.columns[0].primary_key + self.columns[0].primary_key or \ + mapper.version_id_col in set(self.columns) _register_attribute(self, mapper, useobject=False, compare_function=coltype.compare_values, diff --git a/test/orm/test_versioning.py b/test/orm/test_versioning.py index 026793c97..9379543ed 100644 --- a/test/orm/test_versioning.py +++ b/test/orm/test_versioning.py @@ -829,4 +829,90 @@ class ServerVersioningTest(fixtures.MappedTest): sess.commit ) +class ManualVersionTest(fixtures.MappedTest): + run_define_tables = 'each' + + @classmethod + def define_tables(cls, metadata): + Table("a", metadata, + Column('id', Integer, primary_key=True, test_needs_autoincrement=True), + Column('data', String(30)), + Column('vid', Integer) + ) + + @classmethod + def setup_classes(cls): + class A(cls.Basic): + pass + + + @classmethod + def setup_mappers(cls): + mapper(cls.classes.A, cls.tables.a, + version_id_col=cls.tables.a.c.vid, + version_id_generator=False) + + def test_insert(self): + sess = Session() + a1 = self.classes.A() + + a1.vid = 1 + sess.add(a1) + sess.commit() + + eq_(a1.vid, 1) + + def test_update(self): + sess = Session() + a1 = self.classes.A() + + a1.vid = 1 + a1.data = 'd1' + sess.add(a1) + sess.commit() + + a1.vid = 2 + a1.data = 'd2' + + sess.commit() + + eq_(a1.vid, 2) + + def test_update_concurrent_check(self): + sess = Session() + a1 = self.classes.A() + + a1.vid = 1 + a1.data = 'd1' + sess.add(a1) + sess.commit() + + a1.vid = 2 + sess.execute(self.tables.a.update().values(vid=3)) + a1.data = 'd2' + assert_raises( + orm_exc.StaleDataError, + sess.commit + ) + + def test_update_version_conditional(self): + sess = Session() + a1 = self.classes.A() + + a1.vid = 1 + a1.data = 'd1' + sess.add(a1) + sess.commit() + + # change the data and UPDATE without + # incrementing version id + a1.data = 'd2' + sess.commit() + + eq_(a1.vid, 1) + + a1.data = 'd3' + a1.vid = 2 + sess.commit() + eq_(a1.vid, 2)
\ No newline at end of file |