iT邦幫忙

2022 iThome 鐵人賽

DAY 26
2
Software Development

Moleculer 家家酒系列 第 26

Day 26 : 資料庫 Adapters

  • 分享至 

  • xImage
  •  

資料庫 Adapters

資料庫 Adapters 就像 IKEA 家具一樣提供各種不同的組裝套件,你可以根據不同的需求來選擇適合的 Adapter 。 Moleculer 框架有屬於自己的資料庫 Adapters,你可以使用它來將資料儲存於資料庫。

Moleculer 遵循一個服務一個資料庫的模式。想了解更多可以參閱此設計模式文章[2] 。關於資料庫 Adapters 的常見問題可以參閱 FAQ[3] 。

功能

  • 預設 CRUD Actions
  • 快取 Actions
  • 支援分頁
  • 可替換 Adapter (預設使用 NeDB[4] 這套記憶體 Adapter 來測試及原型設計。)
  • 官方支援的 Adapters 有 MongoDBPostgreSQLSQLiteMySQLMSSQL
  • 欄位過濾
  • 填充欄位
  • ID 編解碼
  • 生命週期實體事件

基礎 Adapter

Moleculer 使用 NeDB[4] 作為預設的 Adapter ,你可以快速使用它來配置與測試原型系統。

注意,NeDB 只能用在測試或原型開發環境。生產環境請換到 MongoMongooseSequelize 等 adapter ,這些方法都具備通用的設定、 Action 與方法。

安裝

npm install moleculer-db --save

使用

"use strict";

const { ServiceBroker } = require("moleculer");
const DbService = require("moleculer-db");

const broker = new ServiceBroker();

// 建立一個 users 服務
broker.createService({
    name: "users",

    // 將 DB 函數混合到 'users' 服務
    mixins: [DbService],

    settings: {
        fields: ["_id", "username", "name"],
        entityValidator: {
            username: "string"
        }
    },

    afterConnected() {
        // 建立一些初始資料函數 `this.create`
    }
});

broker.start()

    // 建立新使用者
    .then(() => broker.call("users.create", {
        _id: 1,
        username: "john",
        name: "John Doe",
        status: 1
    }))

    // 取得所有 user
    .then(() => broker.call("users.find").then(console.log))

    // 使用分頁取得 user
    .then(() => broker.call("users.list", { page: 2, pageSize: 10 }).then(console.log))

    // 取得一筆 user
    .then(() => broker.call("users.get", { id: 1 }).then(console.log))

    // 更新一筆 user
    .then(() => broker.call("users.update", { id: 1, name: "Jane Doe" }).then(console.log))

    // 刪除一筆 user
    .then(() => broker.call("users.remove", { id: 1 }).then(console.log));

更多範例請參閱:

https://github.com/moleculerjs/moleculer-db/tree/master/packages/moleculer-db/examples

設定

所有的資料庫 adapters 都能使用通用的設定:

名稱 類型 預設值 說明
idField <String> "_id" 欄位名稱
fields <String[]> null 欄位過濾清單。如果設為 nullundefined 則不過濾。
populates <Array> null 填充綱目
pageSize <Number> 10 list Action 的分頁大小
maxPageSize <Number> 100 list Action 的最大分頁大小
maxLimit <Number> -1 find Action 的最大限制資料數。設為 -1 為不限制。
entityValidator <Object> | <function> null createinsert Action 用來驗證傳入實體的驗證器綱目或函數。

idField 不適用於 Sequelize adapter ,因為它可以在創建模型時自由的設定你自己的 ID 。

Actions

資料庫 adapters 也實作了 CRUD 操作。這些 Action 是已被發布的方法,可以被其它服務呼叫使用。

find

Query 查詢實體。支援快取。

參數:

名稱 類型 預設值 說明
populate <String[]> - 欄位填充清單
fields <String[]> - 欄位過濾清單
limit <Number> 必填 最大資料數
offset <Number> 必填 跳過資料數
sort <String> 必填 排序設定
search <String> 必填 查詢的文字
searchFields <String> 必填 查詢的欄位
query <Object> 必填 Query 查詢。直接傳遞給 adapter 。

響應:

<Object[]> - 查詢到的實體物件清單。

count

Query 查詢實體數量。支援快取。

參數:

名稱 類型 預設值 說明
search <String> 必填 查詢的文字
searchFields <String> 必填 查詢的欄位
query <Object> 必填 Query 查詢。直接傳遞給 adapter 。

