) | BLOB | bytea | BLOB |
| `ormpp::date` | DATE | date | TEXT |
| `ormpp::time` | TIME | time | TEXT |
| `ormpp::datetime` | DATETIME | timestamp | TEXT |
| `ormpp::timestamp` | TIMESTAMP | timestamp | TEXT |
| `ormpp::decimal` | DECIMAL(P,S) | numeric(P,S) | DECIMAL(P,S) |
| `enum` / `enum class` | INTEGER | integer | INTEGER |
| `std::optional` | 同 T 类型 | 同 T 类型 | 同 T 类型 |
### 日期、时间和定点数类型
下面五种类型用于明确表达字段的数据库语义,避免把日期、时间和金额全部声明成普通
`std::string`:
| C++ 类型 | 含义 | 推荐文本形式 | 说明 |
|---------|------|-------------|------|
| `ormpp::date` | 不带时间的日历日期 | `2026-08-29` | 适合生日、账期、营业日期等字段 |
| `ormpp::time` | 不带日期的时刻 | `14:30:05`、`14:30:05.123456` | 是否支持小数秒及其精度由数据库决定 |
| `ormpp::datetime` | 不带时区的日期和时间 | `2026-08-29 14:30:05` | MySQL 映射为 `DATETIME`,PostgreSQL 映射为 `timestamp` |
| `ormpp::timestamp` | 数据库的 TIMESTAMP 字段 | `2026-08-29 14:30:05` | 不是 Unix 整数时间戳;时区转换遵循数据库自身规则 |
| `ormpp::decimal` | 定点十进制数 | `12345.67` | `P` 是总位数,`S` 是小数位数,例如 `decimal<12, 2>` |
这些类型都是轻量文本包装,内部通过公开的 `value` 成员保存 `std::string`,可以从
字符串字面量或 `std::string` 构造:
```cpp
ormpp::date business_date{"2026-08-29"};
ormpp::time created_time{"14:30:05"};
ormpp::datetime paid_at{"2026-08-29 14:30:05"};
ormpp::timestamp synced_at{"2026-08-29 14:30:05"};
ormpp::decimal<12, 2> amount{"12345.67"};
std::cout << amount.value; // 12345.67
```
它们的作用是生成合适的列类型,并通过各数据库驱动按文本绑定和读取。ormpp 不解析日期,
不校验日期/时间范围,也不检查传入值是否满足 decimal 的 `P`、`S`;非法值是被数据库拒绝、
转换还是保留,取决于具体后端。应用需要日期运算、时区换算或精确十进制运算时,应先使用
专门的日期/decimal 库完成计算,再把最终文本写入包装类型。
`std::optional` 可以与这些类型组合。空的 optional 写入 SQL `NULL`,非空值按内部类型
映射,例如 `std::optional` 在 MySQL 中仍是可空的 `DATETIME` 列。
下面的示例完全使用 ormpp 的实体和链式接口建表、写入、更新与查询:
```cpp
#include
#include
#include "dbng.hpp"
#include "sqlite.hpp"
struct billing_record {
int id;
ormpp::date business_date;
ormpp::time created_time;
ormpp::datetime paid_at;
std::optional synced_at;
ormpp::decimal<12, 2> amount;
std::optional> discount_rate;
};
REGISTER_AUTO_KEY(billing_record, id)
ormpp::dbng database;
database.connect(":memory:");
database.create_table()
.auto_increment(ormpp::col(&billing_record::id))
.execute();
billing_record item{
0,
{"2026-08-29"},
{"14:30:05"},
{"2026-08-29 14:30:05"},
std::nullopt,
{"12345.67"},
ormpp::decimal<5, 2>{"12.50"},
};
database.insert(item);
database.update()
.set(ormpp::col(&billing_record::synced_at),
ormpp::timestamp{"2026-08-29 14:35:00"})
.where(ormpp::col(&billing_record::id) == 1)
.execute();
auto rows =
database.select(ormpp::all)
.from()
.where(ormpp::col(&billing_record::business_date) >=
ormpp::date{"2026-08-01"} &&
ormpp::col(&billing_record::amount) >
ormpp::decimal<12, 2>{"1000.00"})
.order_by(ormpp::col(&billing_record::amount).desc())
.collect();
if (!rows.empty()) {
std::cout << rows.front().amount.value; // 12345.67
}
```
MySQL 和 PostgreSQL 会使用各自的原生日期、时间及 decimal/numeric 类型,因此格式、范围、
精度和时区行为最终由服务器定义。特别是 MySQL `TIMESTAMP` 可能根据连接时区进行转换,而
`DATETIME` 不表达时区;ormpp 不在两者之间做隐式转换。
SQLite 将日期和时间类型保存为 `TEXT`。使用统一的、固定宽度且从高位到低位排列的 ISO 风格
文本(例如 `YYYY-MM-DD HH:MM:SS`),才能让字符串比较和排序与时间先后一致。SQLite 的
`DECIMAL(P,S)` 使用 NUMERIC affinity,使常规比较和排序遵循数值语义;但 SQLite 不强制
`P`/`S`,超出其原生整数/浮点表示能力的高精度 decimal 也不会获得额外精度保证。
MySQL 读取超出初始缓冲区的长文本或 blob 字段时,单列补取缓冲区默认上限为 64MB,可通过
`mysql::set_max_mysql_result_buffer_size()` 调整;超过
`mysql::mysql_result_buffer_size_hard_limit`(1GB)的设置会被截断到该上限。
查询结果中的 `std::string_view` 指向连接内部存储,在同一次查询的多行、多列结果中保持有效;
该连接发起下一次查询或被销毁后视图会失效,需要长期保存时请使用 `std::string`。
`std::array` 和 C 字符数组按 C 字符串语义绑定,遇到 `\0` 会结束;需要保存含零字节的数据时请使用
`blob`。
## 连接池
ormpp 内置了数据库连接池,支持自动创建、回收和健康检查,避免频繁创建/销毁连接带来的性能开销。
### 基本用法
```cpp
#include "connection_pool.hpp"
#include "mysql.hpp"
// 初始化连接池(单例,全局只需初始化一次)
// 参数:最大连接数, host, user, password, db, timeout, port
ormpp::connection_pool::instance().init(
10, "127.0.0.1", "root", "12345", "testdb", 5, 3306);
// 从连接池中获取连接(智能指针,超出作用域自动归还)
auto conn = ormpp::connection_pool::instance().get();
if (conn) {
conn->create_datatable();
conn->insert(p);
auto result = conn->query_s();
// conn 析构时自动归还到连接池
}
```
### 连接池特性
- **自动回收**:连接使用完毕后通过自定义 deleter 自动归还到池中
- **超时等待**:获取连接时最多等待 3 秒,超时返回 `nullptr`
- **健康检查**:自动检测连接是否存活(`ping`),失效连接会自动重建
- **空闲超时**:连接空闲超过 8 小时会自动重建,避免数据库端超时断开
- **线程安全**:连接池本身是线程安全的(`get()` / `init()` 内部使用 `std::mutex` 保护),但**从池中获取的单个连接对象不可跨线程并发使用**。如需多线程并发查询,每个线程应独立 `get()` 一个连接。
## 异步 MySQL
ormpp 支持基于 ASIO/async_simple 的异步 MySQL 查询,适合高并发场景。
### 编译选项
```bash
cmake -B build -DENABLE_MYSQL_ASYNC=ON
```
### 基本用法
```cpp
#include
#include "mysql_async.hpp"
struct person {
int id;
std::string name;
int age;
};
YLT_REFL(person, id, name, age);
asio::awaitable run_mysql_async() {
ormpp::mysql_async async_mysql(co_await asio::this_coro::executor);
bool connected =
co_await async_mysql.connect("127.0.0.1", "root", "12345", "testdb");
if (!connected) {
co_return;
}
// 异步查询
auto result = co_await async_mysql.query_s();
for (auto& p : result) {
std::cout << p.name << std::endl;
}
// 异步插入
person p{0, "tom", 20}; // id=0 表示自增
int affected = co_await async_mysql.insert(p);
}
int main() {
asio::io_context io;
asio::co_spawn(io, run_mysql_async(), asio::detached);
io.run();
}
```
### 异步连接池
```cpp
#include
#include "async_connection_pool.hpp"
#include "mysql_async.hpp"
asio::awaitable run_pool() {
auto executor = co_await asio::this_coro::executor;
auto pool =
std::make_shared>(
executor);
bool initialized =
co_await pool->init(10, "127.0.0.1", "root", "12345", "testdb");
if (!initialized) {
co_return;
}
auto conn = co_await pool->get();
if (conn) {
auto result = co_await conn->query_s();
}
}
int main() {
asio::io_context io;
asio::co_spawn(io, run_pool(), asio::detached);
io.run();
}
```
注意:异步接口需要 C++20 协程支持,编译器要求 GCC 10+、Clang 13+ 或 MSVC 2019 16.8+。
## 线程安全
### 问题背景
ormpp 底层依赖 iguana 的编译期反射。iguana 的部分反射元数据采用**懒加载(Lazy Initialization)**策略:首次查询某个实体类型时才会初始化字段映射信息。
在多线程场景下,如果多个线程**同时首次查询同一类型**,可能因并发初始化导致字段名映射错乱、字符串内存损坏(double-free)等问题。
### 线程安全保证范围
| 对象 | 线程安全性 | 说明 |
|------|-----------|------|
| `connection_pool` | ✅ 线程安全 | `init()`、`get()` 等操作内部使用 `std::mutex` 保护 |
| 单个 `dbng` / 连接对象 | ❌ 非线程安全 | 不可跨线程共享同一个连接实例 |
| `query` / `insert` / `update` | 依赖连接对象 | 底层调用数据库 C API,本身无额外锁保护 |
**推荐做法**:每个工作线程通过 `connection_pool::get()` 独立获取连接,用完自动归还。严禁多个线程同时操作同一个连接对象。
### 解决方案
#### 方案一:预初始化(推荐)
在启动工作线程之前,于主线程中显式调用 `ormpp::init_reflection()` 触发一次性的反射初始化:
```cpp
#include "utility.hpp"
struct person {
int id;
std::string name;
int age;
};
YLT_REFL(person, id, name, age);
int main() {
// 1. 单线程预初始化(主线程)
ormpp::init_reflection();
// 2. 此后可安全地在多线程中并发查询
std::vector threads;
for (int i = 0; i < 4; ++i) {
threads.emplace_back([]() {
dbng mysql;
mysql.connect("127.0.0.1", "root", "12345", "testdb");
auto result = mysql.query(); // 线程安全
});
}
for (auto &t : threads) t.join();
}
```
#### 方案二:自动保护(内部缓存)
ormpp 内部对 SQL 字段列表缓存(`get_fields()`)已使用 `std::call_once` 保护,确保多线程首次并发访问时只初始化一次。但**仍强烈建议在应用层做预初始化**,因为 iguana 层面的反射元数据初始化不在 ormpp 控制范围内。
### 适用场景
- ✅ 单线程顺序查询:无需额外处理
- ⚠️ 多线程同时首次查询**同一类型**:需要预初始化
- ✅ 多线程查询不同类型(每个类型首次查询单线程):无需额外处理
- ❌ 多线程共享同一连接对象:不支持,必须使用连接池或每线程独立连接
## roadmap
1. ✅ 支持组合键(已通过链式调用 `primary_key(col1, col2)` 实现)
2. ✅ 多表查询谓词(`where`, `group_by`, `having`, `order_by`, `join`, `limit`, `offset` 已支持)
3. ✅ 日志支持(可通过 AOP 切面 `warper_connect` 实现)
4. ✅ 连接池(已实现,见[连接池](#连接池))
5. ✅ 异步 MySQL(已实现,见[异步 MySQL](#异步-mysql))
6. 🔄 增加获取错误消息的公开接口
7. 🔄 支持更多的数据库(如 SQL Server、Oracle 等)
8. 🔄 完善事务的嵌套支持
9. 🔄 增加批量插入/更新的性能优化
**历史版本**
| 功能 | 版本 | 说明 |
|------|------|------|
| 链式查询 | v1.0+ | `select().from().where()` 类型安全构建器 |
| 聚合查询 | v1.0+ | `count()`, `sum()`, `avg()`, `min()`, `max()` |
| JOIN | v1.0+ | `inner_join()`, `left_join()` |
| 连接池 | v1.0+ | 单例连接池,自动回收 |
| 异步 MySQL | v1.0+ | ASIO/async_simple 协程支持 |
| AOP 切面 | v1.0+ | `warper_connect` 日志/校验切面 |
| 范围分区 | v1.0+ | MySQL/PostgreSQL/SQLite 统一分区 API |
| 线程安全 | master | `init_reflection()` 预初始化 + `call_once` 保护 |
## 联系方式
purecpp@163.com
线上讨论: [项目讨论](https://purecpp.cn/chatroom.html)
[http://purecpp.cn/](http://purecpp.cn/ "purecpp")
[https://github.com/qicosmos/ormpp](https://github.com/qicosmos/ormpp "ormpp")