ตัวจัดการ connection pool (PoolManager)
ทุกส่วนที่ต้องคุยกับ PostgreSQL ของ Tenant (identity, query, blob, process, โมดูล) ขอ pool จาก PoolManager ตัวเดียวที่ di สร้างและส่งต่อ (สถาปัตยกรรม Hexagonal และ composition root ของ Go backend (Hexagonal Architecture + DI)). pool ถูกเก็บไว้ตามกุญแจ host|port|user|db และสร้างตอนใช้ครั้งแรก.
ทำงานอย่างไร
- ผู้เรียกประกอบ DSN ด้วย
db.BuildDSN(host, port, database, user, password, sslMode)(dsn.go:13): รูปแบบ key-value ที่ครอบทุกค่าด้วย'...'และ escape\กับ'(quoteConnValue,:29) จึงใส่อักขระพิเศษในรหัสผ่านได้โดยไม่ต้อง URL-encode;sslmodeว่าง =disable. - เรียก
pools.Get(ctx, databaseName, dsn)(pool.go:190):- แยก config ด้วย
pgxpool.ParseConfig; กุญแจ =PoolKey(host, port, user, db)โดย host และ db เป็นพิมพ์เล็ก ตัดช่องว่าง (pool.go:150-172) — Tenant ที่อยู่คนละเซิร์ฟเวอร์แต่ชื่อฐานเหมือนกันจึงไม่ชนกัน; ถ้าdatabaseNameว่างใช้ชื่อใน DSN. - ถ้ามี pool แล้วคืนทันที (อ่านด้วย
RWMutex). - ถ้ายังไม่มี ใช้
singleflightให้คำขอพร้อมกันของกุญแจเดียวกัน dial ครั้งเดียว (:176-228).
- แยก config ด้วย
- หลังเปิด pool ใหม่ ถ้าชื่อฐานข้อมูลไม่ใช่
postgres/smlmain(และไม่ว่าง) จะรันcompanySchemaSQL(pool.go:16, เรียกที่:216-218) ด้วย SimpleProtocol ผลถูกทิ้ง (_, _ =): สร้างตารางic_opposite_unit,erp_initial(ใส่คำนำหน้าชื่อ 9 แถวแบบON CONFLICT DO NOTHING) และตารางช่วยอื่น ๆ +ALTER TABLE IF EXISTS ... ADD COLUMN IF NOT EXISTS ...เพิ่มคอลัมน์ที่โค้ดรุ่นใหม่ต้องใช้ในตารางเดิม (เช่นic_trans_detail,ap_ar_transport_label) — เป็นกลไก "migrate เบา ๆ ตอนเปิด pool" แทนระบบ migration (ไฟล์ migration และการสร้างตารางตอนรัน (Migrations / runtime DDL)). CloseAll()(:232) ปิดและล้างทุก pool ตอนปิดเซอร์วิส (Container.Close, วงจรชีวิตเซอร์วิสและการรัน binary เดียวหลาย OS (Service lifecycle)).
ผู้ใช้ pool
| ผู้ใช้ | ฐานที่เปิด | หมายเหตุ |
|---|---|---|
outbound/identity |
Main DB smlerpmain<code> |
ผู้ใช้/บริษัท/สิทธิ์ (โมเดลผู้เช่าหลายราย: Tenant, Provider, Main DB, Company (Multi-tenant model)) |
dynamicsql.Gateway |
Company DB จาก claim | ชื่อฐานถูก ToLower ก่อน (gateway.go:171) (Query Gateway: /query/list, limit, batch, exec, execBatch) |
blobsql.Gateway |
Company DB จาก claim | แทรกแถวพร้อมรูปภาพ: POST /blob/insert + MinIO (Blob insert) |
| handler ของโมดูล / process | ตามที่ต้องใช้ | ดู ภาพรวมกลุ่ม process ของ Backend (Process overview) |
ค่าที่ทำให้ผลลัพธ์ปลอดภัยต่อเงิน
dynamicsql.NormalizeValue (gateway.go:26) แปลง pgtype.Numeric เป็นสตริงทศนิยมตรงตัว (หรือ "NaN", NULL → nil) ก่อน encode JSON เพื่อไม่ให้ตัวเลขเงินผ่าน float64 (สอดคล้องกฎ ACCOUNTING NUMBER).
ข้อควรระวัง
- ไม่มีการปล่อย pool ที่ไม่ใช้: จำนวน pool โตตามจำนวน (Tenant × ฐานข้อมูล) ที่ถูกเรียก จนกว่าจะรีสตาร์ต; ใน
pool.goไม่พบการตั้งMaxConns/อายุ connection จึงใช้ค่า default ของ pgx (ยังไม่ยืนยันค่าตัวเลข). companySchemaSQLถูกรันกับ ทุกฐานที่ชื่อไม่ใช่postgres/smlmainรวม Main DB ของ Tenant (smlerpmain<code>) ด้วย — มันสร้างตาราง/แถวตั้งต้นใน Main DB ด้วย (อนุมานจากเงื่อนไขpool.go:216).- ความล้มเหลวของสคริปต์ถูกกลืนเงียบ ๆ: pool ใช้งานได้แม้ schema ยังไม่ครบ.
- การรัน DDL ในจุดเปิด pool ขัดกับหลัก "ไม่มี migration runner" — โมดูลก็สร้างตารางเองตอนรันด้วย
AllowDDLเช่นกัน (สถาปัตยกรรม Hexagonal และ composition root ของ Go backend (Hexagonal Architecture + DI)). - รหัสผ่านอยู่ใน DSN ในหน่วยความจำเท่านั้น; ห้าม log DSN.
ที่มาในโค้ด
backend/internal/platform/db/pool.go:16-148—companySchemaSQL.backend/internal/platform/db/pool.go:150-239—PoolKey,PoolManager,Get,CloseAll.backend/internal/platform/db/dsn.go:13-34—BuildDSN.backend/internal/adapter/outbound/dynamicsql/gateway.go:26-51—NormalizeValue.backend/internal/di/container.go:65— สร้างPoolManager.
ดูเพิ่ม
ภาพรวม Backend Go ของ SML Ai Account (Backend overview) · Query Gateway: /query/list, limit, batch, exec, execBatch · โมเดลผู้เช่าหลายราย: Tenant, Provider, Main DB, Company (Multi-tenant model) · ไฟล์ migration และการสร้างตารางตอนรัน (Migrations / runtime DDL) · สารบัญ ADR ของ backend (Architecture Decision Records)