響應:

<Number> - 查詢到的實體物件數量。

list

使用過濾與分頁查詢實體清單。支援快取。

參數:

名稱 類型 預設值 說明
populate <String[]> - 欄位填充清單
fields <String[]> - 欄位過濾清單
page <Number> 必填 分頁數
pageSize <Number> 必填 分頁大小
sort <String> 必填 排序設定
search <String> 必填 查詢的文字
searchFields <String> 必填 查詢的欄位
query <Object> 必填 Query 查詢。直接傳遞給 adapter 。

響應:

<Object> - 查詢到的實體物件清單及數量。

create

建立一個新的實體物件。

參數:

  • 新的實體物件。

響應:

<Object> - 已建立的實體物件。

insert

建立多個新的實體物件。

參數:

名稱 類型 預設值 說明
entity <Object> - 實體物件
entities <Object[]> - 多個實體物件

響應:

<Object> | <Object[]> - 已建立的實體物件或多個實體物件。

get

由 ID 取得實體物件。支援快取。

參數:

名稱 類型 預設值 說明
id <Any> | <Any[]> 必填 ID 或多個 ID
populate <String[]> - 欄位填充清單
fields <String[]> - 欄位過濾清單
mapping <Boolean> - 將響應的陣列轉為物件型態,並將 ID 設為鍵值。

響應:

<Object> | <Object[]> - 查詢到實體物件或清單。

update

由 ID 更新實體物件。

注意,更新後請記得清除快取並呼叫生命週期事件。

參數:

  • 欲更新的實體物件。

響應:

<Object> - 已更新的實體物件。

remove

由 ID 刪除實體物件。

參數:

名稱 類型 預設值 說明
id <Any> | <Any[]> 必填 ID 或多個 ID

響應:

<Number> - 已刪除的實體物件數量。

方法

資料庫 adapters 也實作了輔助的方法。

getById

由一個或多個 ID 取得一個或多個實體物件。

參數:

名稱 類型 預設值 說明
id <String> | <Number> | <Array> 必填 ID 或多個 ID
decoding <Boolean> 必填 ID 是否需要解碼

響應:

<Object> | <Object[]> - 查詢到的一個或多個實體物件。

clearCache

清除快取實體物件。

參數:

  • 無需參數。

響應:

<Promise> - 空的 Promise 物件。

encodeID

編碼實體物件 ID 。

參數:

名稱 類型 預設值 說明
id <Any> 必填 ID

響應:

<Any> - 編碼後的 ID。

decodeID

解碼實體物件 ID 。

參數:

名稱 類型 預設值 說明
id <Any> 必填 未解碼的 ID

響應:

<Any> - 解碼後的 ID。

注意,只有支援的 Adapter 編解碼才有效果。

_find

Query 查詢實體。支援快取。

參數:

名稱 類型 預設值 說明
populate <String[]> - 欄位填充清單
fields <String[]> - 欄位過濾清單
limit <Number> 必填 最大資料數
offset <Number> 必填 跳過資料數
sort <String> 必填 排序設定
search <String> 必填 查詢的文字
searchFields <String> 必填 查詢的欄位
query <Object> 必填 Query 查詢。直接傳遞給 adapter 。

響應:

<Object[]> - 查詢到的實體物件清單。

_count

使用過濾與分頁查詢實體清單。支援快取。

參數:

名稱 類型 預設值 說明
populate <String[]> - 欄位填充清單
fields <String[]> - 欄位過濾清單
page <Number> 必填 分頁數
pageSize <Number> 必填 分頁大小
sort <String> 必填 排序設定
search <String> 必填 查詢的文字
searchFields <String> 必填 查詢的欄位
query <Object> 必填 Query 查詢。直接傳遞給 adapter 。

響應:

<Object> - 查詢到的實體物件清單及數量。

_create

建立一個新的實體物件。

參數:

名稱 類型 預設值 說明
params <Object> - 實體物件

響應:

<Object> - 已建立的實體物件。

_insert

建立多個新的實體物件。

參數:

名稱 類型 預設值 說明
entity <Object> - 實體物件
entities <Object[]> - 多個實體物件

響應:

<Object> | <Object[]> - 已建立的實體物件或多個實體物件。

_get

由 ID 取得實體物件。支援快取。

