summaryrefslogtreecommitdiff
path: root/doc/build/tutorial/dbapi_transactions.rst
diff options
context:
space:
mode:
Diffstat (limited to 'doc/build/tutorial/dbapi_transactions.rst')
-rw-r--r--doc/build/tutorial/dbapi_transactions.rst518
1 files changed, 518 insertions, 0 deletions
diff --git a/doc/build/tutorial/dbapi_transactions.rst b/doc/build/tutorial/dbapi_transactions.rst
new file mode 100644
index 000000000..24df53943
--- /dev/null
+++ b/doc/build/tutorial/dbapi_transactions.rst
@@ -0,0 +1,518 @@
+.. |prev| replace:: :doc:`engine`
+.. |next| replace:: :doc:`metadata`
+
+.. include:: tutorial_nav_include.rst
+
+
+.. _tutorial_working_with_transactions:
+
+Working with Transactions and the DBAPI
+========================================
+
+
+
+With the :class:`_future.Engine` object ready to go, we may now proceed
+to dive into the basic operation of an :class:`_future.Engine` and
+its primary interactive endpoints, the :class:`_future.Connection` and
+:class:`_engine.Result`. We will additionally introduce the ORM's
+:term:`facade` for these objects, known as the :class:`_orm.Session`.
+
+.. container:: orm-header
+
+ **Note to ORM readers**
+
+ When using the ORM, the :class:`_future.Engine` is managed by another
+ object called the :class:`_orm.Session`. The :class:`_orm.Session` in
+ modern SQLAlchemy emphasizes a transactional and SQL execution pattern that
+ is largely identical to that of the :class:`_future.Connection` discussed
+ below, so while this subsection is Core-centric, all of the concepts here
+ are essentially relevant to ORM use as well and is recommended for all ORM
+ learners. The execution pattern used by the :class:`_future.Connection`
+ will be contrasted with that of the :class:`_orm.Session` at the end
+ of this section.
+
+As we have yet to introduce the SQLAlchemy Expression Language that is the
+primary feature of SQLAlchemy, we will make use of one simple construct within
+this package called the :func:`_sql.text` construct, which allows us to write
+SQL statements as **textual SQL**. Rest assured that textual SQL in
+day-to-day SQLAlchemy use is by far the exception rather than the rule for most
+tasks, even though it always remains fully available.
+
+.. rst-class:: core-header
+
+.. _tutorial_getting_connection:
+
+Getting a Connection
+---------------------
+
+The sole purpose of the :class:`_future.Engine` object from a user-facing
+perspective is to provide a unit of
+connectivity to the database called the :class:`_future.Connection`. When
+working with the Core directly, the :class:`_future.Connection` object
+is how all interaction with the database is done. As the :class:`_future.Connection`
+represents an open resource against the database, we want to always limit
+the scope of our use of this object to a specific context, and the best
+way to do that is by using Python context manager form, also known as
+`the with statement <https://docs.python.org/3/reference/compound_stmts.html#with>`_.
+Below we illustrate "Hello World", using a textual SQL statement. Textual
+SQL is emitted using a construct called :func:`_sql.text` that will be discussed
+in more detail later:
+
+.. sourcecode:: pycon+sql
+
+ >>> from sqlalchemy import text
+
+ >>> with engine.connect() as conn:
+ ... result = conn.execute(text("select 'hello world'"))
+ ... print(result.all())
+ {opensql}BEGIN (implicit)
+ select 'hello world'
+ [...] ()
+ {stop}[('hello world',)]
+ {opensql}ROLLBACK{stop}
+
+In the above example, the context manager provided for a database connection
+and also framed the operation inside of a transaction. The default behavior of
+the Python DBAPI includes that a transaction is always in progress; when the
+scope of the connection is :term:`released`, a ROLLBACK is emitted to end the
+transaction. The transaction is **not committed automatically**; when we want
+to commit data we normally need to call :meth:`_future.Connection.commit`
+as we'll see in the next section.
+
+.. tip:: "autocommit" mode is available for special cases. The section
+ :ref:`dbapi_autocommit` discusses this.
+
+The result of our SELECT was also returned in an object called
+:class:`_engine.Result` that will be discussed later, however for the moment
+we'll add that it's best to ensure this object is consumed within the
+"connect" block, and is not passed along outside of the scope of our connection.
+
+.. rst-class:: core-header
+
+.. _tutorial_committing_data:
+
+Committing Changes
+------------------
+
+We just learned that the DBAPI connection is non-autocommitting. What if
+we want to commit some data? We can alter our above example to create a
+table and insert some data, and the transaction is then committed using
+the :meth:`_future.Connection.commit` method, invoked **inside** the block
+where we acquired the :class:`_future.Connection` object:
+
+.. sourcecode:: pycon+sql
+
+ # "commit as you go"
+ >>> with engine.connect() as conn:
+ ... conn.execute(text("CREATE TABLE some_table (x int, y int)"))
+ ... conn.execute(
+ ... text("INSERT INTO some_table (x, y) VALUES (:x, :y)"),
+ ... [{"x": 1, "y": 1}, {"x": 2, "y": 4}]
+ ... )
+ ... conn.commit()
+ {opensql}BEGIN (implicit)
+ CREATE TABLE some_table (x int, y int)
+ [...] ()
+ <sqlalchemy.engine.cursor.CursorResult object at 0x...>
+ INSERT INTO some_table (x, y) VALUES (?, ?)
+ [...] ((1, 1), (2, 4))
+ <sqlalchemy.engine.cursor.CursorResult object at 0x...>
+ COMMIT
+
+Above, we emitted two SQL statements that are generally transactional, a
+"CREATE TABLE" statement [1]_ and an "INSERT" statement that's parameterized
+(the parameterization syntax above is discussed a few sections below in
+:ref:`tutorial_multiple_parameters`). As we want the work we've done to be
+committed within our block, we invoke the
+:meth:`_future.Connection.commit` method which commits the transaction. After
+we call this method inside the block, we can continue to run more SQL
+statements and if we choose we may call :meth:`_future.Connection.commit`
+again for subsequent statements. SQLAlchemy refers to this style as **commit as
+you go**.
+
+There is also another style of committing data, which is that we can declare
+our "connect" block to be a transaction block up front. For this mode of
+operation, we use the :meth:`_future.Engine.begin` method to acquire the
+connection, rather than the :meth:`_future.Engine.connect` method. This method
+will both manage the scope of the :class:`_future.Connection` and also
+enclose everything inside of a transaction with COMMIT at the end, assuming
+a successful block, or ROLLBACK in case of exception raise. This style
+may be referred towards as **begin once**:
+
+.. sourcecode:: pycon+sql
+
+ # "begin once"
+ >>> with engine.begin() as conn:
+ ... conn.execute(
+ ... text("INSERT INTO some_table (x, y) VALUES (:x, :y)"),
+ ... [{"x": 6, "y": 8}, {"x": 9, "y": 10}]
+ ... )
+ {opensql}BEGIN (implicit)
+ INSERT INTO some_table (x, y) VALUES (?, ?)
+ [...] ((6, 8), (9, 10))
+ <sqlalchemy.engine.cursor.CursorResult object at 0x...>
+ COMMIT
+
+"Begin once" style is often preferred as it is more succinct and indicates the
+intention of the entire block up front. However, within this tutorial we will
+normally use "commit as you go" style as it is more flexible for demonstration
+purposes.
+
+.. topic:: What's "BEGIN (implicit)"?
+
+ You might have noticed the log line "BEGIN (implicit)" at the start of a
+ transaction block. "implicit" here means that SQLAlchemy **did not
+ actually send any command** to the database; it just considers this to be
+ the start of the DBAPI's implicit transaction. You can register
+ :ref:`event hooks <core_sql_events>` to intercept this event, for example.
+
+
+.. [1] :term:`DDL` refers to the subset of SQL that instructs the database
+ to create, modify, or remove schema-level constructs such as tables. DDL
+ such as "CREATE TABLE" is recommended to be within a transaction block that
+ ends with COMMIT, as many databases uses transactional DDL such that the
+ schema changes don't take place until the transaction is committed. However,
+ as we'll see later, we usually let SQLAlchemy run DDL sequences for us as
+ part of a higher level operation where we don't generally need to worry
+ about the COMMIT.
+
+
+.. rst-class:: core-header
+
+
+Basics of Statement Execution
+-----------------------------
+
+We have seen a few examples that run SQL statements against a database, making
+use of a method called :meth:`_future.Connection.execute`, in conjunction with
+an object called :func:`_sql.text`, and returning an object called
+:class:`_engine.Result`. In this section we'll illustrate more closely the
+mechanics and interactions of these components.
+
+.. container:: orm-header
+
+ Most of the content in this section applies equally well to modern ORM
+ use when using the :meth:`_orm.Session.execute` method, which works
+ very similarly to that of :meth:`_future.Connection.execute`, including that
+ ORM result rows are delivered using the same :class:`_engine.Result`
+ interface used by Core.
+
+.. rst-class:: orm-addin
+
+.. _tutorial_fetching_rows:
+
+Fetching Rows
+^^^^^^^^^^^^^
+
+We'll first illustrate the :class:`_engine.Result` object more closely by
+making use of the rows we've inserted previously, running a textual SELECT
+statement on the table we've created:
+
+
+.. sourcecode:: pycon+sql
+
+ >>> with engine.connect() as conn:
+ ... result = conn.execute(text("SELECT x, y FROM some_table"))
+ ... for row in result:
+ ... print(f"x: {row.x} y: {row.y}")
+ {opensql}BEGIN (implicit)
+ SELECT x, y FROM some_table
+ [...] ()
+ {stop}x: 1 y: 1
+ x: 2 y: 4
+ x: 6 y: 8
+ x: 9 y: 10
+ {opensql}ROLLBACK{stop}
+
+Above, the "SELECT" string we executed selected all rows from our table.
+The object returned is called :class:`_engine.Result` and represents an
+iterable object of result rows.
+
+:class:`_engine.Result` has lots of methods for
+fetching and transforming rows, such as the :meth:`_engine.Result.all`
+method illustrated previously, which returns a list of all :class:`_engine.Row`
+objects. It also implements the Python iterator interface so that we can
+iterate over the collection of :class:`_engine.Row` objects directly.
+
+The :class:`_engine.Row` objects themselves are intended to act like Python
+`named tuples
+<https://docs.python.org/3/library/collections.html#collections.namedtuple>`_.
+Below we illustrate a variety of ways to access rows.
+
+* **Tuple Assignment** - This is the most Python-idiomatic style, which is to assign variables
+ to each row positionally as they are received:
+
+ ::
+
+ result = conn.execute(text("select x, y from some_table"))
+
+ for x, y in result:
+ # ...
+
+* **Integer Index** - Tuples are Python sequences, so regular integer access is available too:
+
+ ::
+
+ result = conn.execute(text("select x, y from some_table"))
+
+ for row in result:
+ x = row[0]
+
+* **Attribute Name** - As these are Python named tuples, the tuples have dynamic attribute names
+ matching the names of each column. These names are normally the names that the
+ SQL statement assigns to the columns in each row. While they are usually
+ fairly predictable and can also be controlled by labels, in less defined cases
+ they may be subject to database-specific behaviors::
+
+ result = conn.execute(text("select x, y from some_table"))
+
+ for row in result:
+ y = row.y
+
+ # illustrate use with Python f-strings
+ print(f"Row: {row.x} {row.y}")
+
+ ..
+
+* **Mapping Access** - To receive rows as Python **mapping** objects, which is
+ essentially a read-only version of Python's interface to the common ``dict``
+ object, the :class:`_engine.Result` may be **transformed** into a
+ :class:`_engine.MappingResult` object using the
+ :meth:`_engine.Result.mappings` modifier; this is a result object that yields
+ dictionary-like :class:`_engine.RowMapping` objects rather than
+ :class:`_engine.Row` objects::
+
+ result = conn.execute(text("select x, y from some_table"))
+
+ for dict_row in result.mappings():
+ x = dict_row['x']
+ y = dict_row['y']
+
+ ..
+
+.. rst-class:: orm-addin
+
+.. _tutorial_sending_parameters:
+
+Sending Parameters
+^^^^^^^^^^^^^^^^^^
+
+SQL statements are usually accompanied by data that is to be passed with the
+statement itself, as we saw in the INSERT example previously. The
+:meth:`_future.Connection.execute` method therefore also accepts parameters,
+which are referred towards as :term:`bound parameters`. A rudimentary example
+might be if we wanted to limit our SELECT statement only to rows that meet a
+certain criteria, such as rows where the "y" value were greater than a certain
+value that is passed in to a function.
+
+In order to achieve this such that the SQL statement can remain fixed and
+that the driver can properly sanitize the value, we add a WHERE criteria to
+our statement that names a new parameter called "y"; the :func:`_sql.text`
+construct accepts these using a colon format "``:y``". The actual value for
+"``:y``" is then passed as the second argument to
+:meth:`_future.Connection.execute` in the form of a dictionary:
+
+.. sourcecode:: pycon+sql
+
+ >>> with engine.connect() as conn:
+ ... result = conn.execute(
+ ... text("SELECT x, y FROM some_table WHERE y > :y"),
+ ... {"y": 2}
+ ... )
+ ... for row in result:
+ ... print(f"x: {row.x} y: {row.y}")
+ {opensql}BEGIN (implicit)
+ SELECT x, y FROM some_table WHERE y > ?
+ [...] (2,)
+ {stop}x: 2 y: 4
+ x: 6 y: 8
+ x: 9 y: 10
+ {opensql}ROLLBACK{stop}
+
+
+In the logged SQL output, we can see that the bound parameter ``:y`` was
+converted into a question mark when it was sent to the SQLite database.
+This is because the SQLite database driver uses a format called "qmark parameter style",
+which is one of six different formats allowed by the DBAPI specification.
+SQLAlchemy abstracts these formats into just one, which is the "named" format
+using a colon.
+
+.. _tutorial_multiple_parameters:
+
+Sending Multiple Parameters
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+In the example at :ref:`tutorial_committing_data`, we executed an INSERT
+statement where it appeared that we were able to INSERT multiple rows into the
+database at once. For statements that **operate upon data, but do not return
+result sets**, namely :term:`DML` statements such as "INSERT" which don't
+include a phrase like "RETURNING", we can send **multi params** to the
+:meth:`_future.Connection.execute` method by passing a list of dictionaries
+instead of a single dictionary, thus allowing the single SQL statement to
+be invoked against each parameter set individually:
+
+.. sourcecode:: pycon+sql
+
+ >>> with engine.connect() as conn:
+ ... conn.execute(
+ ... text("INSERT INTO some_table (x, y) VALUES (:x, :y)"),
+ ... [{"x": 11, "y": 12}, {"x": 13, "y": 14}]
+ ... )
+ ... conn.commit()
+ {opensql}BEGIN (implicit)
+ INSERT INTO some_table (x, y) VALUES (?, ?)
+ [...] ((11, 12), (13, 14))
+ <sqlalchemy.engine.cursor.CursorResult object at 0x...>
+ COMMIT
+
+Behind the scenes, the :class:`_future.Connection` objects uses a DBAPI feature
+known as `cursor.executemany()
+<https://www.python.org/dev/peps/pep-0249/#id18>`_. This method performs the
+equivalent operation of invoking the given SQL statement against each parameter
+set individually. The DBAPI may optimize this operation in a variety of ways,
+by using prepared statements, or by concatenating the parameter sets into a
+single SQL statement in some cases. Some SQLAlchemy dialects may also use
+alternate APIs for this case, such as the :ref:`psycopg2 dialect for PostgreSQL
+<postgresql_psycopg2>` which uses more performant APIs
+for this use case.
+
+.. tip:: you may have noticed this section isn't tagged as an ORM concept.
+ That's because the "multiple parameters" use case is **usually** used
+ for INSERT statements, which when using the ORM are invoked in a different
+ way. Multiple parameters also may be used with UPDATE and DELETE
+ statements to emit distinct UPDATE/DELETE operations on a per-row basis,
+ however again when using the ORM, there is a different technique
+ generally used for updating or deleting many individual rows separately.
+
+.. rst-class:: orm-addin
+
+.. _tutorial_bundling_parameters:
+
+Bundling Parameters with a Statement
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The two previous cases illustrate a series of parameters being passed to
+accompany a SQL statement. For single-parameter statement executions,
+SQLAlchemy's use of parameters is in fact more often than not done by
+**bundling** the parameters with the statement itself, which is a primary
+feature of the SQL Expression Language and makes for queries that can be
+composed naturally while still making use of parameterization in all cases.
+This concept will be discussed in much more detail in the sections that follow;
+for a brief preview, the :func:`_sql.text` construct itself being part of the
+SQL Expression Language supports this feature by using the
+:meth:`_sql.TextClause.bindparams` method; this is a :term:`generative` method that
+returns a new copy of the SQL construct with additional state added, in this
+case the parameter values we want to pass along:
+
+
+.. sourcecode:: pycon+sql
+
+ >>> stmt = text("SELECT x, y FROM some_table WHERE y > :y ORDER BY x, y").bindparams(y=6)
+ >>> with engine.connect() as conn:
+ ... result = conn.execute(stmt)
+ ... for row in result:
+ ... print(f"x: {row.x} y: {row.y}")
+ {opensql}BEGIN (implicit)
+ SELECT x, y FROM some_table WHERE y > ? ORDER BY x, y
+ [...] (6,)
+ {stop}x: 6 y: 8
+ x: 9 y: 10
+ x: 11 y: 12
+ x: 13 y: 14
+ {opensql}ROLLBACK{stop}
+
+
+The interesting thing to note above is that even though we passed only a single
+argument, ``stmt``, to the :meth:`_future.Connection.execute` method, the
+execution of the statement illustrated both the SQL string as well as the
+separate parameter tuple.
+
+.. rst-class:: orm-addin
+
+.. _tutorial_executing_orm_session:
+
+Executing with an ORM Session
+-----------------------------
+
+As mentioned previously, most of the patterns and examples above apply to
+use with the ORM as well, so here we will introduce this usage so that
+as the tutorial proceeds, we will be able to illustrate each pattern in
+terms of Core and ORM use together.
+
+The fundamental transactional / database interactive object when using the
+ORM is called the :class:`_orm.Session`. In modern SQLAlchemy, this object
+is used in a manner very similar to that of the :class:`_future.Connection`,
+and in fact as the :class:`_orm.Session` is used, it refers to a
+:class:`_future.Connection` internally which it uses to emit SQL.
+
+When the :class:`_orm.Session` is used with non-ORM constructs, it
+passes through the SQL statements we give it and does not generally do things
+much differently from how the :class:`_future.Connection` does directly, so
+we can illustrate it here in terms of the simple textual SQL
+operations we've already learned.
+
+The :class:`_orm.Session` has a few different creational patterns, but
+here we will illustrate the most basic one that tracks exactly with how
+the :class:`_future.Connection` is used which is to construct it within
+a context manager:
+
+.. sourcecode:: pycon+sql
+
+ >>> from sqlalchemy.orm import Session
+
+ >>> stmt = text("SELECT x, y FROM some_table WHERE y > :y ORDER BY x, y").bindparams(y=6)
+ >>> with Session(engine) as session:
+ ... result = session.execute(stmt)
+ ... for row in result:
+ ... print(f"x: {row.x} y: {row.y}")
+ {opensql}BEGIN (implicit)
+ SELECT x, y FROM some_table WHERE y > ? ORDER BY x, y
+ [...] (6,){stop}
+ x: 6 y: 8
+ x: 9 y: 10
+ x: 11 y: 12
+ x: 13 y: 14
+ {opensql}ROLLBACK{stop}
+
+The example above can be compared to the example in the preceding section
+in :ref:`tutorial_bundling_parameters` - we directly replace the call to
+``with engine.connect() as conn`` with ``with Session(engine) as session``,
+and then make use of the :meth:`_orm.Session.execute` method just like we
+do with the :meth:`_future.Connection.execute` method.
+
+Also, like the :class:`_future.Connection`, the :class:`_orm.Session` features
+"commit as you go" behavior using the :meth:`_orm.Session.commit` method,
+illustrated below using a textual UPDATE statement to alter some of
+our data:
+
+.. sourcecode:: pycon+sql
+
+ >>> with Session(engine) as session:
+ ... result = session.execute(
+ ... text("UPDATE some_table SET y=:y WHERE x=:x"),
+ ... [{"x": 9, "y":11}, {"x": 13, "y": 15}]
+ ... )
+ ... session.commit()
+ {opensql}BEGIN (implicit)
+ UPDATE some_table SET y=? WHERE x=?
+ [...] ((11, 9), (15, 13))
+ COMMIT{stop}
+
+Above, we invoked an UPDATE statement using the bound-parameter, "executemany"
+style of execution introduced at :ref:`tutorial_multiple_parameters`, ending
+the block with a "commit as you go" commit.
+
+.. tip:: The :class:`_orm.Session` doesn't actually hold onto the
+ :class:`_future.Connection` object after it ends the transaction. It
+ gets a new :class:`_future.Connection` from the :class:`_future.Engine`
+ when executing SQL against the database is next needed.
+
+The :class:`_orm.Session` obviously has a lot more tricks up its sleeve
+than that, however understanding that it has an :meth:`_orm.Session.execute`
+method that's used the same way as :meth:`_future.Connection.execute` will
+get us started with the examples that follow later.
+
+
+
+
+