Skip to content

9.3 · Model patterns (convention)

ทุก Sequelize model ใน vtrc-api ใช้รูปแบบเดียวกันทุกประการ — ไม่มี config กลาง, ไม่มี base class, แค่ copy-paste pattern ที่ dev แรกเขียนแล้วคนต่อๆ มาทำตาม

บทนี้เล่า convention นั้น + จุดที่ pattern พัง


Pattern หลัก — export default function

javascript
export default (db, Sequelize) => {
  const MyModel = db.define('MyModel', { /* attributes */ }, { /* options */ })
  MyModel.associate = (models) => { /* relations */ }
  return MyModel
}

ลักษณะเฉพาะ:

  • รับ db (connection) + Sequelize (class) เป็นพารามิเตอร์ — ไม่ import เอง
  • เรียก db.define() เพื่อสร้าง model instance
  • (optional) เพิ่ม method associate เป็น property ของ model
  • return model กลับไปให้ auto-loader

Attribute patterns

1. UUID primary key + UUIDV4 default

4:9:vtrc-api/api/src/models/core/withdraw/hospital.js
    wdHospitalId: {
      primaryKey: true,
      type: Sequelize.UUID,
      defaultValue: Sequelize.UUIDV4,
      allowNull: false
    },

ทุก model ใช้ UUID เป็น PK + generate ฝั่งแอป (UUIDV4) — ไม่ใช้ auto-increment integer

ผลกระทบ:

  • PK เป็น string 36 ตัวอักษร → index ใหญ่กว่า int → query ช้ากว่าเล็กน้อย
  • รับ insert หลายๆ row พร้อมกันได้โดยไม่ตีกัน (good for batch)
  • ไม่รั่วจำนวน record (good for security — ไม่เดาได้ว่ามีกี่ user)

2. allowNull: true เกือบทุก column

อย่างเช่น LeaveDocument ทุก field ยกเว้น PK เป็น allowNull: true

46:49:vtrc-api/api/src/models/core/leave/leave.js
        empCode: {
            type: Sequelize.STRING,
            allowNull: true
        },

แม้แต่ field ที่ business บังคับ เช่น empCode ก็ยัง nullable — เพราะ validation อยู่ที่ controller layer, ไม่ใช่ DB constraint

ผลกระทบ:

  • DB ไม่บังคับ data quality — validation อยู่ที่แอปอย่างเดียว
  • ถ้ามี code path อื่นที่ write โดยตรง (เช่น raw query / migration manual) → ข้อมูล null ได้
  • audit ไม่ได้จาก schema — ต้องอ่าน controller ทุกตัวถึงรู้ว่า field ไหนบังคับ

3. ENUM สำหรับ status / state machine

58:60:vtrc-api/api/src/models/core/leave/leave.js
        status: {
            type: Sequelize.ENUM('PEDDING_CANCLE', 'PENDING', 'MOREINFO', 'APPROVE', 'REJECT', 'CANCEL'),
            allowNull: true
        },

หมายเหตุ — PEDDING_CANCLE มี typo ทั้งสองคำ (pending → PEDDING, cancel → CANCLE) แต่ติดอยู่ใน production schema ทุก consumer ต้องเรียกชื่อเดียวกัน

4. JSON-encoded field ที่มี getter/setter

19:28:vtrc-api/api/src/models/centralize/user/user.js
    role: {
      type: Sequelize.TEXT,
      allowNull: false,
      get: function () {
        return JSON.parse(this.getDataValue('role'))
      },
      set: function (value) {
        this.setDataValue('role', JSON.stringify(value))
      },
    },

role เก็บเป็น TEXT ใน DB แต่โหลดขึ้นมาได้เป็น object เลยเพราะ getter parse ให้

ปัญหา — RBAC ไม่ enforce relationally ทำให้ audit "ใครมี role อะไร" ต้อง parse JSON ทุก row

5. FLOAT สำหรับตัวเลขที่ไม่ใช่เงิน

38:41:vtrc-api/api/src/models/core/leave/leave.js
        leaveDay: {
            type: Sequelize.FLOAT,
            allowNull: true
        },

leaveDay (จำนวนวันลา) เป็น FLOAT — ถ้าลา 0.5 วันจะเก็บ 0.5 ได้ แต่ FLOAT มี precision error ทำให้เทียบ leaveDay === 0.5 อาจจะไม่ตรง

6. DECIMAL สำหรับเงิน

ฝั่ง welfare withdrawal ที่เป็นเงิน ใช้ DECIMAL(12,2) — ถูกต้องตามหลัก financial data

// WDSlip.amount เป็น DECIMAL(12,2) — รองรับยอดเงินถึง 999,999,999,999.99

Options patterns

1. tableName override

237:243:vtrc-api/api/src/models/core/withdraw/hospital.js
  }, {
    // options
    tableName: 'WDHospitals',
    timestamps: true,
    charset: 'utf8',
    collate: 'utf8_general_ci',
    indexes: [{ fields: ['status'] }]
  })
  • tableName: 'WDHospitals' — explicit, plural
  • timestamps: true — auto-manage createdAt / updatedAt
  • charset: 'utf8'ควรจะเป็น utf8mb4 แต่หลาย model ตั้ง utf8 ซึ่งไม่รองรับ emoji (มีบาง field เช่น note ที่อาจจะมี emoji ได้)
  • indexes: [{ fields: ['status'] }] — index เดียวที่ประกาศผ่าน Sequelize

2. inconsistency ของ charset

Modelcharsetcollate
WDHospitalutf8utf8_general_ci
Userutf8utf8_general_ci
LeaveDocumentutf8utf8_general_ci
connector defaultutf8mb4utf8mb4_unicode_ci