參數:

名稱 類型 預設值 說明
id <Any> | <Any[]> 必填 ID 或多個 ID
populate <String[]> - 欄位填充清單
fields <String[]> - 欄位過濾清單
mapping <Boolean> - 將響應的陣列轉為物件型態,並將 ID 設為鍵值。

響應:

<Object> | <Object[]> - 查詢到實體物件或清單。

_update

由 ID 更新實體物件。

注意,更新後請記得清除快取並呼叫生命週期事件。

參數:

名稱 類型 預設值 說明
params <Object> - 欲更新的實體物件

響應:

<Object> - 已更新的實體物件。

_remove

由 ID 刪除實體物件。

參數:

名稱 類型 預設值 說明
id <Any> 必填 實體物件 ID

響應:

<Number> - 已刪除的實體物件數量。

資料 Hooks

你可以使用 Action Hooks ,在資料儲存之前變更要儲存進資料庫的資料,或是在取得資料後變更輸出的資料。

範例:使用 Hooks 加入時間戳記與移除敏感資訊

"use strict";
const { ServiceBroker } = require("moleculer");
const DbService = require("moleculer-db");

const broker = new ServiceBroker();

broker.createService({
    name: "db-with-hooks",

    // 載入資料庫 actions
    mixins: [DbService],

    // 在資料庫 action 建立 Hooks
    hooks: {
        before: {
            create: [
                function addTimestamp(ctx) {
                    // 加入時間戳記
                    ctx.params.createdAt = new Date();
                    return ctx;
                }
            ]
        },
        after: {
            get: [
                // 使用箭頭函數建立 Hook
                (ctx, res) => {
                    // 移除敏感資訊
                    delete res.mail;
                    delete res.phoneNumber;

                    return res;
                }
            ]
        }
    }
});

const user = {
    name: "John Doe",
    mail: "john.doe@example.com",
    phoneNumber: 123456789
};

broker.start()
    // 將使用者資料寫入資料庫
    // 呼叫 "create" action 之前會先觸發 hook
    .then(() => broker.call("db-with-hooks.create", user))
    // 由資料庫取得使用者
    // 呼叫 "get" action 之前會先觸發 hook
    .then(entry => broker.call("db-with-hooks.get", { id: entry._id }))
    .then(res => console.log(res))
    .catch(err => console.error(err));

填充

你可以輕鬆的透過 populates 設定來由其它服務填充欄位。例如,你的 post 實體物件中有一個 author 欄位,你可以在 users 服務由 ID 來取得資料填充到 author 中。如果欄位是一個陣列且有多個 ID 時,僅需一個請求就會填充到所有的實體物件。

填充參數可用於 findlistget

範例:填充綱目

broker.createService({
    name: "posts",
    mixins: [DbService],
    settings: {
        populates: {
            // 速記填充規則。由 `users.get` 來填充 `voters` 的值
            "voters": "users.get",

            // 定義 Action 呼叫時的參數。它只會將 `username` 與 `fullName` 填充到 `author`
            "author": {
                action: "users.get",
                params: {
                    fields: "username fullName"
                }
            },
            // 在這個例子中,原始的 `reviewerId` 欄位不應該被覆蓋填充值。
            // 它會由 `reviewerId` 呼叫 `users.get` 取得資料,
            // 但是會將 `username` 與 `fullName` 填充到 `reviewer` 。
            "reviewer": {
                field: "reviewerId",
                action: "users.get",
                params: {
                    fields: "username fullName"
                }
            },

            // 客製化填充處理函數
            "rate"(ids, items, rule, ctx) {
                // `items` 參數是響應的陣列內容
                // 它與 Promise.resolve 的資料無關,你需要的是客製化填充 `items` 。
                // 如有疑問請參閱原始碼[5]

                return Promise.resolve(...);
            }
        }
    }
});

// 查詢 posts 清單並填充 `authors`
broker.call("posts.find", { populate: ["author"] }).then(console.log);

生命週期實體事件

進行實體物件操作的時候,會呼叫 3 個生命週期事件。

注意,如果是使用 updateManyremoveMany 操作多個實體物件,則 json 參數將會是一個 Number 而不是一個實體物件。

