Database


Tôi sẽ giải thích chi tiết cách làm việc với database trong Golang, bao gồm kết nối bằng database/sql, sử dụng ORM (GORM), quản lý transaction (Begin, Commit, Rollback), và tối ưu hóa truy vấn với indexing. Mỗi phần sẽ có ví dụ thực tế và best practices.


1. Kết nối Database với database/sql và ORM (GORM)

database/sql - Kết nối cơ bản

  • Package database/sql là cách chuẩn để làm việc với SQL trong Go, hỗ trợ nhiều driver (MySQL, PostgreSQL, SQLite, v.v.).

  • Cần import driver tương ứng (ví dụ: github.com/go-sql-driver/mysql).

Ví dụ với MySQL:

package main

import (
    "database/sql"
    "fmt"
    _ "github.com/go-sql-driver/mysql" // Driver MySQL
)

func main() {
    // DSN: "user:password@tcp(host:port)/dbname"
    dsn := "root:password@tcp(127.0.0.1:3306)/testdb?parseTime=true"
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        panic(err)
    }
    defer db.Close()

    // Kiểm tra kết nối
    err = db.Ping()
    if err != nil {
        panic(err)
    }
    fmt.Println("Connected to database!")

    // Ví dụ truy vấn
    rows, err := db.Query("SELECT id, name FROM users")
    if err != nil {
        panic(err)
    }
    defer rows.Close()

    for rows.Next() {
        var id int
        var name string
        if err := rows.Scan(&id, &name); err != nil {
            panic(err)
        }
        fmt.Printf("ID: %d, Name: %s\n", id, name)
    }
}

Best Practices với database/sql:

  • Connection Pooling: sql.Open không thực sự mở kết nối mà tạo một pool. Dùng db.SetMaxOpenConns(n) để giới hạn số kết nối.

  • Error Handling: Luôn kiểm tra lỗi sau mỗi thao tác (Ping, Query, Exec).

  • Prepared Statements: Dùng db.Prepare để tránh SQL injection và tăng hiệu suất.

GORM - ORM trong Golang

  • GORM (gorm.io/gorm) là một ORM phổ biến, giúp đơn giản hóa việc ánh xạ struct Go với bảng database.

Ví dụ với GORM (MySQL):

package main

import (
    "fmt"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

type User struct {
    ID    uint   `gorm:"primaryKey"`
    Name  string `gorm:"type:varchar(100)"`
    Email string `gorm:"unique"`
}

func main() {
    dsn := "root:password@tcp(127.0.0.1:3306)/testdb?charset=utf8mb4&parseTime=True&loc=Local"
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        panic(err)
    }

    // Auto-migrate để tạo bảng
    db.AutoMigrate(&User{})

    // Thêm dữ liệu
    user := User{Name: "John", Email: "john@example.com"}
    db.Create(&user)

    // Truy vấn
    var foundUser User
    db.First(&foundUser, "email = ?", "john@example.com")
    fmt.Printf("Found: %+v\n", foundUser)
}

Best Practices với GORM:

  • Tag: Dùng tag gorm:"..." để định nghĩa ràng buộc (primary key, unique, v.v.).

  • Preloading: Dùng db.Preload("Relation") để tải quan hệ (associations).

  • Debug: Dùng db.Debug() để log SQL trong quá trình phát triển.

So sánh:

  • database/sql: Linh hoạt, nhẹ, nhưng cần viết nhiều code thủ công.

  • GORM: Tiện lợi, phù hợp với ứng dụng CRUD nhanh, nhưng nặng hơn và có thể khó tối ưu truy vấn phức tạp.


2. Transaction (Begin, Commit, Rollback)

Transaction với database/sql

  • Transaction đảm bảo tính toàn vẹn dữ liệu (atomicity) khi thực hiện nhiều thao tác.

Ví dụ:

package main

import (
    "database/sql"
    "fmt"
    _ "github.com/go-sql-driver/mysql"
)

func transferMoney(db *sql.DB, fromID, toID, amount int) error {
    tx, err := db.Begin()
    if err != nil {
        return err
    }

    // Rollback nếu có lỗi
    defer func() {
        if err != nil {
            tx.Rollback()
            return
        }
        err = tx.Commit()
    }()

    // Trừ tiền từ tài khoản nguồn
    _, err = tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, fromID)
    if err != nil {
        return err
    }

    // Cộng tiền vào tài khoản đích
    _, err = tx.Exec("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, toID)
    if err != nil {
        return err
    }

    return nil
}

func main() {
    db, err := sql.Open("mysql", "root:password@tcp(127.0.0.1:3306)/testdb")
    if err != nil {
        panic(err)
    }
    defer db.Close()

    err = transferMoney(db, 1, 2, 100)
    if err != nil {
        fmt.Println("Transfer failed:", err)
    } else {
        fmt.Println("Transfer successful!")
    }
}

