80 phút
Database với GORM và Testing trong Go
GORM Setup
Cài đặt
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres
go get -u gorm.io/driver/sqlite
Kết nối database
package database
import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func Connect(dsn string) (*gorm.DB, error) {
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
return nil, err
}
return db, nil
}
Models và Migrations
Model definition
type User struct {
gorm.Model
Name string `gorm:"not null"`
Email string `gorm:"uniqueIndex;not null"`
Age int
Posts []Post
}
type Post struct {
gorm.Model
Title string `gorm:"not null"`
Content string
UserID uint
User User
}
AutoMigrate
db.AutoMigrate(&User{}, &Post{})
CRUD Operations
Create
user := User{Name: "John", Email: "john@example.com", Age: 30}
result := db.Create(&user)
fmt.Println(user.ID)
fmt.Println(result.Error)
fmt.Println(result.RowsAffected)
// Batch insert
users := []User{{Name: "A"}, {Name: "B"}}
db.Create(&users)
Read
// Get first
var user User
db.First(&user)
db.First(&user, 10) // WHERE id = 10
db.First(&user, "name = ?", "John")
// Get all
var users []User
db.Find(&users)
// Conditions
db.Where("age > ?", 18).Find(&users)
db.Where("name LIKE ?", "%oh%").Find(&users)
db.Where(&User{Name: "John", Age: 30}).Find(&users)
// Order, Limit, Offset
db.Order("age desc").Limit(10).Offset(0).Find(&users)
Update
// Update single field
db.Model(&user).Update("age", 31)
// Update multiple fields
db.Model(&user).Updates(User{Age: 31, Name: "Jane"})
db.Model(&user).Updates(map[string]interface{}{"age": 31})
// Update all
db.Model(&User{}).Where("age < ?", 18).Update("active", false)
// Save (all fields)
user.Age = 31
db.Save(&user)
Delete
db.Delete(&user, 10)
db.Where("age < ?", 18).Delete(&User{})
// Soft delete (với gorm.Model)
db.Delete(&user) // sets deleted_at
// Permanent
db.Unscoped().Delete(&user)
Relations
Has Many
var user User
db.Preload("Posts").First(&user, 1)
Belongs To
var post Post
db.Preload("User").First(&post, 1)
Many to Many
type Student struct {
gorm.Model
Name string
Courses []Course `gorm:"many2many:student_courses;"`
}
type Course struct {
gorm.Model
Name string
Students []Student `gorm:"many2many:student_courses;"`
}
Transactions
err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&User{Name: "A"}).Error; err != nil {
return err
}
if err := tx.Create(&Post{Title: "Post"}).Error; err != nil {
return err
}
return nil
})
Testing
Table-driven tests
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive", 2, 3, 5},
{"negative", -1, 1, 0},
{"zero", 0, 0, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Add(tt.a, tt.b)
if got != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, got, tt.expected)
}
})
}
}
Testify
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUserService(t *testing.T) {
user, err := service.Create("John", "john@example.com")
require.NoError(t, err)
assert.Equal(t, "John", user.Name)
assert.NotZero(t, user.ID)
}
In-memory database testing
import "gorm.io/driver/sqlite"
func setupTestDB(t *testing.T) *gorm.DB {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
db.AutoMigrate(&User{})
return db
}
func TestCreateUser(t *testing.T) {
db := setupTestDB(t)
repo := NewUserRepository(db)
user, err := repo.Create(User{Name: "John", Email: "j@e.com"})
require.NoError(t, err)
assert.NotZero(t, user.ID)
}
Mocking
type MockUserRepository struct {
mock.Mock
}
func (m *MockUserRepository) FindByID(id uint) (*User, error) {
args := m.Called(id)
return args.Get(0).(*User), args.Error(1)
}
Bài tập thực hành
Hãy viết tests cho REST API với GORM!