broker.createService({
    name: "posts",
    mixins: [DbService],
    settings: {},

    afterConnected() {
        this.logger.info("Connected successfully");
    },

    entityCreated(json, ctx) {
        this.logger.info("New entity created!");
    },

    entityUpdated(json, ctx) {
        // 你可以在此訪問 context
        this.logger.info(`Entity updated by '${ctx.meta.user.name}' user!`);
    },

    entityRemoved(json, ctx) {
        this.logger.info("Entity removed", json);
    },
});

客製化 Action 延伸功能

你也可以在服務中加入客製化的 Action 來完成所需的功能。

const DbService = require("moleculer-db");

module.exports = {
    name: "posts",
    mixins: [DbService],

    settings: {
        fields: ["_id", "title", "content", "votes"]
    },

    actions: {
        // 由 `id` 更新累加 `votes` 欄位
        vote(ctx) {
            return this.adapter.updateById(ctx.params.id, { $inc: { votes: 1 } });
        },

        // 查詢 `authorID` 的 `posts` 清單
        byAuthors(ctx) {
            return this.find({
                query: {
                    author: ctx.params.authorID
                },
                limit: ctx.params.limit || 10,
                sort: "-createdAt"
            });
        }
    }
};

Mongo Adapter

基於 MongoDB[6] 的 Adapter 。

使用前請安裝 MongoDB 相關套件 npm install moleculer-db moleculer-db-adapter-mongo --save

MongoDB Adapter 需要依賴 MongoDB[7] ,你必須在你的系統安裝它。

範例:

"use strict";

const { ServiceBroker } = require("moleculer");
const DbService = require("moleculer-db");
const MongoDBAdapter = require("moleculer-db-adapter-mongo");

const broker = new ServiceBroker();

// 建立一個 Mongodb 的 `posts` 實體物件
broker.createService({
    name: "posts",
    mixins: [DbService],
    adapter: new MongoDBAdapter("mongodb://localhost/moleculer-demo"),
    collection: "posts"
});

broker.start()
    // 建立一個新 `post`
    .then(() => broker.call("posts.create", {
        title: "My first post",
        content: "Lorem ipsum...",
        votes: 0
    }))

    // 取得所有 `posts`
    .then(() => broker.call("posts.find").then(console.log));

範例:使用 URI 及選項

new MongoDBAdapter("mongodb://localhost/moleculer-db", {
    keepAlive: 1
})

更多 Mongo Adapter 範例請參閱 moleculer-db Github[8] 。

Mongoose Adapter

基於 Mongoose[9] 的 Adapter 。

使用前請安裝 Mongoose 相關套件 npm install moleculer-db moleculer-db-adapter-mongoose mongoose --save

Mongoose Adapter 需要依賴 MongoDB[7] ,你必須在你的系統安裝它。

範例:

"use strict";

const { ServiceBroker } = require("moleculer");
const DbService = require("moleculer-db");
const MongooseAdapter = require("moleculer-db-adapter-mongoose");
const mongoose = require("mongoose");

const broker = new ServiceBroker();

// 建立一個 Mongoose 的 `posts` 實體物件
broker.createService({
    name: "posts",
    mixins: [DbService],
    adapter: new MongooseAdapter("mongodb://localhost/moleculer-demo"),
    model: mongoose.model("posts", mongoose.Schema({
        title: { type: String },
        content: { type: String },
        votes: { type: Number, default: 0 }
    }))
});


broker.start()
    // 建立一個新 `post`
    .then(() => broker.call("posts.create", {
        title: "My first post",
        content: "Lorem ipsum...",
        votes: 0
    }))

    // 取得所有 `posts`
    .then(() => broker.call("posts.find").then(console.log));

範例:使用 URI 及選項

new MongooseAdapter("mongodb://localhost/moleculer-db", {
    user: process.env.MONGO_USERNAME,
    pass: process.env.MONGO_PASSWORD
    keepAlive: true
})

連線到多個資料庫

如果你的服務部屬在多個節點上,且希望能連線到多個資料庫,那麼你可以直接在服務中定義 model 就好。但如果你的服務僅部屬在一個節點上,又希望能連線到多個資料庫,那麼則應該要定義 schema 來建立多個連線的服務。

更多 Mongoose Adapter 範例請參閱 moleculer-db Github[10] 。

Sequelize Adapter

Moleculer DB 使用 Sequelize[11] ORM 工具來開發,它可支援 PostgresMySQLSQLiteMSSQL 的 SQL Adapter。

安裝

npm install moleculer-db-adapter-sequelize --save

