您的位置:首页 > 编程语言 > C语言/C++

如何设计一个更好的C++ ORM

2016-12-02 18:55 459 查看

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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: