如何设计一个更好的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
相关文章推荐
- 如何设计一个简单的C++ ORM
- 如何设计一个简单的C++ ORM
- 如何在一个系统中设计权限控制机制(1)
- 设计一个图书借阅管理系统需要如何分析
- 如何独立设计完成一个软件项目
- 如何设计一个大型的AJAX应用程序
- BBS 设计思路系列 ---- 普通网友如何投诉一个帖子??
- Web Services的设计越来越多,如何用它来分析和组织更大粒度的信息系统,比如一个地区?
- 如何设计一个大型的AJAX应用程序
- [网站设计]如何设计一个成功的网站
- Nielsen:如何设计出更好的网站
- 如何设计一个大型的AJAX应用程序
- 如何在一个系统中设计权限控制机制(2)
- 如何添加一个自定义的columnstyles 到设计器中,以便在设计时直接使用他们?
- 正在学设计模式呵,转载一篇如何保证一个窗体的实例运行
- 如何构建一个ERP系统(需求分析、系统架构、系统设计、系统编码、测试、交付程序及文文件)。
- 一个Query Builder,征集更好用的Query Builder的设计方案
- 如何建立一个网站?规划、设计、目的、原则、宣传(三)
- 如何建立一个网站?规划、设计、目的、原则、宣传(转)
- 如何设计一个公共的数据字典维护模块