ตั้งแต่ connector คือ utf8mb4 แต่ model override เป็น utf8 — ทำให้ join ข้าม table อาจจะมี collation mismatch warning


Associate patterns

1. hasMany มาตรฐาตรา

248:248:vtrc-api/api/src/models/core/withdraw/hospital.js
    models.WDHospital.hasMany(models.WDSlip, { as: 'slip', foreignKey: 'wdHospitalId' })
  • as ใช้เวลา query include (include: [{ as: 'slip' }])
  • foreignKey ระบุชื่อ column ฝั่ง child

2. belongsTo มี alias หลายตัว

254:257:vtrc-api/api/src/models/core/withdraw/hospital.js
    models.WDHospital.belongsTo(models.Sickness, { as: "sicknessWithName", foreignKey: 'sicknessName', targetKey: 'sicknessId' })
    models.WDHospital.belongsTo(models.Sickness, { as: "sickness", foreignKey: 'sicknessId', targetKey: 'sicknessId' })
    models.WDHospital.belongsTo(models.Hospital, { as: "hospitalWithName", foreignKey: 'hospitalName', targetKey: 'hospitalId' })
    models.WDHospital.belongsTo(models.Hospital, { as: "hospital", foreignKey: 'hospitalId', targetKey: 'hospitalId' })

WDHospital reference Sickness สองทาง:

  • sickness — join ผ่าน sicknessId FK (relation ปกติ)
  • sicknessWithName — join ผ่าน sicknessName column ที่เป็น string ธรรมดา

เหตุผล — เป็น half-step migration เดิมใช้ sicknessName เป็น reference, ทีหลังเปลี่ยนเป็น sicknessId แต่ไม่ได้ลบของเก่าออก

3. targetKey สำหรับ relation ผ่าน column ไม่ใช่ PK

251:251:vtrc-api/api/src/models/core/withdraw/hospital.js
    models.WDHospital.hasMany(models.MasterApprover, { as: 'masterApprover', foreignKey: 'orgCode', targetKey: 'orgCode' })

MasterApprover.orgCode ไม่ใช่ PK ของ MasterApprover แต่ใช้เป็น join key — targetKey ระบุให้ Sequelize รู้


Anti-patterns ที่พบใน models

1. Duplicated columns

hospital.js มี column coaCodeName และ isJuristic declare ซ้ำสองครั้ง

// hospital.js
coaCodeName: { type: Sequelize.STRING, ... }   // บรรทัด 14
// ... 100 บรรทัดให้หลัง ...
coaCodeName: { type: Sequelize.STRING, ... }   // บรรทัด 138 (ซ้ำ)

Sequelize เก็บ declaration ตัวหลังเป็น winner — declaration แรกหายไปอย่างเงียบ ทำให้ code อ่านสับสน และถ้าทั้งสองมีค่าไม่เหมือนกัน → bug ที่หายาก

2. ชื่อตัวแปรกับตารางไม่ตรง

  • var WDHospital = db.define('WDHospital', ...) — model name = WDHospital
  • tableName: 'WDHospitals' — table = WDHospitals

var declaration ใช้ singular แต่ table เป็น plural — ส่วนใหญ่ไม่มีปัญหา แต่ตอนเขียน raw SQL ต้องระวัง

3. ใช้ var ไม่ใช่ const/let

ทุก model ใช้ var MyModel = db.define(...) — เป็น legacy จากยุค ES5 ปนกับ ES module ไม่กระทบฟังก์ชัน แต่ linter จะบ่น

4. Comment ลวกลาง (CETRANLIZE)

37:37:vtrc-api/api/src/models/index.js
//[CETRANLIZE] database center

typo ลามไปทั้ง codebase — ปกติไม่ fix เพราะอาจจะมี tool อื่น grep keyword นี้


Model lifecycle hooks (ไม่ค่อยใช้)

แทบไม่มี model ไหนใช้ Sequelize hooks (beforeCreate, afterUpdate, ...) — ทำให้ทุก side effect (เช่น audit log, timestamp ที่ custom) อยู่ใน controller layer

ผลกระทบ:

  • ถ้ามีหลาย path ที่ write model เดียวกัน (controller + raw query + migration) → side effect ไม่เกิด
  • audit ไม่สมบูรณ์ — เพราะ controller อาจจะลืม write audit log

เคล็ดลับตอนทำงานจริง

ถ้าจะเพิ่ม column ใหม่

  1. เพิ่มใน model definition (models/core/xxx/yyy.js)
  2. deploy → syncModelToTable() จะ ALTER TABLE ADD COLUMN อัตโนมัติ
  3. เช็กใน DB จริงว่า column ขึ้น

ห้าม ลบ column ผ่าน model — syncModelToTable() ใช้ { alter: true } บางครั้งอาจจะ DROP column ที่ไม่มีใน model ทิ้ง

ถ้าจะแก้ ENUM ค่า

  1. เพิ่มค่าใหม่ใน Sequelize.ENUM(...) ของ model
  2. deploy — Sequelize จะ ALTER TABLE MODIFY COLUMN enum
  3. อย่าลืมแก้ typeDefs และ controller ที่ consume enum

ถ้าเจอข้อมูล null ที่ไม่ควร null

อย่า trust DB constraint — เพราะทุก field nullable ให้ไป validate ที่ controller แทน

ถ้าจะ join ข้าม schema

db กับ db2 เป็น connection คนละตัว — Sequelize relation ข้ามกันไม่ได้ ต้อง query ทีละอันแล้ว merge ใน memory


ไป 8.4 Schema management + syncModelToTable