-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added a tutorial on UPDATEs, transactions and multi-queries Added a tutorial on connection_pool Added a tutorial on error handling Added examples on INSERTs and DELETEs Rewrote the discussion page on character sets Added a discussion page on the templated connection class Removed superseded examples on timeouts and multi-queries Updated the coverage build to gcc-14 (gcc-13 was using a non-LTS release that caused problems) Contributes to #365 and #366
- Loading branch information
Showing
31 changed files
with
2,805 additions
and
437 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
[/ | ||
Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) | ||
|
||
Distributed under the Boost Software License, Version 1.0. (See accompanying | ||
file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) | ||
] | ||
|
||
[section:tutorial_updates_transactions Tutorial 5: UPDATEs, transactions and semicolon-separated queries] | ||
|
||
All the previous tutorials have only used `SELECT` statements, but | ||
Boost.MySQL is not limited to them. Using [refmemunq any_connection async_execute] | ||
you can run any SQL statement supported by MySQL. | ||
|
||
In this tutorial, we will write a program that changes the first name of an | ||
employee, given their ID, and prints the updated employee details. | ||
We will use an `UPDATE` and transaction management statements. | ||
`INSERT` and `DELETE` statements have similar mechanics. | ||
|
||
|
||
|
||
[heading A simple UPDATE] | ||
|
||
We can use the same tools and functions as in previous tutorials: | ||
|
||
[tutorial_updates_transactions_update] | ||
|
||
By default, auto-commit is enabled, meaning that when `async_execute` | ||
returns, the `UPDATE` is visible to other client connections. | ||
|
||
|
||
|
||
|
||
[heading Checking that the UPDATE took effect] | ||
|
||
The above query will succeed even if there was no employee with the given ID. | ||
We would like to retrieve the updated employee details on success, and emit | ||
a useful error message if no employee was matched. | ||
|
||
We may be tempted to use [refmem results affected_rows] at first, but | ||
this doesn't convey the information we're looking for: | ||
a row may be matched but not affected. For example, if you try to | ||
set `first_name` to the same value it already has, | ||
MySQL will count the row as a matched, but not affected. | ||
|
||
|
||
MySQL does not support the `UPDATE ... RETURNING` syntax, so we will | ||
have to retrieve the employee manually after updating it. | ||
We can add the following after our `UPDATE`: | ||
|
||
[tutorial_updates_transactions_select] | ||
|
||
However, the code above contains a race condition. Imagine the following situation: | ||
|
||
* The `UPDATE` is issued. No employee is matched. | ||
* Before our program sends the `SELECT` query, a different program inserts | ||
an employee with the ID that we're trying to update. | ||
* Our program runs the `SELECT` statement and retrieves the newly inserted row. | ||
|
||
To our program, it looks like we succeeded performing the update, when | ||
we really didn't. Depending on the nature of our program, this may | ||
or may not have serious consequences, but it's something we should avoid. | ||
|
||
|
||
[heading Avoiding the race condition with a transaction block] | ||
|
||
We can fix the race condition using transactions. | ||
In MySQL, a transaction block is opened with `START TRANSACTION`. | ||
Subsequent statements will belong to the transaction block, | ||
until the transaction either commits or is rolled back. | ||
A `COMMIT` statement commits the transaction. | ||
A rollback happens if the connection that initiated the transaction | ||
closes or an explicit `ROLLBACK` statement is used. | ||
|
||
We will enclose our `UPDATE` and `SELECT` statements in | ||
a transaction block. This will ensure that the `SELECT` | ||
will get the updated row, if any: | ||
|
||
[tutorial_updates_transactions_txn] | ||
|
||
|
||
|
||
|
||
[heading Using multi-queries] | ||
|
||
While the code we've written is correct, it's not very performant. | ||
We're incurring in 4 round-trips to the server, when our queries don't depend | ||
on the result of previous ones. The round-trips occur within a transaction | ||
block, which causes certain database rows to be locked, increasing contention. | ||
We can improve the situation by running our four statements in a single batch. | ||
|
||
Multi-queries are a protocol feature that lets you execute several queries | ||
at once. Individual queries must be separated by semicolons. | ||
|
||
Multi-queries are disabled by default. To enable them, set | ||
[refmem connect_params multi_queries] to `true` before connecting: | ||
|
||
[tutorial_updates_transactions_connect] | ||
|
||
Multi-queries can be composed an executed using the same | ||
functions we've been using: | ||
|
||
[tutorial_updates_transactions_multi_queries] | ||
|
||
Accessing the results is slightly different. MySQL returns 4 resultsets, | ||
one for each query. In Boost.MySQL, this operation is said to be | ||
[link mysql.multi_resultset multi-resultset]. | ||
[reflink results] can actually store more than one resultset. | ||
[refmem results rows] actually accesses the rows in the first resultset, | ||
because it's the most common use case. | ||
|
||
We want to get the rows retrieved by the `SELECT` statement, | ||
which corresponds to the third resultset. | ||
[refmem results at] returns a [reflink resultset_view] containing data | ||
for the requested resultset: | ||
|
||
[tutorial_updates_transactions_dynamic_results] | ||
|
||
|
||
[heading Using manual indices in with_params] | ||
|
||
Repeating `employee_id` in the parameter list passed to `with_params` | ||
violates the DRY principle. | ||
As with `std::format`, we can refer to a format argument more than once | ||
by using manual indices: | ||
|
||
[tutorial_updates_transactions_manual_indices] | ||
|
||
|
||
|
||
[heading Using the static interface with multi-resultset] | ||
|
||
Finally, we can rewrite our code to use the static interface so it's safer. | ||
In multi-resultset scenarios, we can pass as many row types | ||
to [reflink static_results] as resultsets we expect. | ||
We can use the empty tuple (`std::tuple<>`) as a row type | ||
for operations that don't return rows, like the `UPDATE`. | ||
Our code becomes: | ||
|
||
[tutorial_updates_transactions_static] | ||
|
||
|
||
[heading Wrapping up] | ||
|
||
Full program listing for this tutorial is [link mysql.examples.tutorial_updates_transactions here]. | ||
|
||
You can now proceed to [link mysql.tutorial_connection_pool the next tutorial]. | ||
|
||
|
||
[endsect] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
[/ | ||
Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 at gmail dot com) | ||
|
||
Distributed under the Boost Software License, Version 1.0. (See accompanying | ||
file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) | ||
] | ||
|
||
[section:tutorial_connection_pool Tutorial 6: Connection pools] | ||
|
||
All our programs until now have used one-shot connections. | ||
They also didn't feature any fault tolerance: | ||
if the server is unavailable, our program throws an exception | ||
and terminates. Most real world scenarios require | ||
long-lived, reliable connections, instead. | ||
|
||
In this tutorial, we will implement a server for a simple request-reply | ||
protocol. The protocol allows clients to retrieve the full name of | ||
an employee given their ID. | ||
We will use [reflink connection_pool] to maintain a set of healthy connections | ||
that we can use when a client connects to our server. | ||
|
||
|
||
|
||
|
||
[heading The protocol] | ||
|
||
The protocol is TCP based, and can be described as follows: | ||
|
||
* After connecting, the client sends a message containing the employee ID, | ||
encoded as an 8-byte, big-endian integer. | ||
* The server replies with a string containing the employee full name, | ||
or "NOT_FOUND", if the ID doesn't match any employee. | ||
* The connection is closed after that. | ||
|
||
This protocol is intentionally overly simplistic, and | ||
shouldn't be used in production. See our | ||
[link mysql.examples.connection_pool HTTP examples] | ||
for more advanced use cases. | ||
|
||
|
||
|
||
|
||
[heading Creating a connection pool] | ||
|
||
[reflink connection_pool] is an I/O object that contains | ||
[reflink any_connection] objects, and can be | ||
constructed from an execution context and a [reflink pool_params] | ||
config struct: | ||
|
||
[tutorial_connection_pool_create] | ||
|
||
A single connection pool is usually created per application. | ||
|
||
[refmem connection_pool async_run] should be called once per pool: | ||
|
||
[tutorial_connection_pool_run] | ||
|
||
|
||
|
||
|
||
|
||
|
||
[heading Using pooled connections] | ||
|
||
Let's first write a coroutine that encapsulates database access. | ||
Given an employee ID, it should return the string to be sent as response to the client. | ||
Don't worry about error handling for now - we will take care of it in the next tutorial. | ||
|
||
When using a pool, we don't need to explicitly create, connect or close connections. | ||
Instead, we use [refmem connection_pool async_get_connection] to obtain them from the pool: | ||
|
||
[tutorial_connection_pool_get_connection] | ||
|
||
[reflink pooled_connection] is a wrapper around [reflink any_connection], | ||
with some pool-specific additions. We can use it like a regular connection: | ||
|
||
[tutorial_connection_pool_use] | ||
|
||
When a [reflink pooled_connection] is destroyed, the connection is returned | ||
to the pool. The underlying connection will be cleaned up using a lightweight | ||
session reset mechanism and recycled. | ||
Subsequent [refmemunq connection_pool async_get_connection] | ||
calls may retrieve the same connection. This improves efficiency, | ||
since session establishment is costly. | ||
|
||
[refmemunq connection_pool async_get_connection] waits | ||
for a client connection to become available before completing. | ||
If the server is unavailable or credentials are invalid, | ||
it may wait indefinitely. This is a problem for both development and production. | ||
We can solve this by using [asioreflink cancel_after cancel_after], | ||
which allows setting timeouts to async operations: | ||
|
||
[tutorial_connection_pool_get_connection_timeout] | ||
|
||
Don't worry if you don't fully understand how this works. | ||
We will go into more detail on [asioreflink cancel_after cancel_after], | ||
cancellations and completion tokens in the next tutorial. | ||
|
||
Putting all pieces together, our coroutine becomes: | ||
|
||
[tutorial_connection_pool_db] | ||
|
||
|
||
|
||
|
||
[heading Handling a client session] | ||
|
||
Let's now build a function that handles a client sessions, | ||
invoking the database access logic in the process: | ||
|
||
[tutorial_connection_pool_session] | ||
|
||
|
||
|
||
|
||
[heading Listening for connections] | ||
|
||
We now need logic to accept incoming TCP connections. | ||
We will use an `asio::ip::tcp::acceptor` object | ||
to accomplish it, listening for connections in a loop | ||
until the server is stopped: | ||
|
||
[tutorial_connection_pool_listener] | ||
|
||
|
||
|
||
|
||
[heading Waiting for signals] | ||
|
||
Finally, we need a way to stop our program. We will use an `asio::signal_set` object | ||
to catch signals, and call `io_context::stop` when Ctrl-C is pressed: | ||
|
||
[tutorial_connection_pool_signals] | ||
|
||
Putting all these pieces together, our main program becomes: | ||
|
||
[tutorial_connection_pool_main] | ||
|
||
|
||
|
||
|
||
[heading Wrapping up] | ||
|
||
Full program listing for this tutorial is [link mysql.examples.tutorial_connection_pool here]. | ||
|
||
For simplicity, we've left error handling out of this tutorial. | ||
This is usually very important in a server like the one we've written, | ||
and is the topic of our [link mysql.tutorial_error_handling next tutorial]. | ||
|
||
[endsect] |
Oops, something went wrong.