閱讀這篇文章前,你最好知道什麼是 Object Relation Mapping (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
0. 這個ORM要做什麼
- 將對C++物件操作轉化成SQL查詢語句 (LINQ to SQL);
- 提供C++ Style介面,更方便的使用;
我的設計上大致分為6個方面:
- 封裝SQL連結器
- 遍歷物件內需要持久化的成員
- 序列化和反序列化
- 獲取類名和各個欄位的字串
- 獲取欄位型別
- 將對C++物件的操作轉化為SQL語句
1. 封裝SQL連結器
為了讓ORM支援各種資料庫, 我們應該把對資料庫的操作抽象為一個統一的 Execute
:
1 2 3 4 5 6 |
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__
傳入需要持久化的成員列表:
1 2 3 4 5 6 7 8 9 10 11 |
#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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
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
操作改為:
1 2 3 4 5 6 7 8 9 10 |
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
操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
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
實現:
1 2 3 4 5 |
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
的函式:
1 2 3 4 5 6 7 8 9 |
#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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// 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
完成:
1 2 3 4 5 |
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
函式, 然後對其進行相應的序列化和反序列化;
以序列化為例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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
進行序列化和反序列化;
以反序列化為例:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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
成員存入這個類中:
1 2 3 |
#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
儲存起來:
1 2 3 4 5 |
template <typename T> void _Visit (T &property) override { typeIds.emplace_back (typeid (T)); } |
執行時根據 typeid
判斷型別並匹配字串:
1 2 3 4 5 6 7 8 |
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>
進行編譯時判斷:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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:
1 2 3 4 5 6 7 8 9 |
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
:
1 2 3 4 5 6 |
template <typename C> void Insert (const C &entity) { auto tableName = C::__ClassName; // ... } |
帶有條件的查詢通過 Query<MyClass>
,將自動對映到 MyClass
表中; 並返回自己的引用,實現Fluent Interface:
1 2 3 4 5 6 7 8 9 10 11 |
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
,以實現條件查詢:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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
:
1 2 3 4 5 6 |
mapper.Query (helper) .Where ( Expr (helper.name, "=", std::string ("July")) && (Expr (helper.id, "<=", 90) && Expr (helper.id, ">=", 60)) ) // ... |
所以,我們在這裡引入一個 Expr_Field
實現自動構造表示式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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++物件的成員欄位名
由於沒有想到很好的辦法,所以目前使用了指標進行執行時判斷:
1 2 3 4 5 6 7 8 9 |
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