ormpp

repository·master·Indexed 23 days ago

https://github.com/qicosmos/ormpp

A modern, header-only C++17 ORM library providing a unified, type-safe interface for MySQL, PostgreSQL, and SQLite. It features compile-time reflection, a chainable SQL builder for CRUD and aggregate queries, built-in connection pooling, asynchronous MySQL support via ASIO, and range partitioning. Supports cross-platform development on Linux, macOS, and Windows, with optional SQLCipher encryption for SQLite.

Tokens
29.1K
Snippets
73
Records
100
Agent score
80%

What's inside ormpp

  1. Overview of ormpp

    master

    ormpp is a modern C++ ORM (Object-Relational Mapping) library designed to simplify database programming in C++. It provides a unified interface across multiple database backends, allowing developers to switch between databases with minimal code changes.

    Key features include:

    • Header-only: No compilation required; just include the headers.
    • Cross-platform: Supports Linux, macOS, and Windows.
    • Unified Interface: Supports MySQL, PostgreSQL, and SQLite with a consistent API.
    • Type-safe SQL Builder: Uses compile-time reflection and safe chained calls to ensure field types are checked at compile time.
    • Built-in Connection Pool: Supports automatic recovery and health checks.
    • Asynchronous MySQL: Supports non-blocking queries via ASIO/async_simple.
    • AOP Support: Allows logging and validation via warper_connect.
    • SQLite Encryption: Supports SQLCipher for encrypted storage.
  2. Summary of ORMPP Async Connection Pool benefits

    master

    The ORMPP asynchronous connection pool provides the following key benefits for high-performance database applications:

    • High Performance: Reuses connections to reduce the overhead of establishing new ones.
    • Ease of Use: Uses RAII (Resource Acquisition Is Initialization) for automatic connection management, meaning you do not need to manually return connections to the pool.
    • Reliability: Includes automatic reconnection logic and connection health checks.
    • Concurrency Safety: Designed with thread safety based on strand.
    • Coroutine Friendly: Fully asynchronous implementation that does not block the event loop.
  3. Understand the ormpp asynchronous architecture

    master

    ormpp uses a four-layer architecture to provide fully asynchronous database operations that integrate with coroutine frameworks like async_simple. This design ensures that database I/O and business logic can run on the same non-blocking event loop, preventing thread blocking in high-concurrency services.

    The Four Layers

    1. mysql_async (Protocol Layer): A direct implementation of the MySQL client protocol where all I/O is performed using ASIO coroutines.
    2. AsioAwaitableAdapter / SafeAsioAwaitableAdapter (Bridge Layer): Bridges asio::awaitable into an awaiter that can be used with co_await in async_simple::Lazy.
    3. dbng<mysql_async> + Chained Query API (Interface Layer): Provides the unified chained query API (e.g., select(), insert(), update()) and transaction management on top of the adapter.
    4. async_connection_pool (Management Layer): Manages connection pools, supporting health checks, dynamic scaling, and timeout waiting.
    ┌─────────────────────────────────────────────────────────────┐
    │  业务代码 (async_simple::Lazy)                               │
    │  co_await db.await(db.raw().select(...).from<T>().collect())│
    └────────────────────────────────────────┬────────────────────┘
                             │ co_await
                             ▼
    ┌─────────────────────────────────────────────────────────────┐
    │  AsioAwaitableAdapter / SafeAsioAwaitableAdapter            │
    │  协程框架桥接层                                              │
    └────────────────────────────────────────┬────────────────────┘
                             │ asio::co_spawn
                             ▼
    ┌─────────────────────────────────────────────────────────────┐
    │  dbng<mysql_async>                                          │
    │  异步数据库接口 (ASIO 协程)                                  │
    │  - select().from<T>().where(...).collect()                  │
    │  - insert() / update() / delete()                           │
    │  - execute() / begin() / commit() / rollback()             │
    └────────────────────────────────────────┬────────────────────┘
                             │ asio::async_read/write
                             ▼
    ┌─────────────────────────────────────────────────────────────┐
    │  MySQL TCP 连接                                             │
    │  完整实现 MySQL 客户端协议                                   │
    └─────────────────────────────────────────────────────────────┘
  4. Support for optional, enum, and BLOB types

    master

    ORMPP supports advanced C++ types within your mapped structures:

    1. Optional Types: Use std::optional<T> to map columns that allow NULL values. When querying, use .has_value() to check for presence.
    2. Enum Types: Map enum class directly to database integer values. Ensure the enum values correspond to the database representation.
    3. BLOB Data: Use ormpp::blob to handle binary data. Use .assign(ptr, end_ptr) to load data and .size() to retrieve it.

    All types must be registered using REGISTER_AUTO_KEY and YLT_REFL (or similar reflection macros) to be compatible with the ORM.

    // Optional Example
    struct OptionalPerson {
      int id;
      std::optional<std::string> name;
      std::optional<int> age;
    };
    REGISTER_AUTO_KEY(OptionalPerson, id)
    YLT_REFL(OptionalPerson, id, name, age)
    
    // Enum Example
    enum class Gender { Male = 0, Female = 1 };
    struct PersonWithEnum {
      int id;
      std::string name;
      Gender gender;
    };
    REGISTER_AUTO_KEY(PersonWithEnum, id)
    YLT_REFL(PersonWithEnum, id, name, gender)
    
    // BLOB Example
    struct BlobData {
      int id;
      ormpp::blob data;
    };
    YLT_REFL(BlobData, id, data)
  5. Use Aspect-Oriented Programming (AOP) with `warper_connect`

    master

    You can wrap your database connection with AOP (Aspect-Oriented Programming) to inject logic like logging or validation before and after operations.

    To use this, define a struct with before and/or after template methods. The before method can intercept arguments, and the after method can intercept the return value.

  6. Map C++ enums to database integer fields

    master

    ormpp automatically maps C++ enum and enum class types to database integer fields. This allows you to use strongly typed enums in your C++ structs while storing them as integers in the database.

    enum class Color { BLUE = 10, RED = 15 };
    enum Fruit { APPLE, BANANA };
    
    struct test_enum_t {
      Color color;
      Fruit fruit;
      int id;
    };
    YLT_REFL(test_enum_t, color, fruit, id);
    
    mysql.create_datatable<test_enum_t>(ormpp_auto_key{"id"});
    mysql.insert<test_enum_t>({Color::BLUE, APPLE, 0});
    
    auto vec = mysql.query<test_enum_t>();
    // vec[0].color == Color::BLUE
  7. Bridge ASIO and async_simple coroutines

    master

    ormpp uses ASIO coroutines for asynchronous database interfaces, while business logic may use the async_simple framework. Because these two frameworks use different schedulers and suspension mechanisms, they cannot directly co_await each other.

    To bridge them, ormpp provides two types of adapters:

    1. AsioAwaitableAdapter (Lightweight): Use this when you have a single-threaded executor (only one thread calling io_context::run()), a shared execution context between ASIO and async_simple, and structured concurrency where the parent Lazy coroutine is guaranteed not to be destroyed while waiting.
    2. SafeAsioAwaitableAdapter (Safe): Use this for multi-threaded executors (multiple threads calling run() on the same io_context), scenarios where the awaiter might be destroyed prematurely (e.g., parent coroutine cancellation), or when you need explicit cancellation support.
  8. Use the placeholder mechanism for parameterized queries

    master

    To prevent SQL injection and allow dynamic values in your queries, use the token placeholder. When you use token in a clause (like where, limit, or offset), you must provide the corresponding runtime values in the final collect() or scalar() call.

    Alternatively, if a value is fixed and known at compile time, you can pass it directly to the method (e.g., .limit(10)) instead of using token to avoid the extra parameter in collect().

    // Using token for runtime parameters
    co_await db.select(all)
        .from<person>()
        .where(col(&person::name).param())   // → WHERE name = ?
        .limit(token)                         // → LIMIT ?
        .offset(token)                        // → OFFSET ?
        .collect("Alice", 10, 0);            // Alice → name, 10 → LIMIT, 0 → OFFSET
    
    // Using fixed values directly
    co_await db.select(all)
        .from<person>()
        .limit(10)    // → LIMIT 10
        .offset(0);   // → OFFSET 0
  9. How `async_connection_pool` works

    master

    The async_connection_pool is a coroutine-based connection pool designed for ORMPP's asynchronous database interfaces (such as mysql_async). It is built on Asio coroutines and provides several key features:

    • Coroutine Friendly: Fully implemented using Asio coroutines.
    • Automatic Reconnection: Automatically reconnects when a connection is detected as invalid.
    • RAII Management: Uses std::shared_ptr with a custom deleter to automatically return connections to the pool when they go out of scope.
    • Thread Safety: Uses asio::strand to ensure thread-safe connection management.
    • Timeout Control: Supports setting timeouts when requesting a connection.
    • Statistics: Allows querying the current state of the pool (total, available, in-use, and dynamic connections).
  10. Use AsioAwaitableAdapter for lightweight coroutine bridging

    master

    The AsioAwaitableAdapter<T> is a high-performance adapter designed for single-threaded environments. It avoids heap allocations, mutexes, and atomic variables by storing results directly in the awaiter object.

    Constraints for use:

    • Single-threaded io_context::run().
    • Shared underlying io_context for both ASIO and async_simple executors.
    • Parent Lazy coroutine will not be cancelled or destroyed during the wait.

    Key implementation details:

    • It implements await_ready, await_suspend, and await_resume for C++ coroutine compatibility.
    • It provides a coAwait(async_simple::Executor*) method to allow async_simple to recognize the custom awaiter.
    template <typename T>
    class AsioAwaitableAdapter {
     public:
      AsioAwaitableAdapter(asio::awaitable<T> awaitable, asio::any_io_executor executor);
      bool await_ready() const noexcept;
      bool await_suspend(std::coroutine_handle<>) noexcept;
      T await_resume();
      auto coAwait(async_simple::Executor*) noexcept;
    };
  11. Choose between Lightweight and Safe adapters

    master

    ormpp provides two types of adapters for asynchronous operations. Choosing the right one depends on your concurrency model and requirements.

    Lightweight Adapter (AsioAwaitableAdapter)

    Use when:

    • You have a single-threaded executor (one io_context run by one thread).
    • The parent coroutine is guaranteed not to be destroyed while awaiting.
    • You do not need cancellation support.
    • You want maximum performance (high-frequency short queries).

    Safe Adapter

    Use when:

    • You are in a multi-threaded environment.
    • The awaiter might be destroyed prematurely.
    • You require cancellation support.
    • Queries are long-running (> 100 μs), making the overhead negligible.
  12. Use SafeAsioAwaitableAdapter for thread-safe coroutine bridging

    master

    The SafeAsioAwaitableAdapter<T> is designed for complex, multi-threaded, or high-reliability environments. It uses a SharedState managed by std::shared_ptr to ensure that even if the awaiter is destroyed, the asynchronous operation can complete safely without causing dangling pointers.

    Features:

    • Thread Safety: Uses asio::strand to serialize all non-atomic state modifications.
    • Lifecycle Safety: Uses std::shared_ptr<SharedState> so the ASIO side can continue working even if the parent coroutine is destroyed.
    • Cancellation Support: Provides a cancellation_handle to request cancellation of the underlying ASIO operation.

    When to use:

    • Multi-threaded io_context::run().
    • When the awaiter might be destroyed before completion.
    • When you need to support cancellation requests.
    template <typename T>
    class SafeAsioAwaitableAdapter {
     public:
      class cancellation_handle {
       public:
        bool valid() const noexcept;
        bool cancel(asio::cancellation_type) const noexcept;
      };
    
      SafeAsioAwaitableAdapter(asio::awaitable<T>, asio::any_io_executor executor);
      ~SafeAsioAwaitableAdapter();
    
      bool await_ready() const noexcept;
      bool await_suspend(std::coroutine_handle<>) noexcept;
      T await_resume();
    
      cancellation_handle get_cancellation_handle() const noexcept;
      bool cancel(asio::cancellation_type) const noexcept;
      auto coAwait(async_simple::Executor*) noexcept;
    };