CppCon 2014 学习:ODB, Advanced Weapons and Tactics
#Schema Evolution 是数据库持久化技术中的一个重要概念,特别是在使用像 ODB 这样的 C++ ORM 框架时。
展示的代码片段正是 ODB 支持的**模式演化(Schema Evolution)**语法示例。
什么是 Schema Evolution?
Schema Evolution 指的是:
当数据库结构(例如类的字段)发生变化时,如何保持与旧版本数据的兼容性并正确迁移已有数据。
提供的代码解释
#pragma db model version(1, 2)
#pragma db object
class user
{std::string first_;std::string last_;
};
表示:在数据库模型版本 1 到 2 之间,类 user
有两个字段:first_
和 last_
。
#pragma db model version(1, 3)
#pragma db object
class user
{std::string name_;
};
表示:从模型版本 3 开始,类 user
结构发生变化:
- 合并了
first_
和last_
→ 变成一个name_
字段。
ODB 如何支持这种演化?
ODB 提供以下功能支持 Schema Evolution:
功能 | 说明 |
---|---|
#pragma db model version(X, Y) | 定义模型适用的版本范围 |
自动 schema 迁移 | ODB 会根据版本差异生成 SQL 迁移脚本 |
--generate-schema 和 --schema-format | 控制生成的迁移 SQL |
支持 升级脚本(SQL ALTER TABLE) | 让数据库数据跟上结构变化 |
需要注意的问题
- 数据迁移逻辑需要开发者介入
例如first_ + " " + last_
→name_
的合并,不是 ODB 自动能做的,需要手动写 SQL 脚本或 C++ 代码协助迁移。 - 版本必须明确声明
否则 ODB 无法知道哪些类在哪些版本中被引入/修改。 - 旧数据要保留向后兼容性?
如果数据不打算迁移,还需考虑是否保留旧字段。
总结一句话
展示的是 ODB 的 schema evolution 技术,通过 #pragma db model version(X, Y)
配合类结构变化,描述数据库模式如何在不同版本间演变。实际使用时,还需要你写一些数据迁移逻辑(如字段合并等)来完成完整升级。
展示的代码片段是 ODB(C++ ORM)中的数据迁移(Data Migration)机制的一个实际用法,它是在数据库模式升级过程中,将旧字段数据迁移到新字段的一种方式。
示例代码解读
schema_catalog::data_migration_function (3,[] (database& db){for (bug& b: db.query<bug> ()){b.name (b.first () + " " + b.last ());db.update (b);}});
背景说明
这个 lambda 函数传给了 data_migration_function()
:
- 第一个参数
3
是目标 schema 版本号。 - 第二个参数是 执行数据迁移的函数。
这表示:在数据库 schema 升级到版本 3 时,要执行这个逻辑来把旧数据结构中的first
和last
字段,合并为新的name
字段。
每一行解析
schema_catalog::data_migration_function (3, // 表示这是升级到第3版时要执行的逻辑
[] (database& db) { // 匿名函数,用于数据迁移,接受 database 实例
for (bug& b: db.query<bug> ()) // 遍历 bug 表中所有记录
b.name (b.first () + " " + b.last ()); // 把 first + last 合并成新的 name 字段
db.update (b); // 更新数据库中的这条记录
}
);
ODB 数据迁移机制总结
步骤 | 说明 |
---|---|
1. 定义模型版本变化 | 使用 #pragma db model version(x, y) |
2. 注册迁移函数 | 使用 schema_catalog::data_migration_function(version, func) |
3. 在函数中处理老数据 | 查询老对象、处理字段、新字段赋值、调用 update() |
4. ODB 自动在升级到指定版本时调用 |
注意事项
- 迁移函数只在 版本升级发生时执行。
- 如果数据库中数据量大,这段迁移代码可能需要优化(批量提交 / 并发)。
- 要在代码中定义好旧字段(
first_
,last_
)和新字段(name_
)的兼容读取方式。
举个现实类比
你可以把这段逻辑理解成 Excel 中的批量操作:
原来每行有「名」和「姓」两列,现在换成「全名」,你写了个脚本,自动合并旧数据,然后更新每行。
展示的是 ODB 中两个高级功能:**版本化命名空间(Versioned Namespace)和软删除(Soft-Delete)**的用法和设计思路,尤其是它们在 schema 演化中的应用。下面详细说明:
1. Versioned Namespace (版本化命名空间)
namespace version2
{#pragma db objectclass user{std::string first_;std::string last_;};
}
- 通过把类放进
version2
命名空间,可以在同一个项目中同时存在不同版本的类定义,比如version1::user
和version2::user
,方便管理不同版本的 schema。 - 这是版本控制的一种策略,允许平行维护多个版本,避免破坏兼容性。
- 对应数据库迁移时,可以映射不同的表结构,或让程序同时支持多版本数据访问。
2. Soft-Delete (软删除)
软删除是数据库设计中一种常见策略:不是真的删除数据,而是逻辑上标记为“删除”状态,保留数据以备恢复或审计。
ODB 中通过 #pragma db deleted(version)
来标记某字段在某版本后不再使用(被“软删除”),但数据依然在数据库里。
你给出的几种软删除写法说明:
写法一:
#pragma db model version(1, 3)
#pragma db object
class user
{#pragma db deleted(3)std::string first_;#pragma db deleted(3)std::string last_;std::string name_;
};
- 表示
first_
和last_
字段从版本 3 开始被标记为删除(不再用),取而代之的是name_
字段。 - 数据库迁移时会把旧字段标记为废弃,但数据依然存留(软删除),程序新版本使用
name_
字段。
写法二(把被删除字段包在子结构里):
#pragma db object
class user
{std::string name_;#pragma db valuestruct deleted_data{#pragma db deleted(3)std::string first_;#pragma db deleted(3)std::string last_;};#pragma db column("")std::unique_ptr<deleted_data> dd_;
};
- 这里把
first_
和last_
封装到一个deleted_data
子结构里,并用unique_ptr
指向。 #pragma db deleted(3)
标记表示这些字段从版本 3 开始被“软删除”。#pragma db column("")
表示dd_
不对应新的表列,而是跟父类的表列共用存储(一般是复合映射)。- 这种设计使得新版本代码更整洁,将旧字段封装,方便管理和维护。
总结
功能 | 作用 | ODB 语法和用法示例 |
---|---|---|
版本化命名空间 | 多版本类管理,保持兼容与清晰分离 | namespace version2 { #pragma db object class user { ... }; } |
软删除 | 字段逻辑废弃但数据保留,防止数据丢失 | #pragma db deleted(3) 标记字段软删除 |
字段封装软删除 | 把废弃字段封装成结构体,保持代码整洁 | struct deleted_data { ... }; std::unique_ptr<deleted_data> dd_; |
ODB 中的 Soft-Add(软添加) 和 Soft-Delete(软删除) 机制,结合你给出的代码,这里详细说明:
1. Soft-Add(软添加)
软添加指的是在新的 schema 版本中向已有对象添加新的字段,这个字段在旧版本里不存在,新增的字段默认需要初始化或赋默认值。
代码示例
class user
{#pragma db added(3)std::string name_;#pragma db valuestruct deleted_data{#pragma db deleted(3)std::string first_;#pragma db deleted(3)std::string last_;};#pragma db column("")std::unique_ptr<deleted_data> dd_;
};
#pragma db added(3)
表示字段name_
是从版本 3 软添加的,即版本 < 3 的数据库没有这个字段。- 结合
deleted_data
结构体软删除旧字段,完成从旧字段到新字段的平滑过渡。
2. 迁移函数补充默认值
因为新添加的字段在老数据中是“空”的(数据库里没数据),所以需要用迁移函数给它赋默认值,否则新版本程序读取会遇到未定义状态。
schema_catalog::data_migration_function (2,[] (database& db){for (bug& b: db.query<bug> ()){b.platform ("Unknown"); // 新增字段 platform 赋默认值db.update(b);}});
- 当数据库升级到版本 2 时,执行这个迁移函数。
- 为所有旧的
bug
记录里的platform
字段赋默认值"Unknown"
,保证程序能正常使用。
3. 软添加与软删除结合使用
- 软删除(
deleted
) 表示字段废弃但数据留存。 - 软添加(
added
) 表示新字段加入。 - 结合使用可以保证版本升级时的平滑转换,旧字段和新字段共存一段时间,数据通过迁移函数同步。
总结
机制 | 作用 | 示例 |
---|---|---|
Soft-Delete | 标记字段废弃,旧数据保留 | #pragma db deleted(3) std::string first_; |
Soft-Add | 新字段添加,老版本无该字段 | #pragma db added(3) std::string name_; |
数据迁移 | 赋默认值,迁移旧数据到新字段 | schema_catalog::data_migration_function(2, ... ) |
简单比喻
就像软件升级,你先保留旧功能(软删除的字段),再加新功能(软添加的字段),升级时自动帮你把旧数据迁移到新字段,同时给新字段赋默认值,避免程序崩溃。
ODB 中如何定义和操作带有 容器成员(std::vector) 的持久化类,以及事务中的加载和更新流程。
1. 容器成员声明示例
#pragma db object
class bug
{// ...#pragma db id autounsigned long long id_; // 主键,自增长status status_; // 其他普通字段std::string summary_;std::string description_;std::vector<std::string> comments_; // 容器成员,持久化保存多条评论
};
comments_
是std::vector<std::string>
类型,ODB 支持自动映射标准容器到数据库表结构。- 通过 ODB,容器中的每个元素都会被存储为独立的行,和所属
bug
对象通过外键关联。
2. 操作示例:事务里添加评论并更新
transaction t (db.begin ());
std::shared_ptr<bug> b (db.load<bug> (id)); // 加载 bug 对象b->add_comment("I also have this problem! Help me!"); // 增加评论
db.update(b); // 更新数据库t.commit (); // 提交事务
- 先开启一个事务,保证数据完整性和一致性。
- 加载指定 id 的
bug
对象。 - 通过自定义的
add_comment
函数向comments_
容器添加新字符串。 - 调用
db.update()
保存改动。 - 提交事务,确保所有更改原子地写入数据库。
3. 关键点
- 容器内元素会被拆分存储,但对开发者来说用法像普通成员变量一样简单。
- 事务机制确保数据的完整性,支持回滚和并发控制。
#pragma db id auto
是主键自增长,加载和更新操作需要依赖它。
4. 小结
特性 | 说明 |
---|---|
容器成员持久化 | 支持 std::vector 及其他 STL 容器 |
事务支持 | 通过 transaction 保证数据一致性 |
加载更新 | 先 load,再修改成员,最后 update 保存 |
ODB 提供的 Change-Tracking Containers,它们是普通容器的“替代品”,专门用来优化持久化对象的变更跟踪。
1. Change-Tracking Containers 概念
- 作用:代替普通 STL 容器(如
std::vector
),自动记录元素的增删改变化,避免每次调用update()
时都重新写入整个容器数据,提高性能。 - 特性:
- Drop-in replacement:接口和用法与标准容器几乎一致,换用时改动极小。
- 维护每个元素的状态(新增、修改、删除)用 2-bit 标记,内存开销极小。
- 仅更新实际发生变动的元素,减少数据库写操作。
2. 常见实现示例
ODB 容器 | 对应标准容器 |
---|---|
odb::vector<T> | 替代 std::vector<T> |
QOdbList<T> | 替代 QList<T> |
3. 优势总结
优点 | 说明 |
---|---|
自动变更追踪 | 只更新修改的元素,数据库操作更高效 |
兼容性好 | API 与标准容器兼容,易于迁移和维护 |
轻量级开销 | 仅 2-bit/元素 的额外开销,几乎无性能负担 |
4. 典型用法示例
#include <odb/vector.hxx>
class bug
{#pragma db id autounsigned long long id_;odb::vector<std::string> comments_; // 变更跟踪容器
};
- 直接用
odb::vector
代替std::vector
,增加变更跟踪。 - 操作和普通
std::vector
类似。
ODB 中的容器(Containers)和对象缓存(Object Cache) 概念,结合你的代码片段,详细说明:
1. Containers — 使用 odb::vector
#pragma db object
class bug
{// ...odb::vector<std::string> comments_; // 变更跟踪的容器,替代 std::vector
};
odb::vector
是 ODB 专门设计的容器,支持变更追踪,比普通的std::vector
更高效。- 适合存储多条评论这类动态增长、修改频繁的容器数据。
2. Object Cache — 双向关联与 weak_ptr 避免循环引用
#pragma db object
class user
{// ...#pragma db inverse(reporter_)std::vector<std::weak_ptr<bug>> reported_bugs_; // 指向报告的 bug 集合
};#pragma db object
class bug
{// ...std::shared_ptr<user> reporter_; // 指向报告者 user
};
user
和bug
之间是双向关联:bug
持有指向user
的shared_ptr
(强引用)。user
持有指向bug
的weak_ptr
(弱引用),用#pragma db inverse(reporter_)
声明它是bug
中reporter_
成员的反向关联。
- 这样设计避免了循环引用导致的内存泄漏。
3. Object Cache — 事务与 Session
transaction t (db.begin ());
session s; // 启用会话缓存
std::shared_ptr<user> u (db.load<user> (email)); // 从数据库加载 user 对象
t.commit ();
transaction
控制数据库操作的原子性。session
是 ODB 的对象缓存机制:- 在 session 生命周期内,同一个对象只加载一次,重复加载返回同一内存地址的实例。
- 减少数据库访问次数,优化性能。
- 一般在事务中创建 session,加载/操作对象后提交事务。
4. 总结
概念 | 作用 |
---|---|
odb::vector | 变更跟踪容器,提升容器元素的持久化效率 |
双向关联 | #pragma db inverse 定义对象间相互引用关系 |
weak_ptr | 防止循环引用,安全管理关联对象生命周期 |
Session 缓存 | 事务内缓存对象实例,减少重复加载数据库 |
ODB 中的 Lazy Pointers(延迟指针),它们用于更细粒度控制关联对象的加载时机。
1. Lazy Pointers 概念
- 作用:普通的智能指针(
shared_ptr
、weak_ptr
)会在对象加载时立即加载关联对象,可能导致性能开销大(比如递归加载大量数据)。 - Lazy Pointers 延迟加载关联对象,只有在明确调用
.load()
时才真正从数据库加载,提高性能和响应速度。 - 每种支持的智能指针类型(
shared_ptr
、weak_ptr
等)都有对应的延迟版本,如odb::lazy_shared_ptr
、odb::lazy_weak_ptr
。
2. 代码示例
#pragma db object
class user
{// ...#pragma db inverse(reporter_)std::vector<odb::lazy_weak_ptr<bug>> reported_bugs_; // 使用延迟弱指针
};
reported_bugs_
不是普通weak_ptr
,而是odb::lazy_weak_ptr
。- 这样
user
对象加载时不会自动加载所有bug
,只有显式访问时才加载。
3. 访问延迟指针示例
odb::lazy_weak_ptr<bug> lb = ...; // 获得延迟指针
std::shared_ptr<bug> b = lb.load(); // 明确调用 load(),加载并锁定 bug 对象
lb.load()
触发数据库加载,将延迟指针升级为普通的shared_ptr
。- 允许按需加载关联对象,避免无谓开销。
4. 总结
特性 | 说明 |
---|---|
延迟加载 | 关联对象只有在显式调用 .load() 时加载 |
性能优化 | 避免一开始加载所有关联对象,减少数据库查询 |
完整智能指针接口 | 与普通指针互换方便,行为类似 |
ODB 的 Object Sections(对象分区),它允许你将对象划分成多个部分,分别控制加载和更新行为,常用于延迟加载和减少数据库访问。
1. Object Sections 基本用法
#pragma db object
class bug
{// ...#pragma db load(lazy) update(change)odb::section details_; // 定义一个分区,名称是 details_#pragma db section(details_)std::string description_;#pragma db section(details_)odb::vector<std::string> comments_;
};
details_
是对象的一个逻辑分区(section)。- 使用
#pragma db section(details_)
标记成员属于该分区。 - 通过分区,可以控制这些成员的延迟加载 (
load(lazy)
) 和按需更新 (update(change)
)。 - 这样对象加载时,
description_
和comments_
不会自动加载,只有调用db.load(obj, obj.details_)
时才加载。
2. 代码示例:按需加载和更新分区
transaction t (db.begin ());
for (bug& b: db.query<bug> (query::status == open))
{if (is_interesting(b)){db.load(b, b.details_); // 仅加载 details_ 分区数据b.comments_.push_back("I am working on a fix."); // 修改 comments_b.details_.change(); // 标记 details_ 分区已变更}db.update(b); // 只更新已变更的分区字段
}
t.commit ();
- 先用
db.load(b, b.details_)
延迟加载details_
分区的字段。 - 修改分区中的字段后调用
b.details_.change()
,告诉 ODB 这个分区已变更。 db.update(b)
只会更新被标记为已变更的分区,避免不必要的写操作。
3. 多分区示例(以 user 类为例)
#pragma db object
class user
{// ...#pragma db load(lazy)odb::section details_; // 延迟加载分区#pragma db section(details_)std::vector<odb::lazy_weak_ptr<bug>> reported_bugs_; // 延迟加载关联对象
};
reported_bugs_
被放在details_
分区,默认延迟加载。- 访问时根据需要显式加载。
4. 主要优势
优点 | 说明 |
---|---|
延迟加载 | 减少初次加载时数据库访问,提升性能 |
分区更新 | 精准控制哪些字段被更新,减少写操作 |
逻辑分离 | 代码上分区,结构更清晰,维护更方便 |
支持复杂数据结构和关联对象 | 分区内可包含容器和延迟关联指针等复杂成员 |
你说的是 ODB 的 Views 概念,主要用于从数据库中加载和处理对象的部分数据,或者关联多个表的数据,甚至执行更复杂的 SQL 查询。下面帮你梳理一下:
1. 基本用法示例
typedef odb::query<bug> query;transaction t(db.begin());
for (const bug& b : db.query<bug>(query::status == open))
{const user& r = b.reporter();std::cout << b.id() << " "<< b.summary() << " "<< r.first() << " "<< r.last() << std::endl;
}
t.commit();
- 这是从
bug
表里查询状态为open
的 bug。 - 通过
b.reporter()
访问关联的user
对象(报错者)。 - 打印 bug 的ID、摘要,以及报错者的名字。
2. Views 的主要功能和特点
功能 | 说明 |
---|---|
选择部分数据成员 | 只加载需要的字段,避免不必要的数据加载 |
多表关联查询 | 通过关联关系 join 多张表,方便一次性查询相关对象数据 |
复杂查询支持 | 支持执行自定义的 SQL 查询,如聚合函数、存储过程调用等 |
只读视图 | 通常用于数据展示,避免对数据库对象的修改 |
3. 为什么用 Views?
- 性能优化:避免加载整个对象,特别是大型对象或包含大字段(比如文本、二进制数据)时。
- 方便聚合和分析:一次查询多个表,减少数据库交互次数。
- 灵活性强:支持复杂 SQL 语句,满足多样化需求。
4. 举个简单的“视图”用法
比如只想查询 bug 的 ID 和摘要,可以定义一个专门的视图类(非持久化对象),用 SQL 自定义查询,然后映射结果。
ODB 中“视图(Views)”的声明与使用,结合了多个数据库对象,并映射到一个结构体上,从而简化查询和结果处理。具体说明如下:
1. 声明 Views
#pragma db view object(bug) object(user)
struct bug_summary
{unsigned long long id;std::string summary;std::string first;std::string last;
};
#pragma db view
声明一个视图结构bug_summary
,它是由bug
和user
两个表的数据组成。- 它包含
bug
的id
和summary
,以及关联的user
的first
和last
名字字段。 - ODB 会根据声明自动生成对应的 SQL 语句来关联这两个对象。
2. 使用 Views
typedef odb::query<bug_summary> query;
for (const bug_summary& b : db.query<bug_summary>(query::bug::status == open))
{std::cout << b.id << " "<< b.summary << " "<< b.first << " "<< b.last << std::endl;
}
- 你通过
db.query<bug_summary>
直接查询这个视图类型。 - 查询条件写的是
query::bug::status == open
,即从bug
表筛选status
为open
的记录。 - 遍历查询结果,访问结构体的成员字段。
3. 生成的 SQL 语句
ODB 编译时会自动生成相应 SQL,如:
SELECT bug.id, bug.summary, user.first, user.last
FROM bug
LEFT JOIN user ON bug.reporter = user.email
WHERE bug.status = ?
- 这里用
LEFT JOIN
连接两个表。 - 用
bug.status = $1
作为过滤条件。
总结
- 视图(View)是面向查询的轻量结构,方便查询多个对象字段。
- 无需加载完整对象,提高效率。
- 自动生成 SQL 连接和过滤,简化代码编写。
ODB 中使用“聚合视图(Aggregate Views)”的用法,能对数据库进行聚合查询(例如计数)并返回结构化结果。
1. 简单聚合视图示例:统计 Bug 数量
#pragma db view object(bug)
struct bug_stats
{#pragma db column("COUNT(" + bug::id_ + ")")std::size_t count;
};
bug_stats
结构体只包含一个字段count
,用来统计bug
表中符合条件的记录数。#pragma db column
里用COUNT(bug::id_)
来生成 SQL 聚合函数。
使用:
typedef odb::query<bug_stats> query;
bug_stats bs = *db.query<bug_stats>(query::status == closed).begin();
std::cout << "Closed bugs count: " << bs.count << std::endl;
- 查询状态为
closed
的 bug 数量。 db.query
返回结果迭代器,使用begin()
取第一个结果。
2. 多表聚合视图示例:按用户统计
#pragma db view object(user) object(bug) \query ((?) + "GROUP BY" + user::email_)
struct user_stats
{std::string first;std::string last;#pragma db column("COUNT(" + bug::id_ + ")")std::size_t count;
};
- 这里视图结合了
user
和bug
两个表。 query ((?) + "GROUP BY" + user::email_)
表示会以用户邮箱分组(GROUP BY
)。- 统计每个用户对应的 bug 数量。
- 返回用户的
first
,last
及其对应的 bug 数量。
使用示例:
for (const user_stats& us : db.query<user_stats>(query::user::last == "Doe" &&query::bug::status == open))
{std::cout << us.first << " " << us.last << ": " << us.count << std::endl;
}
- 查询姓氏是
"Doe"
且 Bug 状态为open
的统计数据。 - 遍历结果打印每位用户的名字和对应的 Bug 数量。
总结
- 聚合视图让复杂的聚合和分组查询变得类型安全且简洁。
- 利用
#pragma db column
自定义 SQL 聚合函数。 - 支持多表联合查询及分组。
- 用法和普通视图类似,返回的是聚合结果而非完整对象。
ODB 中调用**存储过程(Stored Procedure)**并将结果映射到结构体的用法。
1. 存储过程视图定义示例
#pragma db view query("EXEC analyze_bugs (?)")
struct report
{unsigned long long id;std::string result;
};
#pragma db view query("EXEC analyze_bugs (?)")
这里定义了一个视图,视图背后是执行数据库的存储过程analyze_bugs
,它接受一个参数(?
表示参数占位符)。- 结果会被映射到
report
结构体,结构体成员对应存储过程返回的列。
2. 调用存储过程查询示例
typedef odb::query<report> query;
auto results = db.query<report>(query::_val("abc") + "," + query::_val(123));
query::_val(...)
用于给存储过程传递实际参数,参数以逗号分隔组合成调用语句。- 这里调用了
analyze_bugs
存储过程,传入两个参数"abc"
和123
。 - 返回值是
report
对象集合。
3. 总结
- 通过
#pragma db view query("SQL语句")
可以直接调用存储过程或执行任意SQL查询。 - 用
query::_val
提供参数,拼接参数时用字符串拼接符号(+ "," +
)分隔。 - 返回值映射到对应结构体,方便在C++中操作。
ODB 中的 Native Query 使用示例,能让你直接执行原生 SQL 查询,并将结果映射到 C++ 结构体。
1. 定义视图结构体
#pragma db view
struct sequence_value
{unsigned long long value;
};
- 这里定义了一个简单的视图结构体
sequence_value
,用来映射数据库查询结果。 - 结构体成员对应查询结果中的列名(默认匹配字段名或别名)。
2. 执行原生 SQL 查询
sequence_value sv (*db.query<sequence_value>("SELECT nextval('my_sequence')").begin());
- 使用
db.query<sequence_value>(SQL字符串)
直接执行 SQL 查询。 - 该 SQL 是 PostgreSQL 中获取序列下一个值的标准语句。
- 通过解引用
begin()
迭代器获取查询结果的第一个元素。
3. 总结
- Native Query 允许直接写 SQL,绕过 ODB 的自动查询生成,灵活性更高。
- 结果映射到预定义结构体,方便后续操作。
- 适合执行数据库特定的函数或复杂 SQL 语句。
ODB 中乐观并发控制(Optimistic Concurrency) 的多个示例与总结。以下是完整的深入理解:
什么是乐观并发控制?
乐观并发控制是一种假设“冲突不常发生”的并发策略:
- 多个事务可以同时读取数据。
- 当事务提交更新时,系统会检查数据是否在读取之后被别人修改过。
- 如果数据被其他事务更改过,当前事务将失败并抛出异常,从而防止数据被覆盖。
ODB 实现机制:版本控制字段
ODB 使用 #pragma db version
或 #pragma db object version()
指定的字段来进行版本控制。
例如:
#pragma db object
class bug
{#pragma db id autounsigned long long id_;status status_;#pragma db versionunsigned int version_;
};
在每次更新时,ODB 会:
- 检查数据库中的
version_
是否等于内存中对象的版本。 - 如果不一致,抛出
odb::optimistic_concurrency
异常。
示例代码解释
示例 1:事务封装完整更新流程
transaction t (db.begin ());
std::shared_ptr<bug> b (db.load<bug> (id));cout << "current status: " << b->status() << endl<< "enter new status: ";
status s;
cin >> s;b->status(s);db.update(b);
t.commit();
示例 2:延迟修改,分两次事务执行
std::shared_ptr<bug> b;
{transaction t (db.begin ());b = db.load<bug> (id);t.commit ();
}cout << "current status: " << b->status() << endl<< "enter new status: ";
status s;
cin >> s;
b->status(s);{transaction t (db.begin ());db.update(b); // 此处可能触发版本冲突t.commit ();
}
这种写法适合做用户交互型程序,但会更容易触发版本冲突。
总结提示(提供的总结句子)
“Hope for the best, prepare for the worst”
→ 假设不会冲突,但需要做好异常处理。
ODB uses object versioning
→ ODB 利用 version
字段实现乐观锁。
Works best for low to medium contention levels
→ 适用于“写冲突不频繁”的应用场景。
错误处理建议(建议添加)
你可以加上冲突处理逻辑:
try {transaction t (db.begin ());db.update(b);t.commit();
} catch (const odb::object_changed& e) {std::cerr << "The bug was modified by someone else." << std::endl;// 可以重新加载或提示用户冲突
}
ODB 中使用“乐观并发控制”(Optimistic Concurrency)类的完整声明与使用方式。下面是对其的详细理解与解释:
一、什么是 #pragma db object optimistic
?
这个指令用于声明一个对象为启用乐观并发控制的持久化类。
#pragma db object optimistic
class bug
{#pragma db id autounsigned long long id_;#pragma db versionunsigned long long version_; // 用于并发检测status status_;std::string summary_;std::string description_;
};
关键点:
optimistic
标志告诉 ODB 对此类使用版本检测。#pragma db version
声明了用于版本比较的字段,通常是unsigned int
或unsigned long long
。- 每次更新会自动检查该字段是否匹配,匹配才更新。
二、使用方式与异常处理
你展示的使用代码如下:
for (bool done (false); !done;)
{cout << "current status: " << b->status() << endl<< "enter new status: ";cin >> s;b->status(s);transaction t (db.begin());try {db.update(b); // 尝试提交更改done = true; // 成功,退出循环}catch (const odb::object_changed&) {db.reload(b); // 有人已修改此对象 → 重新加载}t.commit();
}
流程解析:
- 显示当前状态,接受用户输入的新状态。
- 启动一个事务,尝试更新数据库。
- 如果没人更改过此记录,更新成功 → 退出循环。
- 如果被别人修改过(版本号不匹配),ODB 抛出
object_changed
异常 → 调用db.reload(b)
重新加载,继续重试。
总结:
项目 | 说明 |
---|---|
#pragma db object optimistic | 声明此类启用乐观并发控制 |
#pragma db version | 指定版本字段,ODB会使用它检测冲突 |
异常类 | odb::object_changed 表示版本冲突 |
典型场景 | 多人可能同时编辑数据,但冲突概率较低 |
优点 | 无需数据库锁,性能高,响应快 |
缺点 | 冲突时需回退或重试,适用于低并发写的系统 |
ODB 对象关系映射中“多态继承(Polymorphism)”的实现机制。下面是详细的解析和理解:
一、类继承结构
class issue
{unsigned long long id_;status status_;std::string summary_;std::string description_;
};class bug: public issue
{std::string platform_;
};class feature: public issue
{unsigned int votes_;
};
这是一个典型的多态类层次结构:
issue
是基类。bug
和feature
是两个派生类。
二、数据库表结构对应(多表继承)
CREATE TABLE issue (id BIGSERIAL NOT NULL PRIMARY KEY,typeid TEXT NOT NULL,status INTEGER NOT NULL,summary TEXT NOT NULL,description TEXT NOT NULL
);CREATE TABLE bug (id BIGINT NOT NULL PRIMARY KEY,platform TEXT NOT NULL,CONSTRAINT id_fk FOREIGN KEY(id) REFERENCES issue(id)
);CREATE TABLE feature (id BIGINT NOT NULL PRIMARY KEY,votes INTEGER NOT NULL,CONSTRAINT id_fk FOREIGN KEY(id) REFERENCES issue(id)
);
理解点:
issue
是主表,存储所有共有字段。bug
和feature
是从表,通过外键引用issue.id
。- 每个对象会存在两张表中(主表 + 子表)。
typeid
是内部使用的字段,由 ODB 自动维护,用来记录具体子类型(如"bug"
、"feature"
)。
三、ODB 如何使用这种结构支持多态性?
1. 映射为多态对象:
你需要使用:
#pragma db object polymorphic
class issue { ... };#pragma db object
class bug: public issue { ... };#pragma db object
class feature: public issue { ... };
2. 查询时使用基类指针:
std::shared_ptr<issue> i = db.load<issue> (id);// 根据 typeid 自行决定 i 实际是 bug 还是 feature
if (std::shared_ptr<bug> b = std::dynamic_pointer_cast<bug>(i))std::cout << b->platform_ << std::endl;
ODB 在内部通过 typeid
字段决定实际对象类型,并完成动态加载。
总结表格:
项目 | 描述 |
---|---|
基类 | issue ,声明为 polymorphic |
派生类 | bug , feature ,继承 issue |
存储结构 | 多表继承:主表存共享字段,子表存特有字段 |
ODB 支持 | 自动使用 typeid 判定真实类型并加载 |
使用方式 | 查询返回 shared_ptr<issue> ,可动态转换成子类 |
ODB 中声明和使用多态(polymorphic)类 的完整流程。下面逐步进行详细解析和“理解”总结:
一、声明多态类(polymorphic
)
基类:issue
#pragma db object polymorphic
class issue
{
public:virtual ~issue () = 0; // 必须是抽象类才能多态#pragma db id autounsigned long long id_;status status_;std::string summary_;std::string description_;
};
说明:
#pragma db object polymorphic
:告诉 ODB 这是一个多态基类。virtual ~issue() = 0;
:C++ 要求多态基类必须有虚析构函数(哪怕纯虚)。- 成员字段映射到
issue
表的公共字段。
派生类:bug
和 feature
#pragma db object
class bug : public issue
{std::string platform_;
};#pragma db object
class feature : public issue
{unsigned int votes_;
};
说明:
- 不需要
polymorphic
,因为继承了issue
。 - 每个派生类都有自己独立的数据库表(以
id
为主键并作为外键关联issue
表)。
二
std::shared_ptr<issue> i(new bug(...));
transaction t(db.begin());
db.persist(i); // 保存 bug
i->status(confirmed);
db.update(i); // 更新 bug
db.reload(i); // 重新加载 bug
t.commit();
说明:
- 即使是通过
issue*
保存的对象,只要是bug
实例,ODB 也能正确存储到两个表中。 - 这是典型的运行时多态行为 + ORM 映射。
三、查询多态对象
typedef odb::query<issue> query;
transaction t(db.begin());std::shared_ptr<issue> i = db.load<issue>(id); // 可加载 bug 或 featurefor (const issue& i: db.query<issue>(query::status == open)) {// i 是 bug 或 feature,实际类型由 ODB 确定
}db.query<issue>(query::status == open); // 查询所有(多态)
db.query<bug>(query::status == open); // 仅查询 bug
db.query<feature>(query::status == open); // 仅查询 featuret.commit();
说明:
- 查询
issue
会返回所有派生类对象(由typeid
判定类型)。 - 可以安全地使用
dynamic_cast
判断对象实际类型。 - 可选择性查询
bug
或feature
子类表。
总结
概念 | 说明 |
---|---|
#pragma db object polymorphic | 声明为多态基类 |
虚析构函数 | 必须有,允许 RTTI 和安全释放 |
存储结构 | 多表继承,主表为基类,子表保存派生特有字段 |
查询方式 | 可统一查 issue ,也可单查 bug / feature |
类型识别 | ODB 自动维护 typeid 字段决定派生类类型 |
ODB 中的批量操作(Bulk Operations)和相关异常处理机制,下面进行逐条解释和总结以帮助你理解。
1. Bulk Operations(批量操作)
基本函数原型:
template <typename I>
void persist(I begin, I end);template <typename I>
void update(I begin, I end);template <typename I>
void erase(I begin, I end);
理解:
这些模板函数允许你对一批对象进行持久化、更新或删除。例如:
std::vector<bug> bugs = {...};
db.persist(bugs.begin(), bugs.end()); // 批量插入
这种方式比一个个插入更高效,尤其是当对象数量较多时。
2. 设置每批最大记录数(bulk()
)
#pragma db object oracle:bulk(5000) mssql:bulk(7000)
class bug
{...
};
理解:
- 对于不同数据库后端(如 Oracle、SQL Server),可以指定一次批量操作最多处理多少条记录。
bulk(n)
表示一次操作 最多插入/更新/删除 n 条记录,超过会自动分批处理。
3. Bulk Exceptions(批量异常)
catch (const multiple_exceptions& mex)
{for (const auto& e: mex){cerr << "exception at " << e.position();try{throw e.exception(); // 抛出对应异常}catch (const odb::...){}catch (const odb::...){}}
}
理解:
- 如果某条记录出错,ODB 抛出
multiple_exceptions
,里面包含每个失败记录的位置和具体异常。 - 使用
e.position()
可以知道哪个对象出错(在容器中的位置)。 e.exception()
是对具体异常对象的包装,可以重新抛出再 catch。
总结表
特性 | 描述 |
---|---|
persist(begin, end) | 批量插入对象 |
update(begin, end) | 批量更新对象 |
erase(begin, end) | 批量删除对象 |
#pragma db ... bulk(n) | 设置最大每批记录数 |
multiple_exceptions | 捕获批量操作中多个异常 |
e.position() | 异常对应的容器索引 |
e.exception() | 捕获并处理具体异常 |
Pimpl Idiom(Pointer to Implementation)在 ODB 中的使用。以下是对其概念和你提供代码的完整解释:
什么是 Pimpl Idiom(实现隐藏技术)
Pimpl(Pointer to Implementation) 是 C++ 中的一种设计模式,用来隐藏类的实现细节,从而降低编译依赖和提高封装性。
结构简述:
class bug
{
public:// 公有接口unsigned long long id() const;void id(unsigned long long);const std::string& summary() const;void summary(std::string);private:class impl; // 前向声明std::unique_ptr<impl> pimpl_; // 指向实际实现的智能指针
};
ODB 与 Pimpl Idiom
在使用 ODB 进行对象-关系映射时,Pimpl 可以用来将数据库映射字段与实际的数据实现解耦。
说明:
#pragma db object
告诉 ODB 这是一个可持久化对象。- 但是类中并没有
id_
或summary_
这样的字段,而是通过 getter/setter 操作pimpl_
中的数据。 - 实际数据保存在
impl
这个私有类里。
为什么在 ODB 中使用 Pimpl
目的 | 说明 |
---|---|
实现隐藏 | 不暴露类的数据成员,保护实现细节 |
降低依赖 | 头文件中不需要包含 <string> 等复杂类型 |
编译隔离 | 修改 impl 不会导致依赖 bug 类的文件重新编译 |
数据映射 | 可以通过 getter/setter 实现和数据库字段的映射 |
注意事项:
在使用 Pimpl Idiom 的类中,ODB 只能访问到通过 getter/setter 提供的字段,因此你必须为需要映射的成员提供合适的访问函数。
如果你使用的是这种方式,还要通过额外的配置告诉 ODB 如何映射 getter/setter 到数据库字段(通常用 #pragma db member(...)
来实现,例子如下)。
进阶使用示例(带映射):
class bug
{
public:unsigned long long id() const;void id(unsigned long long);const std::string& summary() const;void summary(const std::string&);private:class impl;std::unique_ptr<impl> pimpl_;
};// 配合 pragma 指定映射方式
#pragma db object
#pragma db member(id) access(id)
#pragma db member(summary) access(summary)
class bug
{ ... };
总结
项目 | 内容 |
---|---|
技术名 | Pimpl Idiom |
用途 | 隐藏类的实现细节(尤其是数据成员) |
ODB 作用 | 保持接口简洁,分离数据库字段和实现 |
实现方式 | 将实际成员封装在 impl 中,通过 getter/setter 暴露 |
ODB 要求 | 使用 #pragma db member(...) access(...) 明确告诉 ODB 如何访问字段 |
制。
你这段是关于 ODB 中的 Virtual Data Members(虚拟数据成员) 的用法。以下是逐步详细解释,帮助你完全理解其机制与用途。
什么是 Virtual Data Members(虚拟数据成员)
虚拟数据成员 允许你将类的成员函数(getter/setter)映射到数据库字段,而不是直接映射到类中的成员变量。
示例解释:
#pragma db object
class bug
{
public:unsigned long long id () const;void id (unsigned long long);const std::string& summary () const;void summary (std::string);private:class impl;// 将 getter/setter 视为虚拟成员并绑定到数据库字段#pragma db member(id) virtual(unsigned long long)#pragma db member(summary) virtual(std::string)// ODB 不处理这个成员(它只是用来存储实际数据)#pragma db transientstd::unique_ptr<impl> pimpl_;
};
每行含义解析
代码 | 含义 |
---|---|
#pragma db object | 声明该类为可持久化对象 |
#pragma db member(id) virtual(...) | 将 id() / id(...) getter/setter 与数据库字段绑定 |
#pragma db member(summary) virtual(...) | 同样绑定 summary() 与字段 |
#pragma db transient | 告诉 ODB 忽略 pimpl_ ,它不直接映射数据库 |
std::unique_ptr<impl> pimpl_ | 使用 Pimpl Idiom 保存实际数据 |
使用场景
适用情况 | 原因 |
---|---|
使用 Pimpl 模式 | 数据不在主类中而在 impl 中,需要通过函数访问 |
增加封装性 | 不暴露任何成员变量给外部或 ODB |
灵活控制访问 | 可以在 getter/setter 中添加逻辑,比如缓存或验证 |
对比:普通成员 vs 虚拟成员
类型 | 映射方式 | 代码复杂度 | 封装性 |
---|---|---|---|
普通成员 | ODB 直接映射变量 | 简单 | 低 |
虚拟成员 | ODB 通过函数映射变量 | 稍复杂 | 高(适用于大型/模块化项目) |
小提醒:
- 如果你不使用
#pragma db transient
,ODB 会尝试序列化pimpl_
,这通常会失败。 - Getter/Setter 的命名风格必须符合常规(或用显式绑定方式指明)。
总结
项目 | 内容 |
---|---|
功能 | 通过 getter/setter 实现数据库字段映射 |
用途 | 支持实现隐藏(如 Pimpl)和更高封装性 |
关键语法 | #pragma db member(xxx) virtual(type) |
注意 | 搭配 #pragma db transient 使用,避免 impl 被 ODB 管理 |
ODB 的 Accessor/Modifier Expressions(访问器/修改器表达式) 的示例,主要用于将复杂成员类型(如结构体)映射到类的多个 getter/setter 成员函数上。
下面是详细解释:
代码结构回顾
1. 值类型结构体 name
#pragma db value
struct name
{name (std::string, std::string);std::string first, last;
};
#pragma db value
:声明该类型为 值类型(value type),不是数据库对象(object)。first
和last
:表示用户的名字组成部分。
2. 对象类 user
#pragma db object
class user
{const std::string& first () const;const std::string& last () const;void first (std::string);void last (std::string);private:// 映射表达式:#pragma db get(name (this.first (), this.last ())) \set(this.first ((?).first); this.last ((?).last))name name_;
};
Accessor / Modifier Expressions 是什么?
ODB 允许你使用 get(...)
和 set(...)
表达式将对象成员的访问与类的 getter/setter 进行绑定。
解释这两行:
#pragma db get(name (this.first (), this.last ())) \set(this.first ((?).first); this.last ((?).last))
get:
- 表达式:
name(this.first(), this.last())
- 意思是:当 ODB 需要获取
name_
的值时,实际调用的是first()
和last()
组合起来构建一个name
实例。
set:
- 表达式:
this.first((?).first); this.last((?).last)
- 意思是:当 ODB 设置
name_
的值时,它会从name
类型的变量中提取first
和last
,并传给first(...)
和last(...)
函数。
整体作用
你不希望 ODB 直接访问 name_
,而是通过封装的接口(如 first()
和 last()
)进行数据访问与修改。这样你可以:
- 保持封装性
- 在 getter/setter 中添加逻辑(验证、转换、缓存等)
- 灵活映射嵌套对象或结构体
小细节
项 | 内容 |
---|---|
? | 表示传入值(即要设置的整个 name 对象) |
this.first() | 调用当前对象的 getter |
this.first(...) | 调用 setter 设置值 |
使用场景
- 你有嵌套结构体但不希望直接暴露它
- 你有多个字段组成一个逻辑属性(如姓名由 first/last 构成)
- 你需要使用 getter/setter 而不是 public 成员变量
总结
点 | 内容 |
---|---|
功能 | 将结构体类型映射到多个访问器函数 |
优势 | 更好的封装性与灵活性 |
关键语法 | #pragma db get(...) set(...) |
示例核心 | get(name(...)) 和 set(...) 使用访问器而不是变量 |
你给出的这段代码展示了如何在 ODB 中为类定义索引(Index)。下面是逐句的解释,帮助你完全理解:
示例代码解析
#pragma db object
class user
{...#pragma db idstd::string email_; // 主键(唯一标识用户)#pragma db indexstd::string first_; // 默认索引字段(单字段索引)std::string last_; // 普通字段#pragma db index("name_i") \unique // 联合唯一索引(first_ + last_)method("BTREE") // 使用 BTREE 索引方法(适用于 MySQL、PostgreSQL)members(first_, last_) // 索引作用于 first_ 和 last_
};
各部分含义
语法指令 | 说明 |
---|---|
#pragma db object | 将类声明为数据库对象,ODB 会为它生成持久化代码 |
#pragma db id | 声明主键字段(如 email_ ) |
#pragma db index (默认) | 为该字段建立默认索引(这里是 first_ ) |
#pragma db index("name_i") | 定义命名为 "name_i" 的索引 |
unique | 这个索引是唯一索引,不能有重复值 |
method("BTREE") | 使用 BTREE 作为索引方法(数据库相关) |
members(first_, last_) | 定义联合索引,包含 first_ 和 last_ 两个字段 |
数据库层面的结果(以 PostgreSQL 为例)
会自动生成如下 SQL:
CREATE UNIQUE INDEX name_i ON user USING BTREE (first_, last_);
以及:
CREATE INDEX ON user (first_);
使用场景说明
场景 | 示例 |
---|---|
提高查询性能 | SELECT * FROM user WHERE first_ = 'John'; 会用到索引 |
强制业务唯一性 | 联合唯一索引确保 first_ + last_ 组合不重复 |
排序优化 | BTREE 索引在范围查找与排序中效果很好 |
总结
项目 | 说明 |
---|---|
#pragma db index | 建立单字段索引 |
#pragma db index("name_i") + members(...) | 创建联合索引 |
unique | 确保索引字段组合唯一 |
method("BTREE") | 控制底层数据库索引结构 |
ODB 中的 Prepared and Cached Queries(预处理和缓存查询) 示例。下面是详细解释,帮助你理解其工作机制和使用方式。
示例代码回顾
typedef odb::query<bug> query;
typedef odb::prepared_query<bug> prep_query;transaction t (db.begin ());status s;// 查询表达式:动态绑定参数 s
query q (query::status == query::_ref (s));// 创建预处理查询(带名称)
prep_query pq (db.prepare_query<bug>("bug-query", q));// 第一次执行:s == open
s = open;
pq.execute();// 第二次执行:s == confirmed
s = confirmed;
pq.execute();t.commit();
关键概念解释
项目 | 含义 |
---|---|
odb::query<T> | 构造数据库查询的表达式 |
odb::prepared_query<T> | 表示一个预处理后的查询对象,可以重复执行 |
query::_ref(s) | 创建对外部变量 s 的引用,查询中使用这个引用的值 |
db.prepare_query<bug>("bug-query", q) | 将查询 q 作为 "bug-query" 的预处理语句注册到数据库 |
pq.execute() | 执行查询,查询表达式自动读取当前 s 的值 |
工作机制
- 创建一个参数化的查询表达式(
status == ?
)。 - 使用
prepare_query()
将其编译为数据库预处理语句,绑定变量s
。 - 每次调用
pq.execute()
,都会使用当前的s
值重新执行。 - 整个查询是 高效 的,因为语句只编译一次。
优点
优点 | 描述 |
---|---|
更快 | 语句只准备一次,执行多次,提升性能 |
参数安全 | 使用引用绑定方式,可以防止 SQL 注入 |
重复利用 | 一个预处理查询可多次使用,只需改变变量值 |
使用场景
- 查询条件重复但值不同的情况(如分页、状态切换)
- 高性能系统,避免 SQL 重复解析
- 需要缓存查询计划的数据库,如 PostgreSQL、Oracle
小建议
- 事务管理:准备和执行都要在同一个
transaction
中完成。 - 类型一致:
prepared_query<T>
必须与query<T>
匹配。 - 命名缓存:给查询命名可以使其被数据库后端缓存复用。