C++ 对象关系映射 (ORM)- Eating the Bun - 第 1 部分






4.15/5 (20投票s)
在 SQL 数据库之上为 C++ 创建一个简单的 ORM
引言
对象关系映射 (Object Relational Mapping) 是将面向对象的语言(如 C++)中的数据类型映射到关系型系统(如 SQL)中的过程。那么挑战在哪里呢?C++ 有不同类型的基本类型,例如 int
、char
、float
、double
以及它们的变体。因此,将所有这些映射到实际的 SQL 类型是一项真正的挑战。可能存在也可能不存在与 C++ 类型相似的精确类型。例如,对于 float
,C++ 和 SQL 可能支持不同类型的标准。因此,有不同的工具来完成这项工作。市场上也有许多成熟的库。 ODB 就是其中一个非常不错的库。
为了帮助我日常工作,我创建了一个简单的 C++ 库,名为 Bun。
有什么新功能?
- * Bun 1.5.0 将带 vector 的对象转换为 JSON 和 Msgpack,并从包含 vector 的 JSON 创建对象。注意:它尚不包含 vector 的持久化。仍在开发中。
- Bun 1.4.0 支持将对象转换为 JSON 并从 JSON 创建对象。它能够将 对象转换为 Message pack 并从 message pack 构建对象。
- Bun 1.3 支持对象懒惰迭代和基于范围的 for 循环支持。键值存储也支持同样的功能。
- Bun 1.2 支持嵌入式键值存储。但默认情况下,键值存储基于 Unqlite。
特点
- 易于使用
- 使用纯 C++ 对象 (
POCO
) - 对象持久化 - 您可以直接持久化
C++ 对象
- 非侵入性 - 您无需修改类即可使其持久化
- 在纯 C++ 中指定约束
- 持久化嵌套对象
EDSL 对象查询语言
(无需 SQL 查询)编译时 EDSL
语法检查以确保类型安全 - 在执行开始前捕获错误- 多数据库支持 - SQLite、Postgres、MySQL
- 易于使用的嵌入式键值存储
- 将 C++ 对象转换为
JSON
并从JSON
创建 C++ 对象。 - 将 C++ 对象转换为
Message Pack
并从Message Pack
创建 C++ 对象。 STL
友好。这些是常规的 C++ 对象。因此可以用于 C++ STL 算法。
谁在使用 Bun?
本节介绍谁在使用 Bun 以及在什么上下文中。如果您发现 Bun 有用并正在使用它,请告诉我,我会将其添加到此处。
背景
在我的许多工具应用程序中,我使用 SQLite 作为主要数据库。每次使用 SQL 查询时,我都觉得在与实际用例无关的任务上浪费了很多精力。因此,我想创建一个用于这些类型自动映射的框架。该库的标准如下:
- 可免费用于任何类型的项目 (BSD 许可证)
- 易于使用 (无需 SQL 查询知识)
- 提供字段的唯一键约束等约束
- 无需 SQL 查询。EDSL 查询。
- 非侵入性
- 表达性强
- 应该是 C++ 的 DSL,因此查询语法可以由 C++ 编译器检查
- 无需自定义编译器 (C++11 及以上)
- 高性能
- 支持多种数据库后端,如 SQLite、Postgres、MySQL
- 易于使用的嵌入式键值存储
所有这些目前尚未实现。最终,我将解决所有这些问题。目前,只开发了库的基本版本。
Using the Code
Bun 对象存储接口
在深入了解其内部细节之前,在这第一篇文章中,让我们看看如何使用该库。
Bun 拥有 BSD 3-Clause 许可证。它依赖于以下开源和免费库:
- boost (我已在 1.61 版本上测试过,Boost 许可证)
- fmt (小巧、安全、快速的格式化库,BSD 许可证)
- spdlog (快速 C++ 日志记录,MIT 许可证)
- SQLite (独立、无服务器、零配置、事务性 SQL 数据库引擎,公共领域)
- SOCI (C++ 数据库层,BSL 许可证)
- JSON for modern C++ (C++ JSON 和 Message pack 工具,MIT 许可证)
- Rapid JSON (快速 C++ JSON 库,参见许可证)
GitHub 页面包含所有必需的依赖项。它还包含一个 Visual Studio 2015 解决方案文件,以便于使用。不包含 Boost 和 SOCI。要下载项目,请将 boost 头文件放在“include”目录下或更改解决方案文件中的路径。 构建 SOCI (使用 cmake 非常容易构建) 并将库链接到 Bun。
#include "blib/bun/bun.hpp"
namespace test {
// Class that needs to be persisted
struct Person {
std::string name;
std::string uname;
int age;
float height;
};
}
/// @class Child
struct Child {
int cf1;
Child(const int cf = -1) : cf1(cf) {}
Child& operator=(const int i) {
cf1 = i;
return *this;
}
};
/// @class Paret
struct Parent {
int f1;
std::string f2;
// Nested object
Child f3;
Parent() :f1(-1), f2("-1"), f3(-1) {}
};
// Both should be persistable
SPECIALIZE_BUN_HELPER((Child, cf1));
SPECIALIZE_BUN_HELPER((Parent, f1, f2, f3));
/////////////////////////////////////////////////
/// Generate the database bindings at compile time.
/////////////////////////////////////////////////
SPECIALIZE_BUN_HELPER( (test::Person, name, uname, age, height) );
int main() {
namespace bun = blib::bun;
namespace query = blib::bun::query;
// Connect the db. If the db is not there it will be created.
// It should include the whole path
// For SQLite
//bun::connect( "objects.db" );
// For PostGres
bun::connect("postgresql:///postgres?user=postgres&password=postgres");
// Get the fields of the Person. This will be useful in specifying constraints and also
// querying the object.
using PersonFields = query::F<test::Person>;
// Generate the configuration. By default it does nothing.
blib::bun::Configuration<test::Person> person_config;
// This is a unique key constraints that is applied.
// Constraint are applied globally. They need to be set before the
// execution of the create schema statement
// The syntax is Field name = Constraint
// We can club multiple Constraints as below in the same statement.
// There is no need for multiple set's to be called. This is how
// We can chain different constraints in the same statement
person_config.set(PersonFields::name = blib::bun::unique_constraint)
(PersonFields::uname = blib::bun::unique_constraint);
// Create the schema. We can create the schema multiple times. If its already created
// it will be safely ignored. The constraints are applied to the table.
// Adding constraints don't have effect if the table is already created
bun::createSchema<test::Person>();
// Start transaction
bun::Transaction t;
// Create some entries in the database
for (int i = 1; i < 1000; ++i) {
// PRef is a reference to the persistent object.
// PRef keeps the ownership of the memory. Release the memory when it is destroyed.
// Internally it holds the object in a unique_ptr
// PRef also has a oid associated with the object
bun::PRef<test::Person> p = new test::Person;
// Assign the members values
p->age = i + 10;
p->height = 5.6;
p->name = fmt::format( "Brainless_{}", i );
// Persist the object and get a oid for the persisted object.
const bun::SimpleOID oid = p.persist();
//Getting the object from db using oid.
bun::PRef<test::Person> p1( oid );
}
// Commit the transaction
t.commit();
// To get all the object oids of a particular object.
// person_oids is a vector of type std::vector<blib::bun<>SimpleOID<test::Person>>
const auto person_oids = bun::getAllOids<test::Person>();
// To get the objects of a particular type
// std::vector<blib::bun::Pref<test::Person>>
const auto person_objs = bun::getAllObjects<test::Person>();
// EDSL QUERY LANGUAGE ----------------------
// Powerful EDSL object query syntax that is checked for syntax at compile time.
// The compilation fails at the compile time with a message "Syntax error in Bun Query"
using FromPerson = query::From<test::Person>;
FromPerson fromPerson;
// Grammar are checked for validity of syntax at compile time itself.
// Currently only &&, ||, <, <=, >, >=, ==, != are supported. They have their respective meaning
// Below is a valid query grammar
auto valid_query = PersonFields::age > 10 && PersonFields::name != "Brainless_0";
std::cout << "Valid Grammar?: " << query::IsValidQuery<decltype(valid_query)>::value << std::endl;
// Oops + is not a valid grammar
auto invalid_query = PersonFields::age + 10 &&
PersonFields::name != "Brainless_0";
std::cout << "Valid Grammar?: " <<
query::IsValidQuery<decltype(invalid_query)>::value << std::endl;
// Now let us execute the query.
// The where function also checks for the validity of the query, and fails at compile time
const auto objs = fromPerson.where( valid_query ).where( valid_query ).objects();
// Can even use following way of query
// As you see we can join queries
const auto q = PersonFields::age > 21 && PersonFields::name == "test";
const auto objs_again = FromPerson().where( q ).objects();
const auto objs_again_q = FromPerson().where( PersonFields::age > 21
&& PersonFields::name == "test" ).objects()
// Not going to compile if you enable the below line.
// Will get the "Syntax error in Bun Query" compile time message.
// const auto objs1 = FromPerson.where( invalid_query ).objects();
// Check the query generated. It does not give the sql query.
std::cout << fromPerson.query() << std::endl;
// Support for Nested object persistence and retrieval
blib::bun::createSchema<Child>();
blib::bun::createSchema<Parent>();
std::cout << "How many objects to insert? " << std::endl;
int count = 0;
std::cin >> count;
for (int i = 0; i < count; ++i) {
blib::bun::l().info("===============Start===================");
blib::bun::PRef<Parent> p = new Parent;
p->f1 = i;
p->f2 = i % 2 ? "Delete Me" : "Do not Delete Me";
p->f3 = 10 * i;
// Persists the Parent and the Nested Child
p.persist();
std::cout << "Added to db: \n" << p.toJson() << std::endl;
blib::bun::l().info("===============End===================\n");
}
std::cout << "Get all objects and show" << std::endl;
auto parents = blib::bun::getAllObjects<Parent>();
// Iterate and delete the Parent and the nested Child
// Here p is a PRef type. We can modify the object and persist
// the changes if needed.
for (auto p : parents) {
std::cout << p.toJson() << std::endl;
p.del();
}
return 0;
}
这就是我们如何持久化对象。运行此代码后,SQLite 数据库中会创建以下列表:
现在让我们仔细看看其中的一些元素。架构的 DDL 如下:
CREATE TABLE "test::Person" (object_id INTEGER NOT NULL, name TEXT, age INTEGER, height REAL);
此架构由库内部创建。我仅在此显示以供参考。
数据如下:
持久化存储
oid | 名称 | age | height |
90023498019372 | Brainless_1 | 11 | 5.6 |
90023527619226 | Brainless_2 | 12 | 5.6 |
90023537497149 | Brainless_3 | 13 | 5.6 |
90023553459526 | Brainless_4 | 14 | 5.6 |
90023562946990 | Brainless_5 | 15 | 5.6 |
基于范围的迭代
Bun 还支持使用 C++ 中的 range based for 循环来迭代对象。以下给出了一个简单的示例,说明这将如何工作。
// Iterate the parent with range based for loop using FromParents = query::From<Parent>; using ParentFields = query::F<Parent>; FromParents from_parents; // Select the query which you want to execute auto parents_where = from_parents.where(ParentFields::f2 == "Delete Me"); // Fetch all the objects satisfying the query. This is a lazy fetch. It will be fetched // only when it is called. And not all the objects are fetched. // Here v is a PRef so it can be used to modify and persist the object. for(auto v : parents_where) { std::cout << v.toJson() << std::endl; }
JSON 和 Message pack 转换 (到对象和从对象)
现在我们可以将 C++ 对象转换为 JSON 并从 JSON 创建 C++ 对象。我们甚至可以将 C++ 对象转换为 Message Pack 并从 message pack 创建 C++ 对象。
这非常简单,只需专门化 bun 辅助函数,然后就如同玩耍一样简单。
namespace dbg {
struct C1 {
int c1;
C1() :c1(2) {}
};
struct C {
int c;
C1 c1;
C(const int i = 1) :c(i) {}
};
struct P {
std::string p;
C c;
P() :p("s1"), c(1) {}
};
}
SPECIALIZE_BUN_HELPER((dbg::C1, c1));
SPECIALIZE_BUN_HELPER((dbg::C, c, c1));
SPECIALIZE_BUN_HELPER((dbg::P, p, c));
int jsonTest() {
namespace bun = blib::bun;
blib::bun::PRef<dbg::P> p = new dbg::P;
p->p = "s11";
p->c.c = 10;
p->c.c1.c1 = 12;
blib::bun::PRef<dbg::C> c = new dbg::C;
c->c = 666;
// Convert the object to JSON
const std::string json_string = p.toJson();
// Construct the new object out of JSON
blib::bun::PRef<dbg::P> p1;
p1.fromJson(json_string);
const auto msgpack = p1.toMesssagepack();
// Construct another object out of messagepack
blib::bun::PRef<dbg::P> p2;
p2.fromMessagepack(p1.toMesssagepack());
// messagepack to string
std::string msgpack_string;
for (auto c : msgpack) {
msgpack_string.push_back(c);
}
std::cout << "1. Original object Object:" << json_string << std::endl;
std::cout << "2. Object from JSON :" << p1.toJson() << std::endl;
std::cout << "3. Object to Messagepack :" << msgpack_string << std::endl;
std::cout << "4. Object from Messagepck:" << p2.toJson() << std::endl;
std::cout << "=== Vector JSON Conversion ===" << std::endl;
blib::bun::PRef<bakery::B> b = new bakery::B;
b->j = "test";
b->i.push_back(12);
b->i.push_back(23);
std::cout << "5. Object with Vector: " << b.toJson() << std::endl;
blib::bun::PRef<bakery::B> b1 = new bakery::B;
b1.fromJson(b.toJson());
std::cout << "6. Object copy with Vector: " << b1.toJson();
return 1;
}
键值存储
Bun 拥有一个嵌入式键值存储。默认实现基于 Unqlite。
/// @class KVDb
/// @brief The main class for the key value store
template<typename T = DBKVStoreUnqlite>
class KVDb {
public:
/// @fn KVDb
/// @param param
/// @brief The constructor for the KV class
KVDb(std::string const& param);
/// @fn KVDb
/// @param other. The other KVDb from which we can copy values.
/// @brief The copy constructor for the KV class
KVDb(KVDb const& other);
/// @fn ~KVDb
/// @brief destructor for the KV class
~KVDb();
/// @fn ok
/// @brief Returns Ok
bool ok() const;
std::string last_status() const;
/// @fn put
/// @param key The key
/// @param value the value that needs to be stored
/// @details Put stores the key and value and returns true of the store is done,
/// else it returns false
/// All primary C++ data types including std::string is supported as key and value
template<typename Key, typename Value>
bool put(Key const& key, Value const& value);
/// @fn get
/// @param key The key
/// @param value the value is of type ByteVctorType. This carries the out value
/// @details Gets the value corresponding the key.
/// If the retrieval it returns true else it returns false.
/// All primary C++ data types including std::string is supported as key.
/// The value is a byte (std::uint8_t) value
template<typename Key>
bool get(Key const& key, ByteVctorType& value);
/// @fn get
/// @param key The key
/// @param value the value is of type ByteVctorType. This carries the out value
/// @details Gets the value corresponding the key. If the retrieval it returns true
/// else it returns false.
/// All primary C++ data types including std::string is supported as key.
/// The value C++ primary datatype.
/// This function is a wrapper on top of the previous function
/// which returns the byte vector.
template<typename Key, typename Value>
bool get(Key const& key, Value& value);
/// @fn del
/// @param key The key
/// @details Delete the value corresponding to key.
/// If delete is success then returns true else returns false.
/// All primary C++ data types including std::string is supported as key.
template<typename Key>
bool del(Key const& key);
};
以下是我们可以使用它的方式:
/// @fn kvTest
/// @brief A test program for
int kvTest() {
/// @var db
/// @brief Create the database. If the database already exists
/// it opens the database but creates if it doesnt exist
blib::bun::KVDb<> db("kv.db");
/// @brief put a value in database.
db.put("test", "test");
std::string val;
/// @brief get the value. We need to pass a variable by reference to get the value.
db.get("test", val);
std::cout << val << std::endl;
const int size = 10000;
for (int i = 0; i < size; ++i) {
const std::string s = fmt::format("Value: {}", i);
db.put(i, s);
}
for (int i = 0; i < size; ++i) {
std::string val;
db.get(i, val);
std::cout << val << std::endl;
}
return 1;
}
键值的基于范围的迭代
Bun 支持对 kv 存储中元素的键值进行基于范围的迭代。这种迭代类似于 map 的迭代。键和值都作为 pair 返回。如果您在下面看到 kv
是一个 pair,kv.first
包含键值,kv.second
包含值。kv.first
和 kv.second
的值是字节向量。
// ========= KV Store blib::bun::KVDb<> db("kv.db"); const int size = 3; for (int i = 0; i < size; ++i) { const std::string s = fmt::format("storing number: {}", i); db.put(i, s); } std::cout << "Start iteration Via size "<< std::endl; for (int i = 0; i < size; ++i) { std::string val; db.get(i, val); std::cout << val << std::endl; } std::cout << "Start iteration via foreach "<< std::endl; count = 0; // Iterate the key value store using foreach. // We have both the key and value here. So we can change the value at the key for (auto kv : db) { int key = 0; blib::bun::from_byte_vec(kv.first, key); std::string value; blib::bun::from_byte_vec(kv.second, value); std::cout << count++ << ")> key: "<< key << "\n Value: " << value << std::endl; }
内部
ORM 的一些内部机制如下:
Reflection(反射)
Bun 内部使用简单的反射来生成并处理编译时类型信息。有一个计划将其扩展一点,使其更有用。
SPECIALIZE_BUN_HELPER
此宏将在编译时生成对象的所有绑定。所有模板特化都使用此宏创建。在多个头文件或 CPP 文件中使用此宏应该是安全的。
应将以下内容传递给宏:
(<类名,应包含命名空间详细信息>,要持久化的成员...)
成员列表也可以是部分类成员。例如,如果我们有一个对象句柄,就没有必要将其存储在数据库中。在这种情况下,我们可以省略句柄,持久化所有其他特性。这样,只有给定的字段会被填充。
约束
在 Bun 中应用约束非常简单。以下示例进行了说明。
// Get the fields of the Person. This will be useful in specifying constraints and also
// querying the object.
using PersonFields = query::F<test::Person>;
// Generate the configuration. By default it does nothing.
blib::bun::Configuration<test::Person> person_config;
// This is a unique key constrains thats applied.
// Constraint are applied globally. They need to be set before the
// execution of the create schema statement
// The syntax is Field name = Constraint
// Here is how we can chain the different constraints in a single set statement
person_config.set(PersonFields::name = blib::bun::unique_constraint)
(PersonFields::uname = blib::bun::unique_constraint);
正如您所见,创建唯一约束非常简单。如上所述,我们可以使用重载的 () 运算符组合多个约束,而不是多次调用 set。
需要记住的事情
- 目前,约束只能在表创建之前应用。创建表后,这些语句无效。
- 仅支持唯一键。
在后续版本中,我将消除这些限制。
PRef
PRef
是库的核心元素之一。它保存需要持久化的对象。它还包含对象的 oid
,这与实际对象无关。使对象持久化的一些规则:
- 需要持久化的成员必须是
public
。 PRef
维护对象的拥有权,并在对象离开作用域时删除该对象。- 如果我们为另一个
PRef
分配一个PRef
,则前者将失去对象的拥有权。就像unique_ptr
一样。实际上,PRef
在内部将对象存储在unique_ptr
中。 - 在持久化对象之前,我们必须创建架构 (使用
blib::bun::createSchema<>()
) 并生成绑定 (using SPECIALIZE_BUN_HELPER( (test::Person, name, age, height) );
) - 它还包含特定实例的对象的 md5 校验和。因此,如果对象没有变化,它就不会持久化它。在我自己的用法中,我保留了更新的时间戳。我不想每次都更新对象。对于这个公开版本,我省略了时间戳。
插入或更新
库如何知道我们要插入还是更新数据库?这取决于对象的 md5。如果 md5 有值,那么它是一个 update
,否则它是一个 insert
。以下查询会自动为 insert
生成:
INSERT INTO 'test::Person' (object_id,name,age,height) VALUES(91340162041484,'Brainless_4',14,5.6)
搜索
在 Bun 中搜索非常简单。有不同的搜索机制。
- Oid 搜索:我们可以使用以下方法获取所有
Oids
:// The return type is std::vector<blib::bun<SimpleOID<test::Person>> const auto person_oids = blib::bun::getAllOids<test::Person>();
- 搜索特定类型的对象:我们可以将数据库中的所有对象作为对象向量获取:
// std::vector<blib::bun::Pref<test::Person>> const auto person_objs = blib::bun::getAllObjects<test::Person>();
- 对象 EDSL:我们可以通过 Bun 提供的 EDSL 查询进行搜索。EDSL 是使用 boost proto 库实现的。查询在编译时由 C++ 编译器检查。调用
SPECIALIZE_BUN_HELPER
时,它会创建一些特殊变量。例如:对于
Person
类,SPECIALIZE_BUN_HELPER
会生成以下内容:bun::query::F<test::Person>::name bun::query::F<test::Person>::age bun::query::F<test::Person>::heigh
Bun 的 bun::query::F
类将使用 Person
类的所有字段进行特化。
要应用任何类型的过滤器,您只需使用“where
”函数,例如:
// The where(Query) is a lazy function, it does not query the db.
// The actual execution is done in the object() function
const auto objs_again = bun::query::From<test::Person>().where( valid_query ).objects();
// We can also join queries or filters using && or the || operator
const auto objs_again = bun::query::From<test::Person>().where( valid_query && valid_query ).objects();
讨论论坛
- Gitter.im:您可以在这里快速提问,或者在我们有空时与我们聊天。
- GitHub issues:在此处创建问题。
历史
- Alpha 1 (2016年5月16日)
- 库的初始版本
- Alpha 2 (2016年7月2日)
- 实现 Bun EDSL
- Alpha 3 (2018年3月14日)
- 集成了 SOCI 作为数据库交互层。这使得该库可以使用任何 SQL 数据库,如 SQLite、Postgres、MySQL。它主要支持 SOCI 支持的其他数据库,但尚未进行测试。
- 使用 Boost Fusion。代码更清晰,预处理器宏更少。代码更易于调试。
- 支持使用
Transaction
类进行事务处理 - 更好的错误处理和错误日志记录
- 添加了大量注释以帮助用户
- Alpha 4 (2018年3月5日)
- 支持嵌套对象
SimpleOID
现在使用 boost UUID 生成唯一标识符- 其他注释
- 小的性能提升
- Alpha 5 (2018年5月19日)
- 支持表创建前的约束
- Alpha 6 (2018年7月18日)
- 为 bun 添加键值功能
- Alpha 7 (2018年8月11日)
- 为对象迭代添加了基于范围的 for 循环支持。
- 为键值存储迭代添加了基于范围的 for 循环支持。
- 这两种迭代都是惰性迭代。
- Alpha 8 (2018年10月19日)
- 添加了从 JSON 字符串创建 C++ 对象的支持
- 添加了从 C++ 对象创建 Message Pack 的支持
- 添加了从 Message Pack 创建 C++ 对象的支持
- Alpha 9 (2018年1月13日)
- 添加了支持将包含 vector 的 C++ 对象转换为 JSON 和 Msgpack
- 添加了支持将包含 vector 的 JSON 或 Msgpack 转换为 C++ 对象。
后续功能
- 添加 C++ vector 持久化
基于迭代器的惰性数据拉取- 自定义
Oid
类支持 - 支持 ElasticSearch
- 改进错误处理
- EDSL 查询语言增强
- 表创建后的约束修改
- 支持其他约束
- 索引支持
- 支持对象处理的前置和后置钩子
- 持久化
std::vector
成员 - 单元测试实现
- 支持 Leveldb
键值迭代器支持复合类型(完成)。
需要帮助
大家好,
考虑到让该库更加丰富的所需工作,我将需要任何必要的帮助。在以下领域需要帮助:
- 增强功能
- 修复 bug。
- 重构和清理代码。
- 增强文档。
- 建设性批评和功能建议。
- 编写测试。
- 使用 Bun
任何大小的贡献都将受到赞赏。