Transaction với GORM

  • GORM hỗ trợ transaction qua db.Transaction() hoặc db.Begin().

Ví dụ:

package main

import (
    "fmt"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

type Account struct {
    ID      uint `gorm:"primaryKey"`
    Balance int
}

func transferMoneyGORM(db *gorm.DB, fromID, toID, amount int) error {
    return db.Transaction(func(tx *gorm.DB) error {
        var fromAcc Account
        if err := tx.First(&fromAcc, fromID).Error; err != nil {
            return err
        }
        if fromAcc.Balance < amount {
            return fmt.Errorf("insufficient balance")
        }

        // Trừ tiền
        if err := tx.Model(&Account{}).Where("id = ?", fromID).Update("balance", fromAcc.Balance-amount).Error; err != nil {
            return err
        }

        // Cộng tiền
        if err := tx.Model(&Account{}).Where("id = ?", toID).Update("balance", gorm.Expr("balance + ?", amount)).Error; err != nil {
            return err
        }

        return nil
    })
}

func main() {
    db, err := gorm.Open(mysql.Open("root:password@tcp(127.0.0.1:3306)/testdb"), &gorm.Config{})
    if err != nil {
        panic(err)
    }
    db.AutoMigrate(&Account{})

    err = transferMoneyGORM(db, 1, 2, 100)
    if err != nil {
        fmt.Println("Transfer failed:", err)
    } else {
        fmt.Println("Transfer successful!")
    }
}

Best Practices:

  • Defer Rollback: Với database/sql, dùng defer để đảm bảo rollback nếu lỗi.

  • Kiểm tra điều kiện: Validate trước khi thực hiện transaction (ví dụ: đủ số dư).

  • Giới hạn phạm vi: Chỉ dùng transaction cho các thao tác cần atomicity.


3. Indexing & Query Optimization

Indexing

  • Index tăng tốc độ truy vấn bằng cách giảm số hàng cần quét.

  • Các loại index:

    • Primary Key: Tự động có index.

    • Unique Index: Đảm bảo giá trị duy nhất (ví dụ: email).

    • Composite Index: Kết hợp nhiều cột (ví dụ: name + created_at).

Tạo Index với SQL:

CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_orders_user_date ON orders(user_id, created_at);

Với GORM:

type User struct {
    ID    uint   `gorm:"primaryKey"`
    Name  string
    Email string `gorm:"index:idx_email,unique"` // Index trên email
}

type Order struct {
    ID        uint `gorm:"primaryKey"`
    UserID    uint
    CreatedAt time.Time `gorm:"index:idx_user_date,composite:user_id"` // Composite index
}

Query Optimization

  • Giảm quét toàn bảng: Dùng WHERE với cột có index.

  • **Tránh SELECT * **: Chỉ lấy cột cần thiết.

  • Sử dụng EXPLAIN: Phân tích kế hoạch truy vấn.

Ví dụ tối ưu với database/sql:

// Truy vấn không tối ưu
rows, err := db.Query("SELECT * FROM users WHERE name LIKE '%john%'")
// Tối ưu: Dùng index trên email
rows, err := db.Query("SELECT id, name FROM users WHERE email = ?", "john@example.com")

Với GORM:

// Không tối ưu
var users []User
db.Where("name LIKE ?", "%john%").Find(&users)

// Tối ưu
var user User
db.Select("id", "name").Where("email = ?", "john@example.com").First(&user)

Phân tích với EXPLAIN:

  • Chạy EXPLAIN SELECT ... trong MySQL/PostgreSQL để xem kế hoạch truy vấn:

EXPLAIN SELECT id, name FROM users WHERE email = 'john@example.com';
  • Kiểm tra key (index được dùng) và rows (số hàng quét).

Best Practices:

  • Index cẩn thận: Quá nhiều index làm chậm INSERT/UPDATE.

  • Profile truy vấn: Dùng db.Debug() (GORM) hoặc EXPLAIN để tìm bottleneck.

  • Batch Processing: Với dữ liệu lớn, chia nhỏ truy vấn (ví dụ: LIMIT 1000 OFFSET 0).


Tổng kết

  • database/sql: Linh hoạt, nhẹ, phù hợp với truy vấn phức tạp.

  • GORM: Tiện lợi, nhanh cho CRUD, nhưng cần cẩn thận với hiệu suất.

  • Transaction: Đảm bảo atomicity với Begin/Commit/Rollback.

  • Indexing & Optimization: Tăng tốc truy vấn bằng index và tối ưu SQL.

Last updated