ข้ามไปยังเนื้อหา

การเชื่อมต่อฐานข้อมูล

นักพัฒนา TypeScript คุ้นเคยกับการเลือกระหว่าง Prisma, TypeORM, Drizzle, Knex หรือ Sequelize ส่วน Go ก็มีตัวเลือกคล้าย ๆ กัน — GORM, sqlc, Bun, Ent — แต่ package database/sql ของ stdlib ก็เป็นตัวเลือกชั้นหนึ่งที่ควรรู้จักไว้

สิ่งที่ต้องการตัวเลือกใน Node.jsตัวเลือกใน Go
Low-levelpg, mysql2 (raw driver)database/sql + driver (เช่น lib/pq, pgx)
Query builderknexsquirrel, raw database/sql
Full ORMTypeORM, Drizzle, SequelizeGORM, ent, bun
Codegen จาก SQLsqlc (SQL → type-safe Go)

บทนี้ใช้ database/sql กับ driver lib/pq สำหรับ PostgreSQL — เป็น baseline ที่ถ่ายโอนความรู้ได้มากที่สุด

TypeScript
// Node.js — pg connection pool
import { Pool } from 'pg';
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
export default pool;
Go
// Go — database/sql + lib/pq
import (
"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
}

แทนที่จะเรียก db โดยตรงจาก handler ห่อ database logic ไว้ใน store struct ซึ่งเหมือน Repository pattern ใน NestJS และทำให้ handler เบาลง

TypeScript
// 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
// 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
}

ใช้ parameterized query ($1, $2, …) เสมอ ห้าม interpolate value เข้าไปใน SQL string โดยตรง — นั่นคือช่องโหว่ SQL injection

TypeScript
// Prisma insert
const book = await prisma.book.create({
data: { title, author, year },
});
Go
// database/sql insert — parameterized query
func (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
}
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 pattern QueryRowContext / Scan เหมือนกัน แต่ throughput ดีกว่า

รันบนเครื่องของคุณ — ต้องการ PostgreSQL จริง, driver module lib/pq และ network port

ทำไมต้องใช้ parameterized query ($1, $2) แทน string interpolation ใน SQL?
sql.QueryRowContext คืน error ใดเมื่อไม่มีแถวที่ตรงกัน?
ทำไมต้องใช้ method เวอร์ชันที่รับ Context ของ database/sql (เช่น QueryContext)?