前言
最近在進行期末專案,我負責後端的實作,在設定資料庫關聯時踩了很多坑,不斷查資料、看文件,從一頭霧水到小有心得,因此整理成文章,方便之後回顧,如果發現有寫錯的,還請麻煩提醒我修正,感謝。
安裝 Sequelize、Sequelize-cli 與 mysql2
npm install --save sequelize
npm install --save mysql2
npm install --save sequelize-cli
透過 Sequelize-cli 初始化
npx sequlize-cli init
- 這個步驟會建立以下資料夾
- config => config.js
定義用於不同環境 (development & production) 的 DB 設定 (username, password, dialect ...)。 - models
定義各個 table 欄位與關聯。 - migrations
透過 sequelize 執行 migrate & undo 要執行的內容,等於告訴 DB 要建立什麼 table、有什麼欄位、資料型態等,migration 是有順序性的,要像 git 的 commit 一樣看待。 - seeders
定義要寫入 DB 的測試資料,與 migration 一樣有順序性,先寫入存放原始資料的 table,再寫入有引用外鍵的 table。
- config => config.js
定義 model
- 範例:
npx sequelize-cli model:generate --name Address_city --attributes cityName:STRING npx sequelize-cli model:generate --name Order --attributes totalPrice:INTEGER,status:INTEGER npx sequelize-cli model:generate --name Address_district --attributes districtName:STRING npx sequelize-cli model:generate --name Recipient_info --attributes name:STRING,phone:STRING,email:STRING,address:STRING
- Name => table 名稱
- Attributes => 欄位
- 執行後會產生各 table 的 model & migration 檔案
- 這邊只能先定義基本資料型態,詳細設定要到 migration 調整
設定關聯
- 進入各 table 的 model 設定,用 hasOne, hasMany, belongsTo 建立關聯,引用的與被引用的 table 兩邊都要設定。
- 這邊有做關聯之後 ORM 才能 include 有關聯的 table 進來,option 可以指定外鍵名稱
foreignKey: 'orderId'
,這個兩邊都要設定,沒設定的話也會預設幫你建立OrderId
,但會變成大寫開頭。- model/order.js => 被引用的 table
'use strict'; const { Model } = require('sequelize'); module.exports = (sequelize, DataTypes) => { class Order extends Model { static associate(models) { // add associations here Order.hasOne( models.Recipient_info, { // 指定要關聯的 table foreignKey: 'orderId' // 指定的 foreignKey name }); Order.hasMany(models.Order_product); } }; Order.init({ totalPrice: DataTypes.INTEGER, status: DataTypes.INTEGER }, { sequelize, modelName: 'Order', }); return Order; };
- model/recipient_info.js => 引入外鍵的 table
'use strict'; const { Model } = require('sequelize'); module.exports = (sequelize, DataTypes) => { class Recipient_info extends Model { static associate(models) { // add associations here Recipient_info.belongsTo(models.Address_city, { foreignKey: 'cityId', }); Recipient_info.belongsTo(models.Address_district, { foreignKey: 'districtId', }); Recipient_info.belongsTo(models.Order, { foreignKey: 'orderId', }); } }; Recipient_info.init({ name: DataTypes.STRING, phone: DataTypes.STRING, email: DataTypes.STRING, address: DataTypes.STRING }, { sequelize, modelName: 'Recipient_info', }); return Recipient_info; };
- model/order.js => 被引用的 table
設定資料型態
- 進入 migration,定義欄位的資料型態,例如 string,後面加上括號填上數字限制 (例如 32),如同在資料庫裡設定資料格式為 varchar(32)。
- up & down 指的是 sequelize 要執行 migrate 或 undo 時各自要執行什麼操作。
- up: migrate => 建立 table、建立欄位等
- down: undo => 刪除 table
- 範例:
// migrations/recipient-info.js 'use strict'; module.exports = { up: async (queryInterface, Sequelize) => { await queryInterface.createTable('Recipient_infos', { id: { allowNull: false, autoIncrement: true, primaryKey: true, type: Sequelize.INTEGER }, name: { allowNull: false, type: Sequelize.STRING(64) }, phone: { allowNull: false, type: Sequelize.STRING(64) }, email: { allowNull: false, type: Sequelize.STRING(255) }, address: { allowNull: false, type: Sequelize.STRING(255) }, createdAt: { allowNull: false, type: Sequelize.DATE }, updatedAt: { allowNull: false, type: Sequelize.DATE } }); }, down: async (queryInterface, Sequelize) => { await queryInterface.dropTable('Recipient_infos'); } };
建立專門產生關聯的 migration
參考 這篇 的建議,把關聯的設定集中在一個 migration 管理,最好是建立 table 的 migration 都建好後再建立關聯,不然設定外鍵約束時容易出錯。
npx sequelize-cli migration:generate --name add-associations
定義要引用的 table、欄位、外鍵欄位名稱、外鍵約束類型
'use strict'; module.exports = { up: async (queryInterface, Sequelize) => { await queryInterface.addColumn( 'Address_districts', // 要添加外鍵的 table 'cityId', // 外鍵欄位名稱 { type: Sequelize.INTEGER, allowNull: false, references: { model: 'Address_cities', // 引用的 table key: 'id', // 引用的欄位 }, onUpdate: 'CASCADE', // 外鍵約束 onDelete: 'RESTRICT', // 這邊如果用 SET NULL 會與上面 allowNull: false 衝突,所以用 RESTRICT } );
執行 migrate 建立資料庫
npx sequelize-cli db:migrate
寫入 seeder 測試資料
先建立寫入檔,執行後在 seeders 資料夾會產生一個 demo-order 的檔案
npx sequelize-cli seed:generate --name demo-order
開啟後長這樣
// seeders/demo-recipient_info 'use strict'; const { recipientList } = require('./seederData/recipient-info'); module.exports = { up: async (queryInterface, Sequelize) => { await queryInterface.bulkInsert( 'Recipient_infos', // 要寫入的 table recipientList // 資料 [{}, {}, ...] ); }, down: async (queryInterface, Sequelize) => { return queryInterface.bulkDelete('Recipient_infos', null, {}); } };
- 在 bulkInsert 接收 2 個參數:要寫入的 table 名稱、要寫入的資料 (陣列)
- 另外再開一個 seederData/order.js 存放原始資料,再輸出成要放入 table 的格式
寫入資料
npx seqelize-cli db:seeder:all
實際測試
- GET
/recipients/1
時會拿到 id 為 1 的收件人資料 include 可以從關聯的 table 把資料帶出來
// controllers/recipient.js const db = require('../models'); const { Recipient_info, Order, Address_city, Address_district } = db; const recipientController = { getOne: (req, res) => { const id = req.params.id; Recipient_info.findOne({ where: { id }, include: [Order, Address_city, Address_district] }) .then((addresse) => { res.status(200).json(addresse); }) .catch(err => { console.log(err); }); }, } module.exports = recipientController;
response
把資料庫拆掉重建
npx sequelize-cli db:migrate:undo:all
- 如果前面有注意 migration 的順序,拆掉就不會有問題 (seeder 資料也可以 undo)。
- undo 的時候如果出錯通常是因為先前加上的外鍵約束規則 (Foreign key constraint),就要手動去資料庫把 table 刪除,然後把 migration 刪除再依順序重新建立。
總結
以上就是如何用 Sequelize-cli 建立資料庫、設定關聯與寫入測試資料的流程,如果錯誤的地方請不吝指教,謝謝收看。
參考文章
Manual | Sequelize - Migrations
How to define Sequelize associations using migrations