summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2013-09-06 21:39:36 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2013-09-06 21:39:36 -0400
commit5a6895471fb6bf9afe9bdf017f1fa2c6246ae303 (patch)
tree5c14811273d5b2e328a59e323593535bfa112b86
parente8167548429b9d4937caaa09740ffe9bdab1ef61 (diff)
downloadsqlalchemy-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.rst8
-rw-r--r--doc/build/changelog/migration_09.rst9
-rw-r--r--doc/build/orm/mapper_config.rst50
-rw-r--r--lib/sqlalchemy/orm/mapper.py5
-rw-r--r--lib/sqlalchemy/orm/persistence.py9
-rw-r--r--lib/sqlalchemy/orm/strategies.py3
-rw-r--r--test/orm/test_versioning.py86
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