]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
more docs for autocommit isolation level
authorMike Bayer <mike_mp@zzzcomputing.com>
Sun, 12 Jul 2020 23:52:54 +0000 (19:52 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 13 Jul 2020 02:01:52 +0000 (22:01 -0400)
this concept is not clear that we offer real
DBAPI autocommit everywhere.  backport 1.3 with edits
as well

Change-Id: I2e8328b7fb6e1cdc5453ab29c94276f60c7ca149
(cherry picked from commit 28fbb0cb94ddf92a014adbfe63a15b7d0797ccee)

doc/build/core/connections.rst
doc/build/orm/session_transaction.rst
lib/sqlalchemy/dialects/mssql/base.py
lib/sqlalchemy/dialects/mysql/base.py
lib/sqlalchemy/dialects/oracle/base.py
lib/sqlalchemy/dialects/postgresql/base.py
lib/sqlalchemy/dialects/sqlite/base.py
lib/sqlalchemy/engine/base.py
test/engine/test_transaction.py

index 99f84d9acb86ea13284699f1b4e03a29d09ead3c..2ef3e8fd4e36ca8a79606a38dfb9d2fce5fad40e 100644 (file)
@@ -167,13 +167,18 @@ one exists.
 
 .. _autocommit:
 
-Understanding Autocommit
-========================
+Library Level (e.g. emulated) Autocommit
+==========================================
 
 .. note:: The "autocommit" feature of SQLAlchemy is a legacy feature that will
    be deprecated in an upcoming release.  New usage paradigms will eliminate
    the need for it to be present.
 
+.. note:: This section discusses the feature within SQLAlchemy that automatically
+   invokes the ``.commit()`` method on a DBAPI connection, however this is against
+   a DBAPI connection that **is itself transactional**.  For true AUTOCOMMIT,
+   see the next section :ref:`dbapi_autocommit`.
+
 The previous transaction example illustrates how to use :class:`.Transaction`
 so that several executions can take part in the same transaction. What happens
 when we issue an INSERT, UPDATE or DELETE call without using
@@ -212,6 +217,108 @@ it so that a SELECT statement will issue a COMMIT::
     with engine.connect().execution_options(autocommit=True) as conn:
         conn.execute(text("SELECT my_mutating_procedure()"))
 
+.. _dbapi_autocommit:
+
+Setting Transaction Isolation Levels including DBAPI Autocommit
+=================================================================
+
+Most DBAPIs support the concept of configurable transaction :term:`isolation` levels.
+These are traditionally the four levels "READ UNCOMMITTED", "READ COMMITTED",
+"REPEATABLE READ" and "SERIALIZABLE".  These are usually applied to a
+DBAPI connection before it begins a new transaction, noting that most
+DBAPIs will begin this transaction implicitly when SQL statements are first
+emitted.
+
+DBAPIs that support isolation levels also usually support the concept of true
+"autocommit", which means that the DBAPI connection itself will be placed into
+a non-transactional autocommit mode.   This usually means that the typical
+DBAPI behavior of emitting "BEGIN" to the database automatically no longer
+occurs, but it may also include other directives.   When using this mode,
+**the DBAPI does not use a transaction under any circumstances**.  SQLAlchemy
+methods like ``.begin()``, ``.commit()`` and ``.rollback()`` pass silently
+and have no effect.
+
+Instead, each statement invoked upon the connection will commit any changes
+automatically; it sometimes also means that the connection itself will use
+fewer server-side database resources. For this reason and others, "autocommit"
+mode is often desirable for non-tranasctional applications that need to read
+individual tables or rows in isolation of a true ACID transaction.
+
+SQLAlchemy dialects can support these isolation levels as well as autocommit to
+as great a degree as possible.   The levels are set via family of
+"execution_options" parameters and methods that are throughout the Core, such
+as the :meth:`_engine.Connection.execution_options` method.   The parameter is
+known as :paramref:`_engine.Connection.execution_options.isolation_level` and
+the values are strings which are typically a subset of the following names::
+
+    # possible values for Connection.execution_options(isolation_level="<value>")
+
+    "AUTOCOMMIT"
+    "READ COMMITTED"
+    "READ UNCOMMITTED"
+    "REPEATABLE READ"
+    "SERIALIZABLE"
+
+Not every DBAPI supports every value; if an unsupported value is used for a
+certain backend, an error is raised.
+
+For example, to force REPEATABLE READ on a specific connection::
+
+  with engine.connect().execution_options(isolation_level="REPEATBLE READ") as connection:
+      connection.execute(<statement>)
+
+The :paramref:`_engine.Connection.execution_options.isolation_level` option
+may also be set engine wide, as is often preferable.   It can be set either
+within :func:`_sa.create_engine` directly via the :paramref:`_sa.create_engine.execution_options`
+parameter::
+
+
+    from sqlalchemy import create_engine
+
+    eng = create_engine(
+        "postgresql://scott:tiger@localhost/test",
+        isolation_level='REPEATABLE READ'
+    )
+
+Or for an application that chooses between multiple levels, as may be the case
+for the use of "AUTOCOMMIT" to switch between "transactional" and "read-only"
+engines, the :meth:`_engine.Engine.execution_options` method will provide a shallow
+copy of the :class:`_engine.Engine` that will apply the given isolation
+level to all connections::
+
+
+    from sqlalchemy import create_engine
+
+    eng = create_engine("postgresql://scott:tiger@localhost/test")
+
+    autocommit_engine = eng.execution_options(isolation_level="AUTOCOMMIT")
+
+
+Above, both ``eng`` and ``autocommit_engine`` share the same dialect
+and connection pool.  However the "AUTOCOMMIT" mode will be set upon connections
+when they are acquired from the ``autocommit_engine``.
+
+The isolation level setting, regardless of which one it is, is unconditionally
+reverted when a connection is returned to the connection pool.
+
+
+.. note:: The :paramref:`_engine.Connection.execution_options.isolation_level`
+   parameter necessarily does not apply to statement level options, such as
+   that of :meth:`_sql.Executable.execution_options`.  This because the option
+   must be set on a DBAPI connection on a per-transaction basis.
+
+.. seealso::
+
+      :ref:`SQLite Transaction Isolation <sqlite_isolation_level>`
+
+      :ref:`PostgreSQL Transaction Isolation <postgresql_isolation_level>`
+
+      :ref:`MySQL Transaction Isolation <mysql_isolation_level>`
+
+      :ref:`SQL Server Transaction Isolation <mssql_isolation_level>`
+
+      :ref:`session_transaction_isolation` - for the ORM
+
 .. _dbengine_implicit:
 
 
index db66aa30e4561b4ad2acd5eb2f042b497311ccd4..505995c32f8e6b935f5ace93ebc8179eafe15772 100644 (file)
@@ -293,14 +293,23 @@ transactions set the flag ``twophase=True`` on the session::
 
 .. _session_transaction_isolation:
 
-Setting Transaction Isolation Levels
-------------------------------------
-
-:term:`Isolation` refers to the behavior of the transaction at the database
-level in relation to other transactions occurring concurrently.  There
-are four well-known modes of isolation, and typically the Python DBAPI
-allows these to be set on a per-connection basis, either through explicit
-APIs or via database-specific calls.
+Setting Transaction Isolation Levels / DBAPI AUTOCOMMIT
+-------------------------------------------------------
+
+Most DBAPIs support the concept of configurable transaction :term:`isolation` levels.
+These are traditionally the four levels "READ UNCOMMITTED", "READ COMMITTED",
+"REPEATABLE READ" and "SERIALIZABLE".  These are usually applied to a
+DBAPI connection before it begins a new transaction, noting that most
+DBAPIs will begin this transaction implicitly when SQL statements are first
+emitted.
+
+DBAPIs that support isolation levels also usually support the concept of true
+"autocommit", which means that the DBAPI connection itself will be placed into
+a non-transactional autocommit mode.   This usually means that the typical
+DBAPI behavior of emitting "BEGIN" to the database automatically no longer
+occurs, but it may also include other directives.   When using this mode,
+**the DBAPI does not use a transaction under any circumstances**.  SQLAlchemy
+methods like ``.begin()``, ``.commit()`` and ``.rollback()`` pass silently.
 
 SQLAlchemy's dialects support settable isolation modes on a per-:class:`_engine.Engine`
 or per-:class:`_engine.Connection` basis, using flags at both the
@@ -312,33 +321,63 @@ connections, but does not expose transaction isolation directly.  So in
 order to affect transaction isolation level, we need to act upon the
 :class:`_engine.Engine` or :class:`_engine.Connection` as appropriate.
 
-.. seealso::
+Setting Isolation For A Sessionmaker / Engine Wide
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-    :paramref:`_sa.create_engine.isolation_level`
+To set up a :class:`.Session` or :class:`.sessionmaker` with a specific
+isolation level globally, the first technique is that an
+:class:`_engine.Engine` can be constructed against a specific isolation level
+in all cases, which is then used as the source of connectivity for a
+:class:`_orm.Session` and/or :class:`_orm.sessionmaker`::
 
-    :ref:`SQLite Transaction Isolation <sqlite_isolation_level>`
+    from sqlalchemy import create_engine
+    from sqlalchemy.orm import sessionmaker
 
-    :ref:`PostgreSQL Isolation Level <postgresql_isolation_level>`
+    eng = create_engine(
+        "postgresql://scott:tiger@localhost/test",
+        isolation_level='REPEATABLE READ'
+    )
 
-    :ref:`MySQL Isolation Level <mysql_isolation_level>`
+    Session = sessionmaker(eng)
 
-Setting Isolation Engine-Wide
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-To set up a :class:`.Session` or :class:`.sessionmaker` with a specific
-isolation level globally, use the :paramref:`_sa.create_engine.isolation_level`
-parameter::
+Another option, useful if there are to be two engines with different isolation
+levels at once, is to use the :meth:`_engine.Engine.execution_options` method,
+which will produce a shallow copy of the original :class:`_engine.Engine` which
+shares the same connection pool as the parent engine.  This is often preferable
+when operations will be separated into "transactional" and "autocommit"
+operations::
 
     from sqlalchemy import create_engine
     from sqlalchemy.orm import sessionmaker
 
-    eng = create_engine(
-        "postgresql://scott:tiger@localhost/test",
-        isolation_level='REPEATABLE_READ')
+    eng = create_engine("postgresql://scott:tiger@localhost/test")
 
-    maker = sessionmaker(bind=eng)
+    autocommit_engine = eng.execution_options(isolation_level="AUTOCOMMIT")
+
+    transactional_session = sessionmaker(eng)
+    autocommit_session = sessionmaker(autocommit_engine)
+
+
+Above, both "``eng``" and ``"autocommit_engine"`` share the same dialect and
+connection pool.  However the "AUTOCOMMIT" mode will be set upon connections
+when they are acquired from the ``autocommit_engine``.  The two
+:class:`_orm.sessionmaker` objects "``transactional_session``" and "``autocommit_session"``
+then inherit these characteristics when they work with database connections.
 
-    session = maker()
+
+The "``autocommit_session``" **continues to have transactional semantics**,
+including that
+:meth:`_orm.Session.commit` and :meth:`_orm.Session.rollback` still consider
+themselves to be "committing" and "rolling back" objects, however the
+transaction will be silently absent.  For this reason, **it is typical,
+though not strictly required, that a Session with AUTOCOMMIT isolation be
+used in a read-only fashion**, that is::
+
+    session = autocommit_session()
+    some_objects = session.query(cls1).filter(...).all()
+    some_other_objects = session.query(cls2).filter(...).all()
+    session.close()  # closes connection
 
 
 Setting Isolation for Individual Sessions
@@ -347,12 +386,12 @@ Setting Isolation for Individual Sessions
 When we make a new :class:`.Session`, either using the constructor directly
 or when we call upon the callable produced by a :class:`.sessionmaker`,
 we can pass the ``bind`` argument directly, overriding the pre-existing bind.
-We can combine this with the :meth:`_engine.Engine.execution_options` method
-in order to produce a copy of the original :class:`_engine.Engine` that will
-add this option::
+We can for example create our :class:`_orm.Session` from the
+"``transactional_session``" and pass the "``autocommit_engine``"::
 
-    session = maker(
-        bind=engine.execution_options(isolation_level='SERIALIZABLE'))
+    session = transactional_session(bind=autocommit_engine)
+    # work with session
+    session.close()
 
 For the case where the :class:`.Session` or :class:`.sessionmaker` is
 configured with multiple "binds", we can either re-specify the ``binds``
@@ -361,8 +400,7 @@ can use the :meth:`.Session.bind_mapper` or :meth:`.Session.bind_table`
 methods::
 
     session = maker()
-    session.bind_mapper(
-        User, user_engine.execution_options(isolation_level='SERIALIZABLE'))
+    session.bind_mapper(User, autocommit_engine)
 
 We can also use the individual transaction method that follows.
 
@@ -373,65 +411,28 @@ A key caveat regarding isolation level is that the setting cannot be
 safely modified on a :class:`_engine.Connection` where a transaction has already
 started.  Databases cannot change the isolation level of a transaction
 in progress, and some DBAPIs and SQLAlchemy dialects
-have inconsistent behaviors in this area.  Some may implicitly emit a
-ROLLBACK and some may implicitly emit a COMMIT, others may ignore the setting
-until the next transaction.  Therefore SQLAlchemy emits a warning if this
-option is set when a transaction is already in play.  The :class:`.Session`
-object does not provide for us a :class:`_engine.Connection` for use in a transaction
-where the transaction is not already begun.  So here, we need to pass
-execution options to the :class:`.Session` at the start of a transaction
-by passing :paramref:`.Session.connection.execution_options`
-provided by the :meth:`.Session.connection` method::
+have inconsistent behaviors in this area.
+
+Therefore it is preferable to use a :class:`_orm.Session` that is up front
+bound to an engine with the desired isolation level.  However, the isolation
+level on a per-connection basis can be affected by using the
+:meth:`_orm.Session.connection` method at the start of a transaction::
 
     from sqlalchemy.orm import Session
 
     sess = Session(bind=engine)
-    sess.connection(execution_options={'isolation_level': 'SERIALIZABLE'})
-
-    # work with session
+    with sess.begin():
+        sess.connection(execution_options={'isolation_level': 'SERIALIZABLE'})
 
-    # commit transaction.  the connection is released
+    # commits transaction.  the connection is released
     # and reverted to its previous isolation level.
-    sess.commit()
 
 Above, we first produce a :class:`.Session` using either the constructor
 or a :class:`.sessionmaker`.   Then we explicitly set up the start of
 a transaction by calling upon :meth:`.Session.connection`, which provides
 for execution options that will be passed to the connection before the
-transaction is begun.   If we are working with a :class:`.Session` that
-has multiple binds or some other custom scheme for :meth:`.Session.get_bind`,
-we can pass additional arguments to :meth:`.Session.connection` in order to
-affect how the bind is procured::
-
-    sess = my_sessionmaker()
-
-    # set up a transaction for the bind associated with
-    # the User mapper
-    sess.connection(
-        mapper=User,
-        execution_options={'isolation_level': 'SERIALIZABLE'})
+transaction is begun.
 
-    # work with session
-
-    # commit transaction.  the connection is released
-    # and reverted to its previous isolation level.
-    sess.commit()
-
-The :paramref:`.Session.connection.execution_options` argument is only
-accepted on the **first** call to :meth:`.Session.connection` for a
-particular bind within a transaction.  If a transaction is already begun
-on the target connection, a warning is emitted::
-
-    >>> session = Session(eng)
-    >>> session.execute("select 1")
-    <sqlalchemy.engine.result.ResultProxy object at 0x1017a6c50>
-    >>> session.connection(execution_options={'isolation_level': 'SERIALIZABLE'})
-    sqlalchemy/orm/session.py:310: SAWarning: Connection is already established
-    for the given bind; execution_options ignored
-
-.. versionadded:: 0.9.9 Added the
-    :paramref:`.Session.connection.execution_options`
-    parameter to :meth:`.Session.connection`.
 
 Tracking Transaction State with Events
 --------------------------------------
index 4f673fba14d9885add4ae2da95c4380e9a04921a..e66a8fd5d0f02ec36c3dfe3139e7210a68b58913 100644 (file)
@@ -305,11 +305,12 @@ Valid values for ``isolation_level`` include:
 * ``SERIALIZABLE``
 * ``SNAPSHOT`` - specific to SQL Server
 
-.. versionadded:: 1.1 support for isolation level setting on Microsoft
-   SQL Server.
-
 .. versionadded:: 1.2 added AUTOCOMMIT isolation level setting
 
+.. seealso::
+
+    :ref:`dbapi_autocommit`
+
 Nullability
 -----------
 MSSQL has support for three levels of column nullability. The default
index 836aa3d1982a787a94827d164f3464da6bdd162e..d84e3770f4f3801d02e972212679a98561ef5b64 100644 (file)
@@ -158,7 +158,9 @@ MySQLdb, MySQL-Client, MySQL-Connector Python, and PyMySQL.   Using it,
 the MySQL connection will return true for the value of
 ``SELECT @@autocommit;``.
 
-.. versionadded:: 1.1 - added support for the AUTOCOMMIT isolation level.
+.. seealso::
+
+    :ref:`dbapi_autocommit`
 
 AUTO_INCREMENT Behavior
 -----------------------
index d66bfe0824bb5b9ef7ed445212f90bb3fb5a9969..f1ac597bc84e7a472601b7cb96f964577d7be27d 100644 (file)
@@ -68,6 +68,10 @@ Valid values for ``isolation_level`` include:
    as well as the notion of a default isolation level, currently harcoded
    to "READ COMMITTED".
 
+.. seealso::
+
+    :ref:`dbapi_autocommit`
+
 Identifier Casing
 -----------------
 
index aacfc47d8a3a4759ba16205ebfe438b439dfab35..4ad6cb11cfcb410a2dbcd566591c19af18d2595a 100644 (file)
@@ -117,6 +117,8 @@ Valid values for ``isolation_level`` include:
 
 .. seealso::
 
+    :ref:`dbapi_autocommit`
+
     :ref:`psycopg2_isolation_level`
 
     :ref:`pg8000_isolation_level`
index 5fd856eee3e3a34c58e0e81a35928ec66fbcd86b..77afe5fb1b4d73989b9f43269cd58d630dd0ccb8 100644 (file)
@@ -212,6 +212,10 @@ by *not even emitting BEGIN* until the first write operation.
     degree than is often feasible. See the section :ref:`pysqlite_serializable`
     for techniques to work around this behavior.
 
+.. seealso::
+
+    :ref:`dbapi_autocommit`
+
 SAVEPOINT Support
 ----------------------------
 
index aad98c3982a2859df5b3b07f475d2271590f7951..6c9eb41ccdc3ae088b1a8619146f9593c0cb4185 100644 (file)
@@ -211,16 +211,14 @@ class Connection(Connectable):
         :param autocommit: Available on: Connection, statement.
           When True, a COMMIT will be invoked after execution
           when executed in 'autocommit' mode, i.e. when an explicit
-          transaction is not begun on the connection. Note that DBAPI
-          connections by default are always in a transaction - SQLAlchemy uses
-          rules applied to different kinds of statements to determine if
-          COMMIT will be invoked in order to provide its "autocommit" feature.
-          Typically, all INSERT/UPDATE/DELETE statements as well as
-          CREATE/DROP statements have autocommit behavior enabled; SELECT
-          constructs do not. Use this option when invoking a SELECT or other
-          specific SQL construct where COMMIT is desired (typically when
-          calling stored procedures and such), and an explicit
-          transaction is not in progress.
+          transaction is not begun on the connection.   Note that this
+          is **library level, not DBAPI level autocommit**.  The DBAPI
+          connection will remain in a real transaction unless the
+          "AUTOCOMMIT" isolation level is used.
+
+          .. deprecated:: 1.4  The library-level "autocommit" feature is being
+             removed in favor of database driver "autocommit" which is
+             now widely available.  See the section :ref:`dbapi_autocommit`.
 
         :param compiled_cache: Available on: Connection.
           A dictionary where :class:`.Compiled` objects
index 970435e030f1fa6564ebaef6e4f5ff14744f367e..6ca018df8487d63eb5e924d2ab3db8b8cdb38af3 100644 (file)
@@ -1110,6 +1110,17 @@ class IsolationLevelTest(fixtures.TestBase):
             self._non_default_isolation_level(),
         )
 
+    def test_per_option_engine(self):
+        eng = create_engine(testing.db.url).execution_options(
+            isolation_level=self._non_default_isolation_level()
+        )
+
+        conn = eng.connect()
+        eq_(
+            eng.dialect.get_isolation_level(conn.connection),
+            self._non_default_isolation_level(),
+        )
+
     def test_isolation_level_accessors_connection_default(self):
         eng = create_engine(testing.db.url)
         with eng.connect() as conn: