代码改变世界

如何设计一个更好的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
,存在以下不足:

  1. 不支持 Nullable 数据类型;
  2. 自动判断C++对象成员的字段名所用的方法耦合度高;
  3. 表达式系统不够完善;
  4. 不支持多表操作和选择部分字段的操作;

利用了课余时间,在最新版本中已经改进了以上内容;😄

如果你只是对模板部分感兴趣,可以直接看 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. 提取对象成员字段名

这里的提取对象成员字段名,指的是:
如果想表示 UserModeluser_id 字段,可以通过
UserModel useruser.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
  • 每次遍历这个引用,判断各个字段指针是否相同;

首先,这么做将导致 queryHelperQueryable 对象严重耦合:

  • 每一个 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 基本类型设计

字段和聚合函数:

  • SelectableFieldAggregate 的基类;
  • 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)); }
...
  • 由于 FieldNullableFieldAggregate 都是 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——
ToVectorToList 时,都根据这个 queryHelper 生成结果;

template <typename C>
ORQuery<C> Query (const C &queryHelper)
{
    return ORQuery<C> (queryHelper, this);
}

修改 queryHelper 类型,实现不同的返回值

当然,如果使用了 SELECT <columns ...>JOIN 之后,
ToVectorToList 时,返回的结果就不是原来 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 等复合选择 产生的结果和原来相同,并在编译时进行类型检查;

不过,如果把 C1C2 的所有数据成员提取出来,排在一
变为 std::tuple<C1F1, C1F2, ..., C2F1, C2F2, ...>
就可以实现 SELECT / JOIN / UNION 的统一结果;

低头不见抬头见的 Nullable

虽然 C1C2 都可能不含有 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 / JoinQueryable 在调用
ToVectorToList 时,返回的仍是原始的数据类型;

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::ToNullabletuple 加一层 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