2016/11/15
“沒有好的接口,用C++讀寫數據庫和寫圖形界面一樣痛苦”
閱讀這篇文章前,你最好知道什么是
Object Relation Mapping (ORM)
閱讀這篇文章后,歡迎閱讀下一篇 如何設計一個更好的C++ ORM
😉
為什么C++要ORM
As good object-oriented developers got tired of this repetitive work,
their typical tendency towards enlightened laziness started
to manifest itself in the creation of tools to help automate
the process.When working with relational databases,
the culmination of such efforts were object/relational mapping tools.
- 一般的C++數據庫接口,都需要手動生成SQL語句;
- 手動生成的查詢字符串,常常會因為模型改動而失效;
- 查詢語句/結果和C++原生數據之間的轉換,每次都要手動解析;
我為什么要寫ORM
C++大作業需要實現一個在線的對戰游戲,其中的游戲信息需要保存到數據庫里;
而我最初始的里沒有使用 ORM 導致生成 SQL 語句的代碼占了好大一個部分;
並且這一大堆代碼里的小錯誤往往很難被發現;
每次修改游戲里怪物的模型都需要同步修改這些代碼;
然而在修改的過程中經常因為疏漏而出現小錯誤;
所以,我打算讓代碼生成這段代碼; 😇
市場上的C++ ORM
大致可以分成這幾類:
- 使用 預編譯器 生成模型和操作:
- 使用 宏 生成模型和操作:
- 需要在定義模型時,通過手動插入 代碼 進行注入:
- Hiberlite ORM
- Open Object Store
- Wt::Dbo
- QxORM(Qt風格的龐大。。)
以上的方案都使用了
Proxy Pattern
和
Adapter Pattern
實現 ORM 功能,並提供一個用於和數據庫交換數據的 容器;
所以我打算封裝一個直接操作 原始模型 的 ORM; 😎
一個簡單的設計 —— ORM Lite
關於這個設計的代碼和樣例 😊:
https://github.com/BOT-Man-JL/ORM-Lite/tree/v1.0
0. 這個ORM要做什么
- 將對C++對象操作轉化成SQL查詢語句
(LINQ to SQL); - 提供C++ Style接口,更方便的使用;
我的設計上大致分為6個方面:
- 封裝SQL鏈接器
- 遍歷對象內需要持久化的成員
- 序列化和反序列化
- 獲取類名和各個字段的字符串
- 獲取字段類型
- 將對C++對象的操作轉化為SQL語句
1. 封裝SQL鏈接器
為了讓ORM支持各種數據庫,
我們應該把對數據庫的操作抽象為一個統一的 Execute
:
class SQLConnector
{
public:
SQLConnector (const std::string &connectionString);
void Execute (...);
};
2. 遍歷對象內需要持久化的成員
2.1 使用 Visitor Pattern + Variadic Template 遍歷
一開始,我想到的是使用
Visitor Pattern
組合
Variadic Template
進行成員的遍歷;
首先,在模型處加入 __Accept
操作;
通過 VISITOR
接受不同的 Visitor
來實現特定功能;
並用 __VA_ARGS__
傳入需要持久化的成員列表:
#define ORMAP(_MY_CLASS_, ...) \
template <typename VISITOR> \
void __Accept (VISITOR &visitor) \
{ \
visitor.Visit (__VA_ARGS__); \
} \
template <typename VISITOR> \
void __Accept (VISITOR &visitor) const \
{ \
visitor.Visit (__VA_ARGS__); \
} \
然后,針對不同功能,實現不同的 Visitor
;
再通過統一的 Visit
接口,接受模型的變長數據成員參數;
例如 ReaderVisitor
:
class ReaderVisitor
{
public:
// Data to Exchange
std::vector<std::string> serializedValues;
template <typename... Args>
inline void Visit (Args & ... args)
{
_Visit (args...);
}
protected:
template <typename T, typename... Args>
inline void _Visit (T &property, Args & ... args)
{
_Visit (property);
_Visit (args...);
}
template <typename T>
inline void _Visit (T &property) override
{
serializedValues.emplace_back (std::to_string (property));
}
template <>
inline void _Visit <std::string> (std::string &property) override
{
serializedValues.emplace_back ("'" + property + "'");
}
};
Visit
將操作轉發給帶有變長模板的_Visit
;- 有變長模板的
_Visit
將各個操作轉發給處理單個數據的_Visit
; - 處理單個數據的
_Visit
將模型的數據
和Visitor
一個public
數據成員(serializedValues
)交換;
不過,這么設計有一定的缺點:
- 我們需要預先定義所有的
Visitor
,靈活性不夠強; - 我們需要把和需要持久化的成員交換的數據保存到
Visitor
內部,
增大了代碼的耦合;
2.2 帶有 泛型函數參數 的 Visitor
(使用了C++14的特性)
所以,我們可以讓 Visit
接受一個泛型函數參數,用這個函數進行實際的操作;
在模型處加入的 __Accept
操作改為:
template <typename VISITOR, typename FN> \
void __Accept (const VISITOR &visitor, FN fn) \
{ \
visitor.Visit (fn, __VA_ARGS__); \
} \
template <typename VISITOR, typename FN> \
void __Accept (const VISITOR &visitor, FN fn) const \
{ \
visitor.Visit (fn, __VA_ARGS__); \
} \
fn
為泛型函數參數;- 每次調用
__Accept
的時候,把fn
傳給visitor
的Visit
函數;
然后,我們可以定義一個統一的 Visitor
,遍歷傳入的參數,並調用 fn
——
相當於將 Visitor
抽象為一個 for each
操作:
class FnVisitor
{
public:
template <typename Fn, typename... Args>
inline void Visit (Fn fn, Args & ... args) const
{
_Visit (fn, args...);
}
protected:
template <typename Fn, typename T, typename... Args>
inline void _Visit (Fn fn, T &property, Args & ... args) const
{
_Visit (fn, property);
_Visit (fn, args...);
}
template <typename Fn, typename T>
inline void _Visit (Fn fn, T &property) const
{
fn (property);
}
};
最后,實際的數據交換操作通過傳入特定的 fn
實現:
queryHelper.__Accept (FnVisitor (),
[&argv, &index] (auto &val)
{
DeserializeValue (val, argv[index++]);
});
- 對比上邊,這個方法實際上是在處理單個數據的
_Visit
將模型的數據
傳給回調函數fn
; fn
使用
Generic Lambda
接受不同類型的數據成員,然后再轉發給其他函數(DeserializeValue
);- 通過capture和需要持久化的成員交換的數據;
2.3 另一種設計——用 tuple
+ Refrence 遍歷
(使用了C++14的特性)
雖然最后版本沒有使用這個設計,不過作為一個不錯的思路,我還是記下來了;😁
首先,在模型處通過加入生成 tuple
的函數:
#define ORMAP(_MY_CLASS_, ...) \
inline decltype (auto) __ValTuple () \
{ \
return std::forward_as_tuple (__VA_ARGS__); \
} \
inline decltype (auto) __ValTuple () const \
{ \
return std::forward_as_tuple (__VA_ARGS__); \
} \
forward_as_tuple
將__VA_ARGS__
傳入的參數轉化為引用的tuple
;decltype (auto)
自動推導返回值類型;
然后,定義一個 TupleVisitor
:
// Using a _SizeT to specify the Index :-), Cool
template < size_t > struct _SizeT {};
template < typename TupleType, typename ActionType >
inline void TupleVisitor (TupleType &tuple, ActionType action)
{
TupleVisitor_Impl (tuple, action,
_SizeT<std::tuple_size<TupleType>::value> ());
}
template < typename TupleType, typename ActionType >
inline void TupleVisitor_Impl (TupleType &tuple, ActionType action,
_SizeT<0>)
{}
template < typename TupleType, typename ActionType, size_t N >
inline void TupleVisitor_Impl (TupleType &tuple, ActionType action,
_SizeT<N>)
{
TupleVisitor_Impl (tuple, action, _SizeT<N - 1> ());
action (std::get<N - 1> (tuple));
}
- 其中使用了
_SizeT
巧妙的進行tuple
下標的判斷; - 具體參考
http://stackoverflow.com/questions/18155533/how-to-iterate-through-stdtuple
最后,類似上邊,實際的數據交換操作通過 TupleVisitor
完成:
auto tuple = queryHelper.__ValTuple ();
TupleVisitor (tuple, [&argv, &index] (auto &val)
{
DeserializeValue (val, argv[index++]);
});
2.4 問題
- 使用
Variadic Template
和tuple
遍歷數據,
其函數調用的確定,都是編譯時就生成的,這會帶來一定的代碼空間開銷; - 后兩個方法可能在 實例化Generic Lambda 的時候,
針對 不同類型的模型的 不同數據成員類型 實例化出不同的副本,
代碼大小更大;🤔
3. 序列化和反序列化
通過 序列化,
將 C++ 數據類型轉化為字符串,用於查詢;
通過 反序列化,
將查詢得到的字符串,轉回 C++ 的數據類型;
3.1 重載函數 _Visit
針對每種支持的數據類型重載一個 _Visit
函數,
然后對其進行相應的序列化和反序列化;
以序列化為例:
void _Visit (long &property) override
{
serializedValues.emplace_back (std::to_string (property));
}
void _Visit (double &property) override
{
serializedValues.emplace_back (std::to_string (property));
}
void _Visit (std::string &property) override
{
serializedValues.emplace_back ("'" + property + "'");
}
3.2 使用 std::iostream
然而,針對每種支持的數據類型重載,這種事情在標准庫里已經有人做好了;
所以,我們可以改用了 std::iostream
進行序列化和反序列化;
以反序列化為例:
template <typename T>
inline std::ostream &SerializeValue (std::ostream &os,
const T &value)
{
return os << value;
}
template <>
inline std::ostream &SerializeValue <std::string> (
std::ostream &os, const std::string &value)
{
return os << "'" << value << "'";
}
4. 獲取類名和各個字段的字符串
我們可以使用宏中的 #
獲取傳入參數的文字量;
然后將這個字符串作為 private
成員存入這個類中:
#define ORMAP(_MY_CLASS_, ...) \
constexpr static const char *__ClassName = #_MY_CLASS_; \
constexpr static const char *__FieldNames = #__VA_ARGS__; \
其中
#_MY_CLASS_
為類名;#__VA_ARGS__
為傳入可變參數的字符串;__FieldNames
可以通過簡單的字符串處理獲得各個字段的字符串
5. 獲取字段類型
在新建數據庫的 Table
的時候,
我們不僅需要類名和各個字段的字符串,
還需要獲得各個字段的數據類型;
5.1 使用 typeid
運行時判斷
在 Visitor
遍歷成員時,將每個成員的 typeid
保存起來:
template <typename T>
void _Visit (T &property) override
{
typeIds.emplace_back (typeid (T));
}
運行時根據 typeid
判斷類型並匹配字符串:
if (typeId == typeid (int))
typeFmt = "int";
else if (typeId == typeid (double))
typeFmt = "decimal";
else if (typeId == typeid (std::string))
typeFmt = "varchar";
else
return false;
5.2 使用 <type_traits>
編譯時判斷
由於對象的類型在編譯時已經可以確定,
所以我們可以直接使用 <type_traits>
進行編譯時判斷:
constexpr const char *typeStr =
(std::is_integral<T>::value &&
!std::is_same<std::remove_cv_t<T>, char>::value &&
!std::is_same<std::remove_cv_t<T>, wchar_t>::value &&
!std::is_same<std::remove_cv_t<T>, char16_t>::value &&
!std::is_same<std::remove_cv_t<T>, char32_t>::value &&
!std::is_same<std::remove_cv_t<T>, unsigned char>::value)
? "integer"
: (std::is_floating_point<T>::value)
? "real"
: (std::is_same<std::remove_cv_t<T>, std::string>::value)
? "text"
: nullptr;
static_assert (
typeStr != nullptr,
"Only Support Integral, Floating Point and std::string :-)");
constexpr
編譯時完成推導,減少運行時開銷;static_assert
編譯時類型檢查;
6. 將對C++對象的操作轉化為SQL語句
這里,我們應該提供
Fluent Interface:
auto query = mapper.Query (helper)
.Where (
Field (helper.name) == "July" &&
(Field (helper.id) <= 90 && Field (helper.id) >= 60)
)
.OrderByDescending (helper.id)
.Take (3)
.Skip (10)
.ToVector ();
6.1 映射到對應的表中
對於一般的操作,通過模板類型參數,獲取 __ClassName
:
template <typename C>
void Insert (const C &entity)
{
auto tableName = C::__ClassName;
// ...
}
帶有條件的查詢通過 Query<MyClass>
,將自動映射到 MyClass
表中;
並返回自己的引用,實現Fluent Interface:
template <typename C>
ORQuery<C> Query (const C &queryHelper)
{
return ORQuery<C> (queryHelper, this);
}
ORQuery &Where (const Expr &expr)
{
// Parse expr
return *this;
}
6.2 自動將C++表達式轉為SQL表達式
首先,引入一個 Expr
類,用於保存條件表達式;
將 Expr
對象傳入 ORQuery.Where
,以實現條件查詢:
struct Expr
{
std::vector<std::pair<const void *, std::string>> expr;
template <typename T>
Expr (const T &property,
const std::string &relOp,
const T &value)
: expr { std::make_pair (&property, relOp) }
{
// Serialize value into expr.front ().second
}
inline Expr operator && (const Expr &right)
{
// Return Composite Expression
}
// ...
}
expr
保存了表達式序列,包括該成員的指針和關系運算符 值的字符串;- 重載
&& ||
實現表達式的復合條件;
不過這樣的接口還不夠友好;
因為如果我們要生成一個 Expr
則需要手動傳入 const std::string &relOp
:
mapper.Query (helper)
.Where (
Expr (helper.name, "=", std::string ("July")) &&
(Expr (helper.id, "<=", 90) && Expr (helper.id, ">=", 60))
)
// ...
所以,我們在這里引入一個 Expr_Field
實現自動構造表達式:
template <typename T>
struct Expr_Field
{
const T& _property;
Expr_Field (const T &property)
: _property (property)
{}
inline Expr operator == (T value)
{ return Expr { _property, "=", std::move (value) }; }
// != > < >= <=
}
template <typename T>
Expr_Field<T> Field (T &property)
{
return Expr_Field<T> { property };
}
Field
函數用更短的語句,返回一個Expr_Field
對象;- 重載
== != > < >= <=
生成帶有對應關系的Expr
對象;
6.3 自動判斷C++對象的成員字段名
由於沒有想到很好的辦法,所以目前使用了指針進行運行時判斷:
queryHelper.__Accept (FnVisitor (),
[&property, &isFound, &index] (auto &val)
{
if (!isFound && property == &val)
isFound = true;
else if (!isFound)
index++;
});
fieldName = FieldNames[index];
相當於使用 Visitor
遍歷這個對象,找到對應成員的序號;
6.4 問題
之后的版本可能考慮:
- 支持更多的SQL操作(如跨表);
- 改用語法樹實現;(歡迎 Pull Request 😉)
寫在最后
這篇文章是我的第一篇技術類博客,寫的比較淺,見諒;
你有一個蘋果,我有一個蘋果,我們彼此交換,每人還是一個蘋果;
你有一種思想,我有一種思想,我們彼此交換,每人可擁有兩種思想。
如果對以上內容及ORM Lite有什么問題,
歡迎 指點 討論 😉:
https://github.com/BOT-Man-JL/ORM-Lite/issues
Delivered under MIT License © 2016, BOT Man