[DotNet] MartenDB介紹01

@bepnamanh

最近在看紀錄log的資料,偶然看到MartenDB這個把postgres魔改成document database的的套件,蠻有趣的就來寫一下介紹。MartenDB希望可以把Postgres對於Jsonb的支援發揮到極致,官方文件有兩個部份,第一個部份是介紹如何透過MartenDB來把postgres當成mongoDB用,第二個部份是如何進一步用這個框架實做Event Sourcing,通常第二部份我應該會斷更哈哈....

文件資料庫與PostgresSQL

在系統開發中,關聯式資料庫通常被視為資料儲存首選方案。這種做法使得大多數開發者從資料表結構設計開始,並將整個系統設計納入關聯式資料庫的特性限制中。然而,使用文件式資料庫具有許多優點,例如不受資料表結構限制的資料模型設計。尤其在開發初期無法確定商業邏輯的情況下,設計資料庫模式變得困難,往往發生在後期發現規劃與實際需求不符時,無法輕易修改資料表結構而不得不容忍錯誤的情況。因此,許多新創公司開始採用像是 MongoDB 這樣的文件資料庫來進行開發。隨著此類需求的增加,RDBMS陣營也開始提供對 JSON 格式的支援,例如 PostgresSQL 提供了 JSONB 類型,並進一步加強了對 JSON 資料的查詢支援。

MartenDB簡介

MartenDB 是一個針對 PostgresSQL 進行改進的框架,希望能夠充分利用 PostgresSQL 對於 JSONB 的支援。大多數開發者和DBA對於 JSONB 的操作可能不太熟悉,Marten 希望能夠讓Jsonb的操作能夠簡單易用,有興趣可以看一下這個影片。就算不選擇使用 MartenDB,仍然很推薦讀一下官方的文件,裡面包含了關於 PostgresSQL 的 JSONB 的小技巧,例如如何針對鍵值進行索引等等,都十分有幫助。

MartenDB基本操作

  1. 基本設定
    先安裝marten的nuget套件,如果使用微軟提供的相依性注入的話請要加入設定

    services.AddMarten(opts =>
        {
            // 放入Postgre的連線字串
            opts.Connection(connectionString);
            // 可以放入自訂的logger
            opts.Logger();
        })
        // 這個設定會依照環境不同
        // 開發環境下自動生成需要的資料表
        // production環境一律不新增
        .OptimizeArtifactWorkflow()
        // 通常情況下可以開啟這個設定
        .UseLightweightSessions();
    

    如果不使用相依性注入的話只要簡短一行

    var store = DocumentStore.For(connectionString);
    
  2. DocumentStore
    類似於EFCore中的DbContext一樣,Marten中有一個DocumentStore物件控管資料庫連線

    1. IDocumentStore:
      生命週期為Singleton,用來管理連線、操作設定、資料遷移等等
    2. IDocumentSession:
      生命週期為Scoped,用來查詢與寫入,預設情況下會使用identity map來快取每次查詢的資料,前面提到的UseLightweightSessions就是用來取消這樣的行為,因為通常情況下一次Request內不太會需要重複查詢,而快取會在Request結束後隨著物件一起釋放。
    3. IQuerySession:
      針對只讀的情況下建議使用。
  3. 其他設定
    Marten還提供一些其他的設定,這邊只列處幾點

    1. 預設使用JSON.NET進行序列化,可以改用System.Text或者自訂
    2. 可以設定重試策略
    3. 可以對欄位做設定,但對於資料表別名自訂有限,所有的資料表都要加上mt_前綴( 依照這個issue作者看起來完全不打算開放)
    4. 多資料庫連線(應該是搭配Event Source功能)
  4. 範例
    以下只是稍微改了一下文件上的範例,移植到WebAPI專案上

    [ApiController]
    [Route("[controller]")]
    public class UserController : ControllerBase
    {
        [HttpPost]
        public async Task<Guid> Post(
            [FromBody] CreateUserRequest request,
            // 因為要寫入所以使用IDocumentSession 
            [FromServices] IDocumentSession session)
        {  
            var user = new User
            {
                FirstName = request.FirstName,
                LastName = request.LastName,
                Internal = request.Internal
            };
            session.Insert(user);
            await session.SaveChangesAsync();
            // 注意一定要有Id
            // 使用上可以用階層物件的方式
            // 將Id以外的資料用record的方式做成value object
            return user.Id;
        }
    
        [HttpGet("/{id:guid}")]
        public async Task<User?> Get(
            [FromRoute] Guid id,
            // 因為是只讀所以可以使用IQuerySession 
            [FromServices] IQuerySession session) => 
        await session.LoadAsync<User>(id);
        // 也支援Linq查詢
        // await session.LoadAsync<User>().FirstOrDefaultAsync(x => x.Id == id);
    }
    

    還有一些進一步的用法像是upsert或批次操作,這邊就不特別秀出來,接下來我們看一下它生成的表

    table_name: mt_doc_user

    iddatamt_last_modifiedmt_versionmt_dotnet_type
    0188d468-8526-4c4e-ac0a-d142363918ab{"Id": "0188d468-8526-4c4e-ac0a-d142363918ab", "Internal": true, "LastName": "string", "FirstName": "string"}2023-06-19 16:06:39.785208 +00:000188d468-8528-4889-8a47-bdc600e5e039User

    自動生成的資料表叫做mt_doc_user,可以看到User完全被序列化記入data,並且Id被當成資料的Id,文件上也有說明針對需要容易查找的欄位也可以拉成獨立的column,還有下index的技巧,整個使用上還算簡單。

小結

總體而言martenDB還算容易使用,然後僅僅只能使用於PostgresSQL。支援自行定義與db的綁定關係,有點類似EFCore的code-first,也有針對資料庫併發的一些設定,畢竟主力還是把它作為低成本的event-store使用,希望下一篇不要拖太久。