如何設計一個簡單的 C++ ORM

發表於2016-11-24

閱讀這篇文章前,你最好知道什麼是 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

大致可以分成這幾類:

以上的方案都使用了 Proxy PatternAdapter Pattern 實現 ORM 功能,並提供一個用於和資料庫交換資料的 容器

所以我打算封裝一個直接操作 原始模型 的 ORM;

一個簡單的設計 —— ORM Lite

關於這個設計的程式碼和樣例 : https://github.com/BOT-Man-JL/ORM-Lite

0. 這個ORM要做什麼

  • 將對C++物件操作轉化成SQL查詢語句 (LINQ to SQL);
  • 提供C++ Style介面,更方便的使用;

我的設計上大致分為6個方面:

  1. 封裝SQL連結器
  2. 遍歷物件內需要持久化的成員
  3. 序列化和反序列化
  4. 獲取類名和各個欄位的字串
  5. 獲取欄位型別
  6. 將對C++物件的操作轉化為SQL語句

1. 封裝SQL連結器

為了讓ORM支援各種資料庫, 我們應該把對資料庫的操作抽象為一個統一Execute

  • 一個有點意思的介面設計 —— std::db
  • 因為SQLite比較簡單,目前只實現了SQLite的版本;
  • MySql版本應該會在 這裡 維護。。。

2. 遍歷物件內需要持久化的成員

2.1 使用 Visitor Pattern + Variadic Template 遍歷

一開始,我想到的是使用 Visitor Pattern 組合 Variadic Template 進行成員的遍歷;

首先,在模型處加入 __Accept 操作; 通過 VISITOR 接受不同的 Visitor來實現特定功能; 並用 __VA_ARGS__ 傳入需要持久化的成員列表

然後,針對不同功能,實現不同的 Visitor; 再通過統一的 Visit 介面,接受模型變長資料成員引數; 例如 ReaderVisitor

  • Visit 將操作轉發給帶有變長模板_Visit
  • 變長模板_Visit 將各個操作轉發給處理單個資料_Visit
  • 處理單個資料_Visit模型的資料 和 Visitor 一個 public 資料成員(serializedValues)交換;

不過,這麼設計有一定的缺點:

  • 我們需要預先定義所有的 Visitor,靈活性不夠強;
  • 我們需要把和需要持久化的成員交換的資料儲存到 Visitor 內部, 增大了程式碼的耦合;

2.2 帶有 泛型函式引數 的 Visitor

(使用了C++14的特性)

所以,我們可以讓 Visit 接受一個泛型函式引數,用這個函式進行實際的操作;

模型處加入的 __Accept 操作改為:

  • fn泛型函式引數
  • 每次呼叫 __Accept 的時候,把 fn 傳給 visitorVisit 函式;

然後,我們可以定義一個統一的 Visitor,遍歷傳入的引數,並呼叫 fn—— 相當於將 Visitor 抽象為一個 for each 操作:

最後,實際的資料交換操作通過傳入特定的 fn 實現:

  • 對比上邊,這個方法實際上是在處理單個資料_Visit模型的資料 傳給回撥函式 fn
  • fn 使用 Generic Lambda 接受不同型別的資料成員,然後再轉發給其他函式(DeserializeValue);
  • 通過capture需要持久化的成員交換的資料;

2.3 另一種設計——用 tuple + Refrence 遍歷

(使用了C++14的特性)

雖然最後版本沒有使用這個設計,不過作為一個不錯的思路,我還是記下來了;

首先,在模型處通過加入生成 tuple 的函式:

  • forward_as_tuple__VA_ARGS__ 傳入的引數轉化為引用的 tuple
  • decltype (auto) 自動推導返回值型別;

然後,定義一個 TupleVisitor

最後,類似上邊,實際的資料交換操作通過 TupleVisitor 完成:

2.4 問題

  • 使用 Variadic Templatetuple 遍歷資料, 其函式呼叫的確定,都是編譯時就生成的,這會帶來一定的程式碼空間開銷;
  • 後兩個方法可能在 例項化Generic Lambda 的時候, 針對 不同型別的模型的 不同資料成員型別 例項化出不同的副本, 程式碼大小更大;

3. 序列化和反序列化

通過 序列化, 將 C++ 資料型別轉化為字串,用於查詢; 通過 反序列化, 將查詢得到的字串,轉回 C++ 的資料型別;

3.1 過載函式 _Visit

針對每種支援的資料型別過載一個 _Visit 函式, 然後對其進行相應的序列化和反序列化

以序列化為例:

3.2 使用 std::iostream

然而,針對每種支援的資料型別過載,這種事情在標準庫裡已經有人做好了; 所以,我們可以改用了 std::iostream 進行序列化和反序列化

以反序列化為例:

4. 獲取類名和各個欄位的字串

我們可以使用巨集中的 # 獲取傳入引數的文字量; 然後將這個字串作為 private 成員存入這個類中:

其中

  • #_MY_CLASS_類名
  • #__VA_ARGS__ 為傳入可變引數的字串;
  • __FieldNames 可以通過簡單的字串處理獲得各個欄位的字串

5. 獲取欄位型別

新建資料庫的 Table 的時候, 我們不僅需要類名和各個欄位的字串, 還需要獲得各個欄位的資料型別

5.1 使用 typeid 執行時判斷

Visitor 遍歷成員時,將每個成員的 typeid 儲存起來:

執行時根據 typeid 判斷型別並匹配字串:

5.2 使用 <type_traits> 編譯時判斷

由於物件的型別在編譯時已經可以確定, 所以我們可以直接使用 <type_traits> 進行編譯時判斷:

  • constexpr 編譯時完成推導,減少執行時開銷;
  • static_assert 編譯時型別檢查;

6. 將對C++物件的操作轉化為SQL語句

這裡,我們應該提供 Fluent Interface

6.1 對映到對應的表中

對於一般的操作,通過模板型別引數,獲取 __ClassName

帶有條件的查詢通過 Query<MyClass>,將自動對映到 MyClass 表中; 並返回自己的引用,實現Fluent Interface

6.2 自動將C++表示式轉為SQL表示式

首先,引入一個 Expr 類,用於儲存條件表示式; 將 Expr 物件傳入 ORQuery.Where,以實現條件查詢:

  • expr 儲存了表示式序列,包括該成員的指標關係運算子 值的字串;
  • 過載 && || 實現表示式的複合條件

不過這樣的介面還不夠友好; 因為如果我們要生成一個 Expr 則需要手動傳入 const std::string &relOp

所以,我們在這裡引入一個 Expr_Field 實現自動構造表示式

  • Field 函式用更短的語句,返回一個 Expr_Field 物件;
  • 過載 == != > < >= <= 生成帶有對應關係的 Expr 物件;

6.3 自動判斷C++物件的成員欄位名

由於沒有想到很好的辦法,所以目前使用了指標進行執行時判斷:

相當於使用 Visitor 遍歷這個物件,找到對應成員的序號

6.4 問題

之後的版本可能考慮:

  • 支援更多的SQL操作(如跨表);
  • 改用語法樹實現;(歡迎 Pull Request )

寫在最後

這篇文章是我的第一篇技術類部落格,寫的比較淺,見諒;

你有一個蘋果,我有一個蘋果,我們彼此交換,每人還是一個蘋果; 你有一種思想,我有一種思想,我們彼此交換,每人可擁有兩種思想。

如果對以上內容及ORM Lite有什麼問題, 歡迎 指點 討論 :https://github.com/BOT-Man-JL/ORM-Lite/issues

相關文章