mirror of
https://github.com/getml/sqlgen.git
synced 2026-01-04 08:30:30 -06:00
Added a caching strategy (#83)
This commit is contained in:
committed by
GitHub
parent
4cc3e871c3
commit
86ee6e2bcc
@@ -33,6 +33,7 @@ Welcome to the sqlgen documentation. This guide provides detailed information ab
|
||||
|
||||
## Other Operations
|
||||
|
||||
- [Cache](cache.md) - How to improve performance with caching.
|
||||
- [Mathematical Operations](mathematical_operations.md) - How to use mathematical functions in queries (e.g., abs, ceil, floor, exp, trigonometric functions, round).
|
||||
- [String Operations](string_operations.md) - How to manipulate and transform strings in queries (e.g., length, lower, upper, trim, replace, concat).
|
||||
- [Type Conversion Operations](type_conversion_operations.md) - How to convert between types safely in queries (e.g., cast int to double).
|
||||
@@ -62,4 +63,4 @@ Welcome to the sqlgen documentation. This guide provides detailed information ab
|
||||
- [PostgreSQL](postgres.md) - How to interact with PostgreSQL and compatible databases (Redshift, Aurora, Greenplum, CockroachDB, ...)
|
||||
- [SQLite](sqlite.md) - How to interact with SQLite3
|
||||
|
||||
For installation instructions, quick start guide, and usage examples, please refer to the [main README](../README.md).
|
||||
For installation instructions, quick start guide, and usage examples, please refer to the [main README](../README.md).
|
||||
|
||||
65
docs/cache.md
Normal file
65
docs/cache.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# `sqlgen::cache`
|
||||
|
||||
The `sqlgen` library provides a high-performance caching mechanism to reduce database load and improve query performance. The cache is designed to be thread-safe and uses a first-in-first-out (FIFO) eviction policy.
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Caching Example
|
||||
|
||||
To use the cache, you can wrap a query with `sqlgen::cache`. The first time the query is executed, the result is fetched from the database and stored in the cache. Subsequent executions of the same query will return the cached result.
|
||||
|
||||
```cpp
|
||||
#include <sqlgen.hpp>
|
||||
|
||||
// Define a table
|
||||
struct User {
|
||||
std::string name;
|
||||
int age;
|
||||
};
|
||||
|
||||
// Create a query
|
||||
const auto query = sqlgen::read<User> | where("name"_c == "John");
|
||||
|
||||
// Wrap the query with the cache
|
||||
const auto cached_query = sqlgen::cache<100>(query);
|
||||
|
||||
// Execute the query
|
||||
const auto user1 = cached_query(conn).value();
|
||||
const auto user2 = cached_query(conn).value();
|
||||
|
||||
// Also OK
|
||||
const auto user3 = conn.and_then(
|
||||
cache<100>(sqlgen::read<User> | where("name"_c == "John"))).value();
|
||||
|
||||
// The cache size will be 1, because the second and third query were served from the cache.
|
||||
// auto cache_size = cached_query.cache(conn).size(); // cache_size is 1
|
||||
```
|
||||
|
||||
### How it Works
|
||||
|
||||
The cache stores the results of queries in memory, using the generated SQL string as the key. When a cached query is executed, `sqlgen` first checks if a result for the corresponding SQL query exists in the cache. If it does, the cached result is returned immediately. Otherwise, the query is executed against the database, and the result is stored in the cache before being returned.
|
||||
|
||||
### Eviction Policy
|
||||
|
||||
The cache uses a simple first-in-first-out (FIFO) eviction policy. When the cache reaches its maximum size, the oldest entry is removed to make space for the new one. The maximum size of the cache is specified as a template parameter to `sqlgen::cache`.
|
||||
|
||||
To create a cache with a virtually unlimited size, you can specify a `max_size` of `0`:
|
||||
|
||||
```cpp
|
||||
// This cache will grow indefinitely.
|
||||
const auto cached_query = sqlgen::cache<0>(query);
|
||||
```
|
||||
|
||||
### Thread Safety and Concurrency
|
||||
|
||||
The cache is thread-safe and can be accessed from multiple threads concurrently. A `std::shared_mutex` is used to protect the cache from data races.
|
||||
|
||||
It is important to understand the cache's behavior under high contention. If multiple threads request the same uncached query at the exact same time, the database query might be executed multiple times in parallel. However, as soon as one thread has started the query and placed a future for its result into the cache, any other threads that subsequently request the same query will not start a new database operation. Instead, they will wait for the result of the already running query. This mechanism prevents a "cache stampede" for all but the initial concurrent requests.
|
||||
|
||||
## Notes
|
||||
|
||||
- The cache is enabled by wrapping a query with `sqlgen::cache`.
|
||||
- The cache uses a FIFO eviction policy.
|
||||
- The maximum size of the cache can be configured.
|
||||
- The cache is thread-safe.
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "sqlgen/aggregations.hpp"
|
||||
#include "sqlgen/as.hpp"
|
||||
#include "sqlgen/begin_transaction.hpp"
|
||||
#include "sqlgen/cache.hpp"
|
||||
#include "sqlgen/cascade.hpp"
|
||||
#include "sqlgen/col.hpp"
|
||||
#include "sqlgen/commit.hpp"
|
||||
|
||||
120
include/sqlgen/cache.hpp
Normal file
120
include/sqlgen/cache.hpp
Normal file
@@ -0,0 +1,120 @@
|
||||
#ifndef SQLGEN_CACHE_HPP_
|
||||
#define SQLGEN_CACHE_HPP_
|
||||
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <limits>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
|
||||
#include "Ref.hpp"
|
||||
#include "Result.hpp"
|
||||
#include "internal/query_value_t.hpp"
|
||||
#include "is_connection.hpp"
|
||||
#include "transpilation/to_sql.hpp"
|
||||
|
||||
namespace sqlgen {
|
||||
|
||||
template <class QueryT, class Connection, size_t _max_size>
|
||||
requires is_connection<Connection>
|
||||
class CacheImpl {
|
||||
static constexpr size_t max_size_ =
|
||||
_max_size == 0 ? std::numeric_limits<size_t>::max() : _max_size;
|
||||
|
||||
public:
|
||||
using ValueType = internal::query_value_t<QueryT, Ref<Connection>>;
|
||||
|
||||
static Result<ValueType> fetch(const QueryT& _query,
|
||||
const Ref<Connection>& _conn) {
|
||||
const auto sql = _conn->to_sql(transpilation::to_sql(_query));
|
||||
|
||||
const auto try_read_from_cache =
|
||||
[&]() -> Result<std::shared_future<Result<ValueType>>> {
|
||||
std::shared_lock read_lock(mtx_);
|
||||
const auto it = cache_.find(sql);
|
||||
if (it != cache_.end()) {
|
||||
return it->second.first;
|
||||
}
|
||||
return error("Could not find the result for the query in the cache.");
|
||||
};
|
||||
|
||||
const auto write_to_cache =
|
||||
[&](const std::shared_future<Result<ValueType>>& _future) {
|
||||
std::unique_lock write_lock(mtx_);
|
||||
cache_[sql] = std::make_pair(_future, counter_++);
|
||||
if (cache_.size() > max_size_) {
|
||||
const auto it =
|
||||
std::min_element(cache_.begin(), cache_.end(),
|
||||
[](const auto& _p1, const auto& _p2) {
|
||||
return _p1.second.second < _p2.second.second;
|
||||
});
|
||||
cache_.erase(it);
|
||||
}
|
||||
};
|
||||
|
||||
return try_read_from_cache()
|
||||
.and_then([](auto _future) { return _future.get(); })
|
||||
.or_else([&](auto&&) -> Result<ValueType> {
|
||||
const std::shared_future<Result<ValueType>> future =
|
||||
std::async(std::launch::async, [&]() { return _query(_conn); });
|
||||
write_to_cache(future);
|
||||
return future.get();
|
||||
});
|
||||
}
|
||||
|
||||
static const auto& cache() { return cache_; }
|
||||
|
||||
private:
|
||||
inline static size_t counter_ = 0;
|
||||
|
||||
inline static std::unordered_map<
|
||||
std::string, std::pair<std::shared_future<Result<ValueType>>, size_t>>
|
||||
cache_;
|
||||
|
||||
inline static std::shared_mutex mtx_;
|
||||
};
|
||||
|
||||
template <class QueryT, size_t _max_size>
|
||||
struct Cache {
|
||||
template <class Connection>
|
||||
requires is_connection<Connection>
|
||||
auto operator()(const Ref<Connection>& _conn) const {
|
||||
return CacheImpl<QueryT, std::remove_cvref_t<Connection>, _max_size>::fetch(
|
||||
query_, _conn);
|
||||
}
|
||||
|
||||
template <class Connection>
|
||||
requires is_connection<Connection>
|
||||
auto operator()(const Result<Ref<Connection>>& _res) const {
|
||||
return _res.and_then(
|
||||
[&](const Ref<Connection>& _conn) { return (*this)(_conn); });
|
||||
}
|
||||
|
||||
template <class Connection>
|
||||
requires is_connection<Connection>
|
||||
static const auto& cache(const Ref<Connection>& _conn) {
|
||||
return CacheImpl<QueryT, std::remove_cvref_t<Connection>,
|
||||
_max_size>::cache();
|
||||
}
|
||||
|
||||
template <class Connection>
|
||||
requires is_connection<Connection>
|
||||
static const auto& cache(const Result<Ref<Connection>>& _res) {
|
||||
return CacheImpl<QueryT, std::remove_cvref_t<Connection>,
|
||||
_max_size>::cache();
|
||||
}
|
||||
|
||||
QueryT query_;
|
||||
};
|
||||
|
||||
template <size_t _max_size = 2056, class QueryT>
|
||||
auto cache(const QueryT& _query) {
|
||||
return Cache<std::remove_cvref_t<QueryT>, _max_size>{.query_ = _query};
|
||||
}
|
||||
|
||||
} // namespace sqlgen
|
||||
|
||||
#endif
|
||||
30
include/sqlgen/internal/query_value_t.hpp
Normal file
30
include/sqlgen/internal/query_value_t.hpp
Normal file
@@ -0,0 +1,30 @@
|
||||
#ifndef SQLGEN_INTERNAL_QUERYVALUET_HPP_
|
||||
#define SQLGEN_INTERNAL_QUERYVALUET_HPP_
|
||||
|
||||
#include <type_traits>
|
||||
|
||||
#include "../read.hpp"
|
||||
#include "../select_from.hpp"
|
||||
|
||||
namespace sqlgen::internal {
|
||||
|
||||
template <class QueryT, class ConnectionT>
|
||||
struct QueryValueType;
|
||||
|
||||
template <class ConnectionT, class... Args>
|
||||
struct QueryValueType<Read<Args...>, ConnectionT> {
|
||||
using Type = std::invoke_result_t<Read<Args...>, ConnectionT>;
|
||||
};
|
||||
|
||||
template <class ConnectionT, class... Args>
|
||||
struct QueryValueType<SelectFrom<Args...>, ConnectionT> {
|
||||
using Type = std::invoke_result_t<SelectFrom<Args...>, ConnectionT>;
|
||||
};
|
||||
|
||||
template <class QueryT, class ConnectionT>
|
||||
using query_value_t =
|
||||
typename QueryValueType<QueryT, ConnectionT>::Type::value_type;
|
||||
|
||||
} // namespace sqlgen::internal
|
||||
|
||||
#endif
|
||||
56
tests/mysql/test_cache.cpp
Normal file
56
tests/mysql/test_cache.cpp
Normal file
@@ -0,0 +1,56 @@
|
||||
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <rfl.hpp>
|
||||
#include <rfl/json.hpp>
|
||||
#include <sqlgen.hpp>
|
||||
#include <sqlgen/mysql.hpp>
|
||||
#include <vector>
|
||||
|
||||
namespace test_cache {
|
||||
|
||||
struct User {
|
||||
std::string name;
|
||||
int age;
|
||||
};
|
||||
|
||||
TEST(mysql, test_cache) {
|
||||
const auto credentials = sqlgen::mysql::Credentials{.host = "localhost",
|
||||
.user = "sqlgen",
|
||||
.password = "password",
|
||||
.dbname = "mysql"};
|
||||
|
||||
const auto conn = sqlgen::mysql::connect(credentials);
|
||||
|
||||
using namespace sqlgen;
|
||||
using namespace sqlgen::literals;
|
||||
|
||||
(sqlgen::drop<User> | if_exists)(conn);
|
||||
|
||||
const auto user = User{.name = "John", .age = 30};
|
||||
sqlgen::write(conn, user);
|
||||
|
||||
const auto query = sqlgen::read<User> | where("name"_c == "John");
|
||||
|
||||
const auto cached_query = sqlgen::cache<100>(query);
|
||||
|
||||
const auto user1 = conn.and_then(cache<100>(query)).value();
|
||||
|
||||
EXPECT_EQ(cached_query.cache(conn).size(), 1);
|
||||
|
||||
const auto user2 = cached_query(conn).value();
|
||||
const auto user3 = cached_query(conn).value();
|
||||
|
||||
EXPECT_EQ(user1.name, "John");
|
||||
EXPECT_EQ(user1.age, 30);
|
||||
EXPECT_EQ(user2.name, "John");
|
||||
EXPECT_EQ(user2.age, 30);
|
||||
EXPECT_EQ(cached_query.cache(conn).size(), 1);
|
||||
EXPECT_EQ(user3.name, "John");
|
||||
EXPECT_EQ(user3.age, 30);
|
||||
}
|
||||
|
||||
} // namespace test_cache
|
||||
|
||||
#endif
|
||||
56
tests/postgres/test_cache.cpp
Normal file
56
tests/postgres/test_cache.cpp
Normal file
@@ -0,0 +1,56 @@
|
||||
#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <rfl.hpp>
|
||||
#include <rfl/json.hpp>
|
||||
#include <sqlgen.hpp>
|
||||
#include <sqlgen/postgres.hpp>
|
||||
#include <vector>
|
||||
|
||||
namespace test_cache {
|
||||
|
||||
struct User {
|
||||
std::string name;
|
||||
int age;
|
||||
};
|
||||
|
||||
TEST(postgres, test_cache) {
|
||||
const auto credentials = sqlgen::postgres::Credentials{.user = "postgres",
|
||||
.password = "password",
|
||||
.host = "localhost",
|
||||
.dbname = "postgres"};
|
||||
|
||||
const auto conn = sqlgen::postgres::connect(credentials);
|
||||
|
||||
using namespace sqlgen;
|
||||
using namespace sqlgen::literals;
|
||||
|
||||
(sqlgen::drop<User> | if_exists)(conn);
|
||||
|
||||
const auto user = User{.name = "John", .age = 30};
|
||||
sqlgen::write(conn, user);
|
||||
|
||||
const auto query = sqlgen::read<User> | where("name"_c == "John");
|
||||
|
||||
const auto cached_query = sqlgen::cache<100>(query);
|
||||
|
||||
const auto user1 = conn.and_then(cache<100>(query)).value();
|
||||
|
||||
EXPECT_EQ(cached_query.cache(conn).size(), 1);
|
||||
|
||||
const auto user2 = cached_query(conn).value();
|
||||
const auto user3 = cached_query(conn).value();
|
||||
|
||||
EXPECT_EQ(user1.name, "John");
|
||||
EXPECT_EQ(user1.age, 30);
|
||||
EXPECT_EQ(user2.name, "John");
|
||||
EXPECT_EQ(user2.age, 30);
|
||||
EXPECT_EQ(cached_query.cache(conn).size(), 1);
|
||||
EXPECT_EQ(user3.name, "John");
|
||||
EXPECT_EQ(user3.age, 30);
|
||||
}
|
||||
|
||||
} // namespace test_cache
|
||||
|
||||
#endif
|
||||
45
tests/sqlite/test_cache.cpp
Normal file
45
tests/sqlite/test_cache.cpp
Normal file
@@ -0,0 +1,45 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <rfl.hpp>
|
||||
#include <rfl/json.hpp>
|
||||
#include <sqlgen.hpp>
|
||||
#include <sqlgen/sqlite.hpp>
|
||||
#include <vector>
|
||||
|
||||
namespace test_cache {
|
||||
|
||||
struct User {
|
||||
std::string name;
|
||||
int age;
|
||||
};
|
||||
|
||||
TEST(sqlite, test_cache) {
|
||||
const auto conn = sqlgen::sqlite::connect();
|
||||
|
||||
const auto user = User{.name = "John", .age = 30};
|
||||
sqlgen::write(conn, user);
|
||||
|
||||
using namespace sqlgen;
|
||||
using namespace sqlgen::literals;
|
||||
|
||||
const auto query = sqlgen::read<User> | where("name"_c == "John");
|
||||
|
||||
const auto cached_query = sqlgen::cache<100>(query);
|
||||
|
||||
const auto user1 = conn.and_then(cache<100>(query)).value();
|
||||
|
||||
EXPECT_EQ(cached_query.cache(conn).size(), 1);
|
||||
|
||||
const auto user2 = cached_query(conn).value();
|
||||
const auto user3 = cached_query(conn).value();
|
||||
|
||||
EXPECT_EQ(user1.name, "John");
|
||||
EXPECT_EQ(user1.age, 30);
|
||||
EXPECT_EQ(user2.name, "John");
|
||||
EXPECT_EQ(user2.age, 30);
|
||||
EXPECT_EQ(cached_query.cache(conn).size(), 1);
|
||||
EXPECT_EQ(user3.name, "John");
|
||||
EXPECT_EQ(user3.age, 30);
|
||||
}
|
||||
|
||||
} // namespace test_cache
|
||||
Reference in New Issue
Block a user