5 months ago

最近專案打算從手寫SQL的純浪漫改成套用ORM Sequelize,在改用的過程中讓程式碼更好閱讀,但同樣也採了不少坑,尤其是在關聯與搜尋上蠻多小細節要注意,前前後後摸了兩天才整個弄起來,分享一下寫法。

稍微示範一下簡化的活動系統,以下是簡單的UML與資料庫欄位說明,內容會涵蓋 1:1 / 1:m / n:m / 搜尋 / transaction等,基本上實戰會需要的內容都會涵蓋。


一直都沒有找到線上UML繪圖,所以只能簡單示意,基本上就是

  1. 活動與標籤為多對多關聯,有個中間Table紀錄
  2. 活動與訂單為一對多,而訂單與聯絡資料為一對一關聯

代碼架構

我是將所有DB操作封裝成一個DAO層,放在/models資料夾下,以下是資料夾其他內容,這會與後續寫法牽連

connection.js : 初始化Sequelize DB連線並export 實例
index.js : 統整所有model,並建立model間的關聯
其他就是個別model宣告
event.js
order.js
contact.js

首先看event.js,這裡因為標籤其實只有在event中有使用,所以我就懶得另外定義Model

class Event {
    constructor() {
        this._modelEvent = connection.define("Event", {});
        this._modelTag = connection.define("Tag", {});
        
        // Event <=> Tag = n:m

        this._modelEvent.belongsToMany(this._modelTag, {
            through: "Event_Tag",
            foreignKey: 'event_id',
            timestamps: false
        });
        this._modelTag.belongsToMany(this._modelEvent, {
            through: "Event_Tag",
            foreignKey: 'tag_id',
            timestamps: false
        });
    }
}
module.exports = Event;

其他寫法類似,接著我會彙整所有model到index.js中

const Connection = require("./connection"),
    Event = require("./event"),
    Order = require("./order"),
    People = require("./people")
    
const model = {
    event: new Event(),
    order: new Order(),
    people: new People(),
    connection: Connection
}

model.event._modelEvent.hasOne(model.order._model, {
    foreignKey: "event_id"
});
model.order._model.belongsTo(model.event._modelEvent, {
    foreignKey: "event_id"
});

model.order._model.hasOne(model.people._modelContact, {
    foreignKey: "order_id"
});
model.people._modelContact.belongsTo(model.order._model, {
    foreignKey: "order_id"
});

module.exports = model

hasOne會將外鍵加在對方身上,belongsTo則是將外鍵加在自己身上,兩者同時提供 populate的方式,所以理論上只要呼叫其中一個方法就會創建好外鍵,但是兩者都呼叫才可以在後續調用方法時從兩邊都可以取得資料。

自定義中介Table

每筆訂單中可能包含多種票券,所以兩者是多對多關聯,而且必須在中介Table另外儲存購買了幾張票券

const OrderTicket = Connection.define('Order_Ticket', {
    _id: {
        type: Sequelize.BIGINT(),
        primaryKey: true,
        autoIncrement: true
    },
    order_id: {
        type: Sequelize.STRING(30),
    },
    ticket_id: {
        type: Sequelize.STRING(30),
    },
    count: {
        type: Sequelize.INTEGER,
        defaultValue: 0
    }
}, {
    freezeTableName: true
});
model.order._model.belongsToMany(model.ticket._model, {
    through: {
        model: OrderTicket,
        unique: false
    },
    foreignKey: 'order_id',
    constraints: false
});
model.ticket._model.belongsToMany(model.order._model, {
    through: {
        model: OrderTicket,
        unique: false
    },
    foreignKey: 'ticket_id',
    constraints: false
});

JOIN與Transaction

要透過Join一並取出關聯的資料花了很多時間才摸出來,一開始我以為可以直接宣告Table Name讓Sequelize自動Join結果發現是不行的OTZ,以下的方法是定義在個別model中

getOrderWithTickets({
        uid,
        transaction
    }) {
        const model = require("./index");
        return this._model.findOne({
            where: {
                uid
            },
            include: [{
                model: model.event._modelEvent,
                attributes: [....],
                include: [{
                    model: model.sysop._modelHost,
                    attributes: ["name", "email"]
                }]
            }, {
                model: model.ticket._model,
                through: {
                    attributes: ["count"]
                },
                attributes: []
            }],
            attributes: [
                "user",
                "uid",
                "event_id",
            ],
            transaction
        })
    }

這裡千萬注意 model中的foreign key一定要包含在attributes中否則Join永遠不會有資料出現,在此處也就是"event_id";
另外因為Sequelize必須傳入其他Model的Instance,所以我都會在方法的一開始才requre("index.js")避免circulate require;
最後,我在方法中都有留個transaction的參數,方法接收Object是我參考 Elegant patterns in modern JavaScript: RORO的寫法,套用後覺得蠻清爽的。

Transaction用法蠻簡單的,使用前先取得,接著在同個TX下傳入Sequelize的方法即可,記得要commit和rollback

try{
  transaction = await models.connection.transaction()

  await models.ticket.updateTicketAmount({
    uid: t.tix,
    count: +t.count,
    transaction
  })

  await transaction.commit()
}catch(){
    await transaction.rollback()
}
← 初探Neo4j - Graph Database [小技巧] Vue Component如何取得window中的object與同步更新 →
 
comments powered by Disqus