如何设计一个更好的C++ ORM
2016-12-02 18:55 BOT-Man 阅读(1041) 评论(1) 编辑 收藏 举报2016/11/26
“用C++的方式读写数据库,简直太棒了!”
上一篇相关文章:如何设计一个简单的C++ ORM
(旧版代码)
😉
关于这个设计的代码和样例 😊:
https://github.com/BOT-Man-JL/ORM-Lite
0. 上一版本的问题
上一个版本较为简单,仅仅支持了最基本的
CRUD
,存在以下不足:
- 不支持
Nullable
数据类型; - 自动判断C++对象成员的字段名所用的方法耦合度高;
- 表达式系统不够完善;
- 不支持多表操作和选择部分字段的操作;
利用了课余时间,在最新版本中已经改进了以上内容;😄
如果你只是对模板部分感兴趣,可以直接看 4. 推导查询结果
;
1. Nullable
字段
1.1 为什么要支持 Nullable
字段
- 尽管C++原生数据类型并没有
Nullable
的支持,但是SQL里有null
; - 当两个表合并时,会产生可能为空的字段;
(例如,和一个空表LEFT JOIN
后,每一行都会带有null
字段)
1.2 基本语义和实现
所以,ORMLite中实现了一个类似 C# 的 Nullable<T>
:
- 默认 构造/赋值:对象为空;
- 值 构造/赋值:对象为值;
- 复制/移动:目标和源同值;
- GetValueOrDefault:返回 值 或 默认非空值;
- 比较:两个对象相等,当且仅当
- 两个值都为
空
; - 两个值都
非空
且 具有相同的值
;
- 两个值都为
具体参考:
http://stackoverflow.com/questions/2537942/nullable-values-in-c/28811646#28811646
2. 提取对象成员字段名
这里的提取对象成员字段名,指的是:
如果想表示 UserModel
的 user_id
字段,可以通过
UserModel user
的 user.user_id
推导出来;
(之前的文章讲的不是很明白😂)
UserModel user;
auto field = FieldExtractor { user };
// Get Field of 'user'
auto field_user_id = field (user.user_id);
// Get string of Field Name
auto fieldName_user_id = field_user_id.fieldName;
另外,在跨表查询时,除了字段名,我们还需要保存表名;
2.1 之前的实现
在上一篇文章里,我曾经使用这种方法实现自动判断C++对象的成员字段名:
由于没有想到很好的办法,所以目前使用了指针进行运行时判断:
queryHelper.__Accept (FnVisitor (), [&property, &isFound, &index] (auto &val) { if (!isFound && property == &val) isFound = true; else if (!isFound) index++; }); fieldName = FieldNames[index];
相当于使用
Visitor
遍历这个对象,找到对应成员的序号;
总结起来有两点:
- 保存一个被注入对象的引用
queryHelper
; - 每次遍历这个引用,判断各个字段指针是否相同;
首先,这么做将导致 queryHelper
和 Queryable
对象严重耦合:
- 每一个
Queryable
对象里都需要保存对应的queryHelper
; - 当一个
Queryable
对象可以判断多个不同的表的字段名时,
需要将所有queryHelper
保存为一个tuple
;
(因为不同的queryHelper
是不同的数据类型,不能直接用list
)
另外,这将会导致(巨大的)运行时开销:
每次查询的时间复杂度 O(m, n) = queryHelper个数 * queryHelper内字段数
;
当需要判断字段名的次数很大的时候,这将是很复杂的事情。。。(虽然计算速度很快)
2.2 用 Hash Table 实现 —— FieldExtractor
由于每个被注入对象的字段的地址在判断前已经确定,
所以我们可以构造一个 FieldExtractor
,并把这些地址装入一个
std::unordered_map<const void *, Field>
中,
并不需要保存该对象的引用;
template <typename... Args>
FieldExtractor (const Args & ... args)
{
BOT_ORM_Impl::FnVisitor::Visit ([this] (auto &helper)
{
// Get Info from decltype (helper)
const auto &fieldNames =
std::remove_reference_t<
std::remove_cv_t<decltype (helper)>
>::__FieldNames ();
constexpr auto tableName =
std::remove_reference_t<
std::remove_cv_t<decltype (helper)>
>::__TableName;
// Visit all members of this helper
helper.__Accept (
[this, &fieldNames, &tableName] (auto &val)
{
// Insert to _map
});
}, args...);
}
然后提供一个 Field<T> operator () (const T &field)
接口;
用 field
的地址查表构造 Field<T>
;
并将字段名和所属的表的信息存于 Field<T>
;
template <typename T>
inline Field<T> operator () (const T &field)
{
try
{
// Find the pointer at _map
return _map.at ((const void *) &field);
}
catch (...)
{
throw std::runtime_error ("No Such Field...");
}
}
最后通过重载
NullableField<T> operator () (const Nullable<T> &field)
给 Nullable
类型字段生成对应的 NullableField<T>
;
3. 表达式系统
利用 FieldExtractor
我们就可以很方便的提取出数据库表里的字段了;
提取出的字段,可以通过C++原生的表达式运算,生成对应的SQL表达式;
3.1 基本类型设计
字段和聚合函数:
Selectable
为Field
和Aggregate
的基类;Field
为 普通数据字段,也是NullableField
的基类;NullableField
为 可为空数据字段;Aggregate
为 聚合函数;
表达式:
Expr
为 条件语句;SetExpr
为 赋值语句,仅用于ORMapper.Update
;
3.2 从字段生成表达式
这里,很容易可以想到,我们只需要重载关系运算符就可以了:
template <typename T>
inline Expr operator == (const Selectable<T> &op, T value)
{ return Expr (op, "=", std::move (value)); }
...
- 由于
Field
,NullableField
,Aggregate
都是Selectable
,
我们只需要重载一次就可以应用到它们上边; - 字段和聚合函数 设计为模板
Selectable<T>
,可以实现编译时的强类型检查;
另外,我们可以特殊化部分模板来实现针对特殊字段的运算;
例如:
NullableField
可以和nullptr
比较产生IS NULL
运算;- 字符串类型的
Field
可以使用LIKE
运算符,做正则式匹配;
template <typename T>
inline Expr operator == (const NullableField<T> &op, nullptr_t)
{ return Expr { op, " is null" }; }
inline Expr operator & (const Field<std::string> &field,
std::string val)
{ return Expr (field, " like ", std::move (val)); }
...
4. 推导查询结果
第一个版本中,我们并没有实现多表和 SELECT
的操作;
但是为了实现完整的ORM功能,还是继续把它完善了;
4.1 我们要做什么
使用 queryHelper
产生查询结果
上一版本中,ORMapper.Query
生成的每个 ORQuery
(现在改为Queryable
)
都依赖于一个固定的 C queryHelper
——
在 ToVector
和 ToList
时,都根据这个 queryHelper
生成结果;
template <typename C>
ORQuery<C> Query (const C &queryHelper)
{
return ORQuery<C> (queryHelper, this);
}
修改 queryHelper
类型,实现不同的返回值
当然,如果使用了 SELECT <columns ...>
或 JOIN
之后,
在 ToVector
和 ToList
时,返回的结果就不是原来 Query
时的类型了;
简单想来,可以用下面的方式表示这个结果:
SELECT
产生的每一行就可以使用std::tuple<F1, F2, ...>
表示;JOIN
产生的每一行都是两个类型对象的并;
例如C1 JOIN C2
,将当前的C1
转变为std::tuple<C1, C2>
;- 对于三个表合并的时候,应该变为
std::tuple<C1, C2, C3>
,
而不是简单的std::tuple<std::tuple<C1, C2>, C3>
(这么做有点反人类 😆); UNION
等复合选择 产生的结果和原来相同,并在编译时进行类型检查;
不过,如果把 C1
和 C2
的所有数据成员提取出来,排在一行,
变为 std::tuple<C1F1, C1F2, ..., C2F1, C2F2, ...>
,
就可以实现 SELECT
/ JOIN
/ UNION
的统一结果;
低头不见抬头见的 Nullable
虽然 C1
和 C2
都可能不含有 Nullable
字段,但是它们合并之后的表里,
可能会由于 C1
有这一项,而 C2
没有导致 C2
出现空缺;
C2
中字段原本的数据类型,在这种情况下就不能很好的反映真实的结果;
所以,我们给 std::tuple<C1F1, ..., C2F1, ...>
加一层 Nullable
,
变为 std::tuple<Nullable<C1F1>, ..., Nullable<C2F1>, ...>
,
就可以很好的解决了 NULL
的问题;
更好的改进
另外,如果原本 C*F*
的数据类型就是 Nullable
,
就没有必要加上一层 Nullable
了
(Nullable<Nullable<T>>
没什么实际意义);
这样,得到的最后结果是 std::tuple<Nullable<T>, ...>
(其中 T
为C++的基本数据类型) —— 结果更统一,便于解析😎;
最后,对于没有使用过 Select
/ Join
的 Queryable
在调用
ToVector
和 ToList
时,返回的仍是原始的数据类型;
4.2 如何实现
推导 Select ()
返回的 tuple
Select ()
接受的是 Field
或者 Aggregate
,
所以新的 Queryable
的查询结果类型
可以通过传入 Selectable<T>, ...
用以下的方式推导出:
template <typename T>
inline auto SelectToTuple (const Selectable<T> &)
{
return std::make_tuple (Nullable<T> {});
}
template <typename T, typename... Args>
inline auto SelectToTuple (const Selectable<T> &arg,
const Args & ... args)
{
return std::tuple_cat (SelectToTuple (arg),
SelectToTuple (args...));
}
const Selectable<T> &
获取Selectable
的类型T
std::make_tuple
生成只有一个Nullable<T>
的tuple
;std::tuple_cat
将每个SelectToTuple
产生的tuple
拼接起来;
推导 Join ()
返回的 tuple
Join ()
接受的是 需要合并的表对应的Class,
新的 Queryable
的查询结果类型 可以通过传入
原来的 queryHelper
和 需要合并的表对应的Class 的一个对象
用以下的方式推导出:
template <typename C>
inline auto JoinToTuple (const C &arg)
{
using TupleType = decltype (arg.__Tuple ());
constexpr size_t size = std::tuple_size<TupleType>::value;
return TupleHelper<TupleType, size>::ToNullable (
arg.__Tuple ());
}
template <typename... Args>
inline auto JoinToTuple (const std::tuple<Args...>& t)
{
// TupleHelper::ToNullable is not necessary
return t;
}
template <typename Arg, typename... Args>
inline auto JoinToTuple (const Arg &arg,
const Args & ... args)
{
return std::tuple_cat (JoinToTuple (arg),
JoinToTuple (args...));
}
- 实际转换模板的是 两个
JoinToTuple
,
一个处理带有ORMAP
的对象,另一个处理 原先已经是tuple
的对象; - 所有的处理结果,使用
std::tuple_cat
拼接; - 后者直接返回这个
tuple
对象(这里已经保证了所有元素是Nullable
); - 前者调用
ORMAP
注入的.__Tuple ()
返回一个带有所有成员的tuple
,
然后使用TupleHelper::ToNullable
给tuple
加一层Nullable
;
给 tuple
加一层 Nullable
这里我们需要引入一个帮助模板函数 FieldToNullable
:
template <typename T>
inline auto FieldToNullable (const T &val)
{ return Nullable<T> (val); }
template <typename T>
inline auto FieldToNullable (const Nullable<T> &val)
{ return val; }
- 对于普通的类型
T
,构造并返回一个Nullable<T>
对象; - 通过重载,对于
Nullable
类型,直接返回这个对象;
这相当于每个通过一次 FieldToNullable
的结果,被保证是 Nullable<T>
,
其中 T
为C++的基本数据类型;
然后,利用 TupleHelper::ToNullable
遍历一个 tuple
的所有类型,
使用 FieldToNullable
处理,
将不是 Nullable
的类型元素转变为 Nullable
;
template <typename TupleType, size_t N>
struct TupleHelper
{
static inline auto ToNullable (const TupleType &tuple)
{
return std::tuple_cat (
TupleHelper<TupleType, N - 1>::ToNullable (tuple),
std::make_tuple (
FieldToNullable (std::get<N - 1> (tuple)))
);
}
}
template <typename TupleType>
struct TupleHelper <TupleType, 1>
{
static inline auto ToNullable (const TupleType &tuple)
{
return std::make_tuple (
FieldToNullable (std::get<0> (tuple)));
}
}
- 类似上边,使用
std::make_tuple
生成tuple
,
std::tuple_cat
拼接tuple
; struct TupleHelper <TupleType, N>
相当于是N
个参数时的重载,
struct TupleHelper <TupleType, 1>
针对于1
个参数特殊化;
保证 复合操作 的类型安全
我们只需要设计接口时,仅接受相同返回类型的 Queryable
就可以了:
Queryable Union (const Queryable &queryable) const;
...
5. 仍未完善的地方
由于时间限制,部分比较复杂的功能,在这个版本中并没有实现:
- 支持 二进制和日期/时间 类型数据;
- 支持 SubQuery;
- 支持 建表时加入字段限制;
欢迎一起 探讨 改进 😁;
6. 写在最后
由于我的知识有限,实现上可能有不足,欢迎指正;
Modularity is something every software designer does in their sleep.
—— Scott Shenker
如果对以上内容及ORM Lite有什么问题,
欢迎 指点 讨论 😉:
https://github.com/BOT-Man-JL/ORM-Lite/issues
Delivered under MIT License © 2016, BOT Man