請根據資料庫需求安裝對應的資料庫套件:

SQLite

此資料庫會自動生成 SQLite[12] 資料庫檔案。

npm install sqlite3 --save

MySQL

此資料庫依賴 MySQL[13] ,你必須在你的系統安裝它。

npm install mysql2 --save

PostgreSQL

此資料庫依賴 PostgreSQL[14] ,你必須在你的系統安裝它。

npm install pg pg-hstore --save

MSSQL

此資料庫依賴 MSSQL[15] ,你必須在你的系統安裝它。

npm install tedious --save

使用

範例:

"use strict";

const { ServiceBroker } = require("moleculer");
const DbService = require("moleculer-db");
const SqlAdapter = require("moleculer-db-adapter-sequelize");
const Sequelize = require("sequelize");

const broker = new ServiceBroker();

// 建立一個 Sequelize 的 `posts` 實體物件
broker.createService({
    name: "posts",
    mixins: [DbService],
    adapter: new SqlAdapter("sqlite://:memory:"),
    model: {
        name: "post",
        define: {
            title: Sequelize.STRING,
            content: Sequelize.TEXT,
            votes: Sequelize.INTEGER,
            author: Sequelize.INTEGER,
            status: Sequelize.BOOLEAN
        },
        options: {
            // 選項請參閱:
            // https://sequelize.org/docs/v6/moved/models-definition/
        }
    },
});

broker.start()
    // 建立一個新 `post`
    .then(() => broker.call("posts.create", {
        title: "My first post",
        content: "Lorem ipsum...",
        votes: 0
    }))

    // 取得所有 `posts`
    .then(() => broker.call("posts.find").then(console.log));

範例:使用 URI

new SqlAdapter("postgres://user:pass@example.com:5432/dbname")

範例:使用選項連線

new SqlAdapter('database', 'username', 'password', {
    host: 'localhost',
    dialect: 'mysql'|'sqlite'|'postgres'|'mssql',

    pool: {
        max: 5,
        min: 0,
        idle: 10000
    },

    noSync: true // 不同步。Sequelize 將不會同步 Model

    // SQLite 檔案路徑
    storage: 'path/to/database.sqlite'
})

更多 Mongoose Adapter 範例請參閱 moleculer-db Github[16] 。

更多的資料庫 Adapters 請參閱官方手冊:

https://moleculer.services/modules.html#databases

參考文獻

[1] Database Adapters, https://moleculer.services/docs/0.14/moleculer-db.html
[2] Pattern: Database per service, https://microservices.io/patterns/data/database-per-service.html
[3] DB Adapters (moleculer-db), https://moleculer.services/docs/0.14/faq.html#DB-Adapters-moleculer-db
[4] NeDB, https://github.com/louischatriot/nedb
[5] moleculer-db@0.8.19, https://github.com/moleculerjs/moleculer-db/blob/moleculer-db%400.8.19/packages/moleculer-db/src/index.js#L589-L658
[6] MongoDB Node.js Driver, https://mongodb.github.io/node-mongodb-native/
[7] MongoDB, https://www.mongodb.com/
[8] moleculer-db-adapter-mongo examples, https://github.com/moleculerjs/moleculer-db/tree/master/packages/moleculer-db-adapter-mongo/examples
[9] mongoose, https://mongoosejs.com/docs/
[10] moleculer-db-adapter-mongoose examples, https://github.com/moleculerjs/moleculer-db/tree/master/packages/moleculer-db-adapter-mongoose/examples
[11] Sequelize, https://github.com/sequelize/sequelize
[12] SQLite, https://www.sqlite.org/index.html
[13] MySQL, https://www.mysql.com/
[14] PostgreSQL, https://www.postgresql.org/
[15] MSSQL, https://www.microsoft.com/sql-server
[16] moleculer-db-adapter-sequelize examples, https://github.com/moleculerjs/moleculer-db/tree/master/packages/moleculer-db-adapter-sequelize/examples

家家酒小劇場

  • Otter - 今天的內容看起來好像蠻容易的耶。
  • Boxy - 資料庫的使用上雖然並不難,但是微服務的資料庫架構設計上卻是一門大學問,建議使用前一定要先充實相關知識唷。

上一篇
Day 25 : API 閘道器 - Part 2
下一篇
Day 27 : CLI 工具
系列文
Moleculer 家家酒31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言