การเชื่อมต่อฐานข้อมูล
ORM landscape ของ Node.js เทียบกับ Go
หัวข้อที่มีชื่อว่า “ORM landscape ของ Node.js เทียบกับ Go”นักพัฒนา TypeScript คุ้นเคยกับการเลือกระหว่าง Prisma, TypeORM, Drizzle, Knex หรือ Sequelize ส่วน Go ก็มีตัวเลือกคล้าย ๆ กัน — GORM, sqlc, Bun, Ent — แต่ package database/sql ของ stdlib ก็เป็นตัวเลือกชั้นหนึ่งที่ควรรู้จักไว้
| สิ่งที่ต้องการ | ตัวเลือกใน Node.js | ตัวเลือกใน Go |
|---|---|---|
| Low-level | pg, mysql2 (raw driver) | database/sql + driver (เช่น lib/pq, pgx) |
| Query builder | knex | squirrel, raw database/sql |
| Full ORM | TypeORM, Drizzle, Sequelize | GORM, ent, bun |
| Codegen จาก SQL | — | sqlc (SQL → type-safe Go) |
บทนี้ใช้ database/sql กับ driver lib/pq สำหรับ PostgreSQL — เป็น baseline ที่ถ่ายโอนความรู้ได้มากที่สุด
เปิด connection pool
หัวข้อที่มีชื่อว่า “เปิด connection pool”// Node.js — pg connection poolimport { Pool } from 'pg';
const pool = new Pool({ connectionString: process.env.DATABASE_URL,});
export default pool;// Go — database/sql + lib/pqimport ( "database/sql" "log" "os"
_ "github.com/lib/pq" // ลงทะเบียน driver ผ่าน side-effect import)
func NewDB() *sql.DB { dsn := os.Getenv("DATABASE_URL") db, err := sql.Open("postgres", dsn) if err != nil { log.Fatalf("sql.Open: %v", err) } if err := db.Ping(); err != nil { log.Fatalf("db.Ping: %v", err) } db.SetMaxOpenConns(25) db.SetMaxIdleConns(5) return db}Store layer — repository pattern
หัวข้อที่มีชื่อว่า “Store layer — repository pattern”แทนที่จะเรียก db โดยตรงจาก handler ห่อ database logic ไว้ใน store struct ซึ่งเหมือน Repository pattern ใน NestJS และทำให้ handler เบาลง
// NestJS — repository injection@Injectable()export class BooksRepository { constructor( @InjectRepository(Book) private readonly repo: Repository<Book>, ) {}
async findById(id: string): Promise<Book | null> { return this.repo.findOneBy({ id }); }}// Go — store struct (manual DI)type BookStore struct { db *sql.DB}
func NewBookStore(db *sql.DB) *BookStore { return &BookStore{db: db}}
func (s *BookStore) FindByID(ctx context.Context, id string) (Book, error) { const q = `SELECT id, title, author, year FROM books WHERE id = $1` var b Book err := s.db.QueryRowContext(ctx, q, id).Scan( &b.ID, &b.Title, &b.Author, &b.Year, ) if errors.Is(err, sql.ErrNoRows) { return Book{}, fmt.Errorf("FindByID: %w", ErrNotFound) } if err != nil { return Book{}, fmt.Errorf("FindByID: %w", err) } return b, nil}Insert row
หัวข้อที่มีชื่อว่า “Insert row”ใช้ parameterized query ($1, $2, …) เสมอ ห้าม interpolate value เข้าไปใน SQL string โดยตรง — นั่นคือช่องโหว่ SQL injection
// Prisma insertconst book = await prisma.book.create({ data: { title, author, year },});// database/sql insert — parameterized queryfunc (s *BookStore) Insert(ctx context.Context, req CreateBookRequest) (Book, error) { const q = ` INSERT INTO books (title, author, year) VALUES ($1, $2, $3) RETURNING id, title, author, year ` var b Book err := s.db.QueryRowContext(ctx, q, req.Title, req.Author, req.Year, ).Scan(&b.ID, &b.Title, &b.Author, &b.Year) if err != nil { return Book{}, fmt.Errorf("Insert: %w", err) } return b, nil}เชื่อม store เข้า handler ผ่าน main.go
หัวข้อที่มีชื่อว่า “เชื่อม store เข้า handler ผ่าน main.go”db := NewDB()
bookStore := store.NewBookStore(db)bookHandler := handler.NewBookHandler(bookStore)
api := e.Group("/api")api.GET("/books/:id", bookHandler.Get)api.POST("/books", bookHandler.Create)ไม่ต้องมี DI container — type system ของ Go กับ constructor function จัดการให้ได้อย่างชัดเจน
ตัวเลือกระดับสูงอื่น ๆ
หัวข้อที่มีชื่อว่า “ตัวเลือกระดับสูงอื่น ๆ”- sqlc — เขียน SQL file ธรรมดา รัน
sqlc generateได้ฟังก์ชัน Go ที่ type-safe ไม่มี reflection overhead เหมาะกับ team ที่ต้องการควบคุม SQL - GORM — ORM แบบ ActiveRecord คุ้นเคยถ้ามาจาก Sequelize แต่เสียความโปร่งใสของ query บ้าง
- pgx/pgxpool — driver PostgreSQL ประสิทธิภาพสูง (เหนือกว่า
database/sql) แนะนำสำหรับ production patternQueryRowContext/Scanเหมือนกัน แต่ throughput ดีกว่า
รันบนเครื่องของคุณ — ต้องการ PostgreSQL จริง, driver module
lib/pqและ network port