9.3 · Model patterns (convention)
ทุก Sequelize model ใน vtrc-api ใช้รูปแบบเดียวกันทุกประการ — ไม่มี config กลาง, ไม่มี base class, แค่ copy-paste pattern ที่ dev แรกเขียนแล้วคนต่อๆ มาทำตาม
บทนี้เล่า convention นั้น + จุดที่ pattern พัง
Pattern หลัก — export default function
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
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
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
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
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 สำหรับตัวเลขที่ไม่ใช่เงิน
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.99Options patterns
1. tableName override
}, {
// options
tableName: 'WDHospitals',
timestamps: true,
charset: 'utf8',
collate: 'utf8_general_ci',
indexes: [{ fields: ['status'] }]
})tableName: 'WDHospitals'— explicit, pluraltimestamps: true— auto-managecreatedAt/updatedAtcharset: 'utf8'— ควรจะเป็นutf8mb4แต่หลาย model ตั้งutf8ซึ่งไม่รองรับ emoji (มีบาง field เช่น note ที่อาจจะมี emoji ได้)indexes: [{ fields: ['status'] }]— index เดียวที่ประกาศผ่าน Sequelize
2. inconsistency ของ charset
| Model | charset | collate |
|---|---|---|
WDHospital | utf8 | utf8_general_ci |
User | utf8 | utf8_general_ci |
LeaveDocument | utf8 | utf8_general_ci |
| connector default | utf8mb4 | utf8mb4_unicode_ci |
ตั้งแต่ connector คือ utf8mb4 แต่ model override เป็น utf8 — ทำให้ join ข้าม table อาจจะมี collation mismatch warning
Associate patterns
1. hasMany มาตรฐาตรา
models.WDHospital.hasMany(models.WDSlip, { as: 'slip', foreignKey: 'wdHospitalId' })asใช้เวลา query include (include: [{ as: 'slip' }])foreignKeyระบุชื่อ column ฝั่ง child
2. belongsTo มี alias หลายตัว
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 ผ่านsicknessIdFK (relation ปกติ)sicknessWithName— join ผ่านsicknessNamecolumn ที่เป็น string ธรรมดา
เหตุผล — เป็น half-step migration เดิมใช้ sicknessName เป็น reference, ทีหลังเปลี่ยนเป็น sicknessId แต่ไม่ได้ลบของเก่าออก
3. targetKey สำหรับ relation ผ่าน column ไม่ใช่ PK
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 =WDHospitaltableName: '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)
//[CETRANLIZE] database centertypo ลามไปทั้ง 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 ใหม่
- เพิ่มใน model definition (
models/core/xxx/yyy.js) - deploy →
syncModelToTable()จะALTER TABLE ADD COLUMNอัตโนมัติ - เช็กใน DB จริงว่า column ขึ้น
ห้าม ลบ column ผ่าน model — syncModelToTable() ใช้ { alter: true } บางครั้งอาจจะ DROP column ที่ไม่มีใน model ทิ้ง
ถ้าจะแก้ ENUM ค่า
- เพิ่มค่าใหม่ใน
Sequelize.ENUM(...)ของ model - deploy — Sequelize จะ
ALTER TABLE MODIFY COLUMNenum - อย่าลืมแก้ typeDefs และ controller ที่ consume enum
ถ้าเจอข้อมูล null ที่ไม่ควร null
อย่า trust DB constraint — เพราะทุก field nullable ให้ไป validate ที่ controller แทน
ถ้าจะ join ข้าม schema
db กับ db2 เป็น connection คนละตัว — Sequelize relation ข้ามกันไม่ได้ ต้อง query ทีละอันแล้ว merge ใน memory