利用 Sequelize-cli 建立資料庫、設定關聯與寫入 seeder 測試資料


Posted by huiming on 2020-12-08

前言

最近在進行期末專案,我負責後端的實作,在設定資料庫關聯時踩了很多坑,不斷查資料、看文件,從一頭霧水到小有心得,因此整理成文章,方便之後回顧,如果發現有寫錯的,還請麻煩提醒我修正,感謝。

安裝 Sequelize、Sequelize-cli 與 mysql2

  npm install --save sequelize
  npm install --save mysql2
  npm install --save sequelize-cli

透過 Sequelize-cli 初始化

  npx sequlize-cli init
  • 這個步驟會建立以下資料夾
    1. config => config.js
      定義用於不同環境 (development & production) 的 DB 設定 (username, password, dialect ...)。
    2. models
      定義各個 table 欄位與關聯。
    3. migrations
      透過 sequelize 執行 migrate & undo 要執行的內容,等於告訴 DB 要建立什麼 table、有什麼欄位、資料型態等,migration 是有順序性的,要像 git 的 commit 一樣看待。
    4. seeders
      定義要寫入 DB 的測試資料,與 migration 一樣有順序性,先寫入存放原始資料的 table,再寫入有引用外鍵的 table。

定義 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;
      };
      

設定資料型態

  • 進入 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


#Sequelize #MTR04







Related Posts

JavaScript 是如何被執行的 (1)?

JavaScript 是如何被執行的 (1)?

[第十二周] 初探 fetch: 用 fetch 發送 get 與 post request

[第十二周] 初探 fetch: 用 fetch 發送 get 與 post request

ASP.NET Core Web API 入門教學 - 自訂模型資料驗證標籤

ASP.NET Core Web API 入門教學 - 自訂模型資料驗證標籤


Comments