2
0
mirror of https://github.com/boostorg/mysql.git synced 2026-01-20 04:42:19 +00:00
Files
mysql/example/3_advanced/http_server_cpp20/repository.cpp
Anarthal (Rubén Pérez) 793b678287 Updated file copyrights to 2025
2025-02-11 20:42:41 +01:00

370 lines
12 KiB
C++

//
// Copyright (c) 2019-2025 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)
//
#include <boost/asio/awaitable.hpp>
#include <boost/pfr/config.hpp>
#if defined(BOOST_ASIO_HAS_CO_AWAIT) && BOOST_PFR_CORE_NAME_ENABLED
//[example_http_server_cpp20_repository_cpp
//
// File: repository.cpp
//
// See the db_setup.sql file in this folder for the table definitions
#include <boost/mysql/connection_pool.hpp>
#include <boost/mysql/static_results.hpp>
#include <boost/mysql/with_params.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/system/result.hpp>
#include <string>
#include <string_view>
#include <tuple>
#include <vector>
#include "error.hpp"
#include "repository.hpp"
#include "types.hpp"
namespace mysql = boost::mysql;
namespace asio = boost::asio;
using namespace orders;
asio::awaitable<std::vector<product>> db_repository::get_products(std::string_view search)
{
// Get a connection from the pool
auto conn = co_await pool_.async_get_connection();
// Get the products using the MySQL built-in full-text search feature.
// Look for the query string in the short_name and descr fields.
// Parse the query results into product struct instances
mysql::static_results<product> res;
co_await conn->async_execute(
mysql::with_params(
"SELECT id, short_name, descr, price FROM products "
"WHERE MATCH(short_name, descr) AGAINST({}) "
"LIMIT 10",
search
),
res
);
// By default, connections are reset after they are returned to the pool
// (by using any_connection::async_reset_connection). This will reset any
// session state we changed while we were using the connection
// (e.g. it will deallocate any statements we prepared).
// We did nothing to mutate session state, so we can tell the pool to skip
// this step, providing a minor performance gain.
// We use pooled_connection::return_without_reset to do this.
// If an exception was raised, the connection would be reset, for safety.
conn.return_without_reset();
// Return the result
co_return std::vector<product>{res.rows().begin(), res.rows().end()};
}
asio::awaitable<std::vector<order>> db_repository::get_orders()
{
// Get a connection from the pool
auto conn = co_await pool_.async_get_connection();
// Get all the orders.
// Parse the result into order structs.
mysql::static_results<order> res;
co_await conn->async_execute("SELECT id, status FROM orders", res);
// We didn't mutate session state, so we can skip resetting the connection
conn.return_without_reset();
// Return the result
co_return std::vector<order>{res.rows().begin(), res.rows().end()};
}
asio::awaitable<boost::system::result<order_with_items>> db_repository::get_order_by_id(std::int64_t id)
{
// Get a connection from the pool
auto conn = co_await pool_.async_get_connection();
// Get a single order and all its associated items.
// The transaction ensures atomicity between the two SELECTs.
// We issued 4 queries, so we get 4 resultsets back.
// Ignore the 1st and 4th, and parse the other two into order and order_item structs
mysql::static_results<std::tuple<>, order, order_item, std::tuple<>> result;
co_await conn->async_execute(
mysql::with_params(
"START TRANSACTION READ ONLY;"
"SELECT id, status FROM orders WHERE id = {0};"
"SELECT id, product_id, quantity FROM order_items WHERE order_id = {0};"
"COMMIT",
id
),
result
);
// We didn't mutate session state
conn.return_without_reset();
// result.rows<N> returns the rows for the N-th resultset, as a span
auto orders = result.rows<1>();
auto order_items = result.rows<2>();
// Did we find the order we're looking for?
if (orders.empty())
co_return orders::errc::not_found;
const order& ord = orders[0];
// If we did, compose the result
co_return order_with_items{
ord.id,
ord.status,
{order_items.begin(), order_items.end()}
};
}
asio::awaitable<order_with_items> db_repository::create_order()
{
// Get a connection from the pool
auto conn = co_await pool_.async_get_connection();
// Create the new order.
// Orders are created empty, with all fields defaulted.
// MySQL does not have an INSERT ... RETURNING statement, so we use
// a transaction with an INSERT and a SELECT to create the order
// and retrieve it atomically.
// This yields 4 resultsets, one per SQL statement.
// Ignore all except the SELECT, and parse it into an order struct.
mysql::static_results<std::tuple<>, std::tuple<>, order, std::tuple<>> result;
co_await conn->async_execute(
"START TRANSACTION;"
"INSERT INTO orders () VALUES ();"
"SELECT id, status FROM orders WHERE id = LAST_INSERT_ID();"
"COMMIT",
result
);
// We didn't mutate session state
conn.return_without_reset();
// This must always yield one row. Return it.
const order& ord = result.rows<2>().front();
co_return order_with_items{
ord.id,
ord.status,
{} // A newly created order never has items
};
}
asio::awaitable<boost::system::result<order_with_items>> db_repository::add_order_item(
std::int64_t order_id,
std::int64_t product_id,
std::int64_t quantity
)
{
// Get a connection from the pool
auto conn = co_await pool_.async_get_connection();
// Retrieve the order and the product.
// SELECT ... FOR UPDATE places a lock on the retrieved rows,
// so they're not modified by other transactions while we use them.
// If you're targeting MySQL 8.0+, you can also use SELECT ... FOR SHARE.
// For the product, we only need to check that it does exist,
// so we get its ID and parse the returned rows into a std::tuple.
mysql::static_results<std::tuple<>, order, std::tuple<std::int64_t>> result1;
co_await conn->async_execute(
mysql::with_params(
"START TRANSACTION;"
"SELECT id, status FROM orders WHERE id = {} FOR UPDATE;"
"SELECT id FROM products WHERE id = {} FOR UPDATE",
order_id,
product_id
),
result1
);
// Check that the order exists
if (result1.rows<1>().empty())
{
// Not found. We did mutate session state by opening a transaction,
// so we can't use return_without_reset
co_return orders::errc::not_found;
}
const order& ord = result1.rows<1>().front();
// Verify that the order is editable.
// Using SELECT ... FOR UPDATE prevents race conditions with this check.
if (ord.status != status_draft)
{
co_return orders::errc::order_invalid_status;
}
// Check that the product exists
if (result1.rows<2>().empty())
{
co_return orders::errc::product_not_found;
}
// Insert the new item and retrieve all the items associated to this order
mysql::static_results<std::tuple<>, order_item, std::tuple<>> result2;
co_await conn->async_execute(
mysql::with_params(
"INSERT INTO order_items (order_id, product_id, quantity) VALUES ({0}, {1}, {2});"
"SELECT id, product_id, quantity FROM order_items WHERE order_id = {0};"
"COMMIT",
order_id,
product_id,
quantity
),
result2
);
// If everything went well, we didn't mutate session state
conn.return_without_reset();
// Compose the return value
co_return order_with_items{
ord.id,
ord.status,
{result2.rows<1>().begin(), result2.rows<1>().end()}
};
}
asio::awaitable<boost::system::result<order_with_items>> db_repository::remove_order_item(std::int64_t item_id
)
{
// Get a connection from the pool
auto conn = co_await pool_.async_get_connection();
// Retrieve the order.
// SELECT ... FOR UPDATE places a lock on the order and the item,
// so they're not modified by other transactions while we use them.
mysql::static_results<std::tuple<>, order> result1;
co_await conn->async_execute(
mysql::with_params(
"START TRANSACTION;"
"SELECT ord.id AS id, status FROM orders ord"
" JOIN order_items it ON (ord.id = it.order_id)"
" WHERE it.id = {} FOR UPDATE",
item_id
),
result1
);
// Check that the item exists
if (result1.rows<1>().empty())
{
// Not found. We did mutate session state by opening a transaction,
// so we can't use return_without_reset
co_return orders::errc::not_found;
}
const order& ord = result1.rows<1>().front();
// Check that the order is editable
if (ord.status != orders::status_draft)
{
co_return orders::errc::order_invalid_status;
}
// Perform the deletion and retrieve the items
mysql::static_results<std::tuple<>, order_item, std::tuple<>> result2;
co_await conn->async_execute(
mysql::with_params(
"DELETE FROM order_items WHERE id = {};"
"SELECT id, product_id, quantity FROM order_items WHERE order_id = {};"
"COMMIT",
item_id,
ord.id
),
result2
);
// If everything went well, we didn't mutate session state
conn.return_without_reset();
// Compose the return value
co_return order_with_items{
ord.id,
ord.status,
{result2.rows<1>().begin(), result2.rows<1>().end()}
};
}
// Helper function to implement checkout_order and complete_order
static asio::awaitable<boost::system::result<order_with_items>> change_order_status(
mysql::connection_pool& pool,
std::int64_t order_id,
std::string_view original_status, // The status that the order should have
std::string_view target_status // The status to transition the order to
)
{
// Get a connection from the pool
auto conn = co_await pool.async_get_connection();
// Retrieve the order and lock it.
// FOR UPDATE places an exclusive lock on the order,
// preventing other concurrent transactions (including the ones
// related to adding/removing items) from changing the order
mysql::static_results<std::tuple<>, std::tuple<std::string>> result1;
co_await conn->async_execute(
mysql::with_params(
"START TRANSACTION;"
"SELECT status FROM orders WHERE id = {} FOR UPDATE;",
order_id
),
result1
);
// Check that the order exists
if (result1.rows<1>().empty())
{
co_return orders::errc::not_found;
}
// Check that the order is in the expected status
if (std::get<0>(result1.rows<1>().front()) != original_status)
{
co_return orders::errc::order_invalid_status;
}
// Update the order and retrieve the order details
mysql::static_results<std::tuple<>, order_item, std::tuple<>> result2;
co_await conn->async_execute(
mysql::with_params(
"UPDATE orders SET status = {1} WHERE id = {0};"
"SELECT id, product_id, quantity FROM order_items WHERE order_id = {0};"
"COMMIT",
order_id,
target_status
),
result2
);
// If everything went well, we didn't mutate session state
conn.return_without_reset();
// Compose the return value
co_return order_with_items{
order_id,
std::string(target_status),
{result2.rows<1>().begin(), result2.rows<1>().end()}
};
}
asio::awaitable<boost::system::result<order_with_items>> db_repository::checkout_order(std::int64_t id)
{
return change_order_status(pool_, id, status_draft, status_pending_payment);
}
asio::awaitable<boost::system::result<order_with_items>> db_repository::complete_order(std::int64_t id)
{
return change_order_status(pool_, id, status_pending_payment, status_complete);
}
//]
#endif