این جزوه به بررسی جامع و تخصصی GORM، کتابخانه ORM محبوب برای Go، میپردازد. GORM به دلیل انعطافپذیری، پشتیبانی از دیتابیسهای مختلف، و قابلیتهای پیشرفته، یکی از بهترین ابزارها برای مدیریت دیتابیس در Go است. تمام موضوعات درخواستی با جزئیات کامل، مثالهای عملی، و نکات پیشرفته شرح داده شدهاند.
مدلها در GORM به صورت struct تعریف میشوند و با استفاده از برچسبها (tags) به جداول دیتابیس نگاشت میشوند.
package models
import (
"time"
"gorm.io/gorm"
)
// User مدل کاربر با فیلدهای مختلف
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(100);not null"`
Email string `gorm:"type:varchar(100);uniqueIndex"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"` // برای Soft Delete
}
primaryKey
: کلید اصلی.type:varchar(100)
: نوع ستون در دیتابیس.uniqueIndex
: ایندکس منحصربهفرد.autoCreateTime
و autoUpdateTime
: بهروزرسانی خودکار زمان.gorm.DeletedAt
: پشتیبانی از Soft Delete.GORM از انواع دادههای سفارشی، JSON، و Enum پشتیبانی میکند.
package models
import (
"database/sql/driver"
"encoding/json"
"errors"
"gorm.io/gorm"
)
type JSONMap map[string]interface{}
// Value برای تبدیل به دیتابیس
func (j JSONMap) Value() (driver.Value, error) {
return json.Marshal(j)
}
// Scan برای خواندن از دیتابیس
func (j *JSONMap) Scan(value interface{}) error {
bytes, ok := value.([]byte)
if !ok {
return errors.New("type assertion to []byte failed")
}
return json.Unmarshal(bytes, j)
}
type Profile struct {
ID uint `gorm:"primaryKey"`
UserID uint `gorm:"index"`
Settings JSONMap `gorm:"type:jsonb"` // برای Postgres
}
package models
import "gorm.io/gorm"
type Role string
const (
Admin Role = "admin"
User Role = "user"
Guest Role = "guest"
)
type User struct {
ID uint `gorm:"primaryKey"`
Role Role `gorm:"type:varchar(20);default:'user'"`
}
برچسبها (tags) برای کنترل دقیق رفتار مدلها استفاده میشوند.
type Order struct {
ID uint `gorm:"primaryKey"`
UserID uint `gorm:"index;foreignKey:UserID;references:ID"` // کلید خارجی
Amount int `gorm:"not null;check:amount >= 0"` // شرط
Status string `gorm:"type:varchar(50);default:'pending'"`
UniqueCode string `gorm:"uniqueIndex:idx_code"` // ایندکس منحصربهفرد
}
GORM از structهای تو در تو برای مدلسازی روابط پیچیده پشتیبانی میکند.
type BaseModel struct {
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"`
}
type User struct {
BaseModel
ID uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(100)"`
}
type Admin struct {
User
Permissions JSONMap `gorm:"type:jsonb"`
}
BaseModel
فیلدهای مشترک را تعریف میکند.Admin
از User
ارثبری میکند و فیلدهای اضافی دارد.func (User) TableName() string {
return "app_users"
}
validator
برای اعتبارسنجی مدلها استفاده کنید:
type User struct {
Name string `gorm:"type:varchar(100)" validate:"required,min=3"`
}
type Comment struct {
ID uint
Content string
CommentableID uint `gorm:"index"`
CommentableType string `gorm:"index"`
}
GORM از شرطهای پیچیده با روشهای مختلف پشتیبانی میکند.
var users []User
db.Where("name LIKE ? AND role = ?", "%Ali%", "admin").
Or("email LIKE ?", "%@example.com").
Find(&users)
db.Where(map[string]interface{}{
"role": "admin",
"age": 30,
}).Find(&users)
db.Where(&User{Role: "admin"}).Find(&users)
برای ساخت کوئریهای پویا از Scopes
استفاده کنید.
func WithName(name string) func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if name != "" {
return db.Where("name LIKE ?", "%"+name+"%")
}
return db
}
}
func WithRole(role string) func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if role != "" {
return db.Where("role = ?", role)
}
return db
}
}
var users []User
db.Scopes(WithName("Ali"), WithRole("admin")).Find(&users)
برای بارگذاری روابط:
type User struct {
ID uint
Name string
Orders []Order `gorm:"foreignKey:UserID"`
}
var user User
db.Preload("Orders").First(&user, 1)
برای کوئریهای پیچیده:
var results []struct {
UserName string
OrderID uint
}
db.Model(&User{}).
Select("users.name AS user_name, orders.id AS order_id").
Joins("LEFT JOIN orders ON orders.user_id = users.id").
Scan(&results)
users := []User{
{Name: "Ali", Email: "ali@example.com"},
{Name: "Bob", Email: "bob@example.com"},
}
db.CreateInBatches(users, 100) // دستههای 100 تایی
db.Where("status = ?", "pending").FindInBatches(&users, 100, func(tx *gorm.DB, batch int) error {
for i := range users {
users[i].Status = "processed"
}
return tx.Save(&users).Error
})
GORM به طور داخلی از Soft Delete پشتیبانی میکند.
// حذف نرم
db.Delete(&user)
// بازیابی با حذفشدهها
db.Unscoped().Find(&users)
// حذف دائم
db.Unscoped().Delete(&user)
err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&User{Name: "Ali"}).Error; err != nil {
return err
}
if err := tx.Create(&Order{UserID: 1, Amount: 100}).Error; err != nil {
return err
}
return nil
})
db.Transaction(func(tx *gorm.DB) error {
tx.Create(&User{Name: "Ali"})
tx.SavePoint("sp1")
tx.Create(&Order{UserID: 1, Amount: 100})
if someCondition {
tx.RollbackTo("sp1")
}
return nil
})
db.Select("id, name").Find(&users)
db.Raw("SELECT * FROM users WHERE age > ?", 30).Scan(&users)
type User struct {
ID uint
Version int `gorm:"default:1"`
}
db.Clauses(clause.Locking{Strength: "UPDATE"}).Find(&user)
Preload
برای روابط که باعث N+1 Query Problem میشود.Repository Pattern لایهای بین منطق کسبوکار و دیتابیس ایجاد میکند.
package repository
import "github.com/username/app/models"
type UserRepository interface {
Create(user *models.User) error
FindByID(id uint) (*models.User, error)
FindAll() ([]models.User, error)
}
package repository
import (
"gorm.io/gorm"
"github.com/username/app/models"
)
type GORMUserRepository struct {
db *gorm.DB
}
func NewGORMUserRepository(db *gorm.DB) *GORMUserRepository {
return &GORMUserRepository{db: db}
}
func (r *GORMUserRepository) Create(user *models.User) error {
return r.db.Create(user).Error
}
func (r *GORMUserRepository) FindByID(id uint) (*models.User, error) {
var user models.User
err := r.db.First(&user, id).Error
return &user, err
}
func (r *GORMUserRepository) FindAll() ([]models.User, error) {
var users []models.User
err := r.db.Find(&users).Error
return users, err
}
package service
import (
"github.com/username/app/models"
"github.com/username/app/repository"
)
type UserService struct {
repo repository.UserRepository
}
func NewUserService(repo repository.UserRepository) *UserService {
return &UserService{repo: repo}
}
func (s *UserService) CreateUser(name string) error {
user := &models.User{Name: name}
return s.repo.Create(user)
}
type UserReader interface {
FindByID(id uint) (*models.User, error)
}
type UserWriter interface {
Create(user *models.User) error
}
func main() {
db, _ := gorm.Open(postgres.Open("..."), &gorm.Config{})
repo := repository.NewGORMUserRepository(db)
service := service.NewUserService(repo)
}
برای تستپذیری، مدلها را از دیتابیس جدا کنید:
package models
type User struct {
ID uint
Name string
}
// Validate برای اعتبارسنجی
func (u *User) Validate() error {
if u.Name == "" {
return errors.New("name is required")
}
return nil
}
type UnitOfWork struct {
db *gorm.DB
UserRepo repository.UserRepository
}
func (u *UnitOfWork) Commit() error {
return u.db.Commit().Error
}
type UserQueryService struct {
db *gorm.DB
}
func (s *UserQueryService) FindByName(name string) ([]models.User, error) {
var users []models.User
return users, s.db.Where("name LIKE ?", "%"+name+"%").Find(&users).Error
}
db.AutoMigrate(&User{}, &Order{})
golang-migrate
یا atlas
.go get -u github.com/golang-migrate/migrate/v4
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL
);
DROP TABLE users;
package main
import (
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
)
func main() {
m, err := migrate.New("file://migrations", "postgres://user:secret@localhost:5432/mydb?sslmode=disable")
if err != nil {
log.Fatal(err)
}
if err := m.Up(); err != nil {
log.Fatal(err)
}
}
202505170001_create_users.sql
).atlas
برای بررسی تفاوتهای اسکیما:
atlas schema diff --from postgres://... --to file://schema.sql
name: Database Migration
on:
push:
branches: [main]
jobs:
migrate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run migrations
run: |
go install github.com/golang-migrate/migrate/v4/cmd/migrate@latest
migrate -path migrations -database "postgres://..." up
func TestMigration(t *testing.T) {
m, _ := migrate.New("file://migrations", "postgres://...")
if err := m.Up(); err != nil {
t.Fatal(err)
}
defer m.Down()
// تست مدلها
}
if err := m.Steps(-1); err != nil {
log.Fatal(err)
}
db.Where("settings->>'key' = ?", "value").Find(&profiles)
db.Raw("SELECT * FROM users WHERE to_tsvector(name) @@ to_tsquery(?)", "Ali").Scan(&users)
db.Set("gorm:table_options", "ENGINE=InnoDB").AutoMigrate(&User{})
db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
sqlDB, _ := db.DB()
sqlDB.SetMaxOpenConns(100)
sqlDB.SetMaxIdleConns(10)
sqlDB.SetConnMaxLifetime(time.Hour)
db, _ := gorm.Open(postgres.New(postgres.Config{
DSN: "user=postgres password=secret dbname=mydb sslmode=disable",
}), &gorm.Config{
QueryFields: true,
ConnMaxLifetime: 30 * time.Minute,
})
PgBouncer
یا تنظیمات Read Replica:
db, _ := gorm.Open(postgres.New(postgres.Config{
DSN: "host=primary,secondary user=postgres password=secret dbname=mydb",
}))
readDB, _ := gorm.Open(postgres.New(postgres.Config{DSN: "host=read-replica ..."}))
writeDB, _ := gorm.Open(postgres.New(postgres.Config{DSN: "host=primary ..."}))
TableName
پویا:
func (User) TableName() string {
return fmt.Sprintf("users_shard_%d", userID%10)
}
db.Set("gorm:table_options", "SCHEMA tenant_1").AutoMigrate(&User{})
sqlDB, _ := db.DB()
if err := sqlDB.Ping(); err != nil {
log.Fatal("Database unreachable")
}
type User struct {
ID uint `gorm:"primaryKey"`
Email string `gorm:"uniqueIndex"`
Name string `gorm:"index:idx_name"`
}
type Order struct {
UserID uint `gorm:"index:idx_user_status,priority:1"`
Status string `gorm:"index:idx_user_status,priority:2"`
}
db, _ := gorm.Open(postgres.Open("..."), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
var explain string
db.Raw("EXPLAIN ANALYZE SELECT * FROM users WHERE name = ?", "Ali").Scan(&explain)
fmt.Println(explain)
type CustomLogger struct {
logger.Logger
}
func (l *CustomLogger) LogMode(level logger.LogLevel) logger.Interface {
newLogger := *l
newLogger.Logger = l.Logger.LogMode(level)
return &newLogger
}
func (l *CustomLogger) Info(ctx context.Context, msg string, data ...interface{}) {
log.Printf("[GORM] %s %v", msg, data)
}
db, _ := gorm.Open(postgres.Open("..."), &gorm.Config{
Logger: &CustomLogger{},
})
import "github.com/go-redis/redis/v8"
func GetUser(ctx context.Context, db *gorm.DB, client *redis.Client, id uint) (*User, error) {
cacheKey := fmt.Sprintf("user:%d", id)
cached, err := client.Get(ctx, cacheKey).Result()
if err == nil {
var user User
json.Unmarshal([]byte(cached), &user)
return &user, nil
}
var user User
if err := db.First(&user, id).Error; err != nil {
return nil, err
}
data, _ := json.Marshal(user)
client.Set(ctx, cacheKey, data, 10*time.Minute)
return &user, nil
}
import "github.com/prometheus/client_golang/prometheus"
var queryDuration = prometheus.NewHistogram(prometheus.HistogramOpts{
Name: "gorm_query_duration_seconds",
Help: "Duration of GORM queries",
})
func init() {
prometheus.MustRegister(queryDuration)
}
db.Callback().Query().After("gorm:query").Register("prometheus", func(d *gorm.DB) {
queryDuration.Observe(float64(d.Statement.SQL.String()))
})
db.CreateInBatches(users, 500) // تست برای یافتن اندازه بهینه
Preload
یا Joins
استفاده کنید.db.Set("gorm:query_option", "INDEX (idx_name)").Find(&users)
همیشه از پارامترهای باندشده استفاده کنید:
db.Where("name = ?", userInput).Find(&users) // ایمن
db.Raw("SELECT * FROM users WHERE name = '" + userInput + "'") // ناایمن
func GetUserData(db *gorm.DB, userID uint) ([]Data, error) {
var data []Data
return data, db.Where("user_id = ?", userID).Find(&data).Error
}
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
CREATE POLICY user_access ON users USING (id = current_user_id());
type AuditLog struct {
ID uint
TableName string
RecordID uint
Action string
Data JSONMap `gorm:"type:jsonb"`
CreatedAt time.Time
}
func LogAudit(db *gorm.DB, table string, recordID uint, action string, data interface{}) error {
logEntry := AuditLog{
TableName: table,
RecordID: recordID,
Action: action,
Data: JSONMap(data),
CreatedAt: time.Now(),
}
return db.Create(&logEntry).Error
}
type User struct {
ID uint
EncryptedData string `gorm:"type:text"`
}
func Encrypt(data string) string {
// استفاده از پکیج crypto
return encrypted
}
db.Set("gorm:prepare_stmt", true).Find(&users)
استفاده از پکیج go-sqlmock
:
package repository
import (
"testing"
"github.com/DATA-DOG/go-sqlmock"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func TestGORMUserRepository_FindByID(t *testing.T) {
db, mock, _ := sqlmock.New()
gormDB, _ := gorm.Open(postgres.New(postgres.Config{Conn: db}), &gorm.Config{})
repo := NewGORMUserRepository(gormDB)
mock.ExpectQuery(`SELECT * FROM "users" WHERE id = \$1`).
WithArgs(1).
WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(1, "Ali"))
user, err := repo.FindByID(1)
if err != nil || user.Name != "Ali" {
t.Errorf("Expected user Ali, got %v", user)
}
}
version: '3'
services:
postgres:
image: postgres:14
environment:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: test
ports:
- "5432:5432"
func TestIntegration(t *testing.T) {
db, _ := gorm.Open(postgres.Open("host=localhost user=test password=test dbname=test"), &gorm.Config{})
db.AutoMigrate(&User{})
repo := NewGORMUserRepository(db)
user := &User{Name: "Ali"}
repo.Create(user)
found, _ := repo.FindByID(user.ID)
if found.Name != "Ali" {
t.Errorf("Expected Ali, got %s", found.Name)
}
}
func TestMigration(t *testing.T) {
db, _ := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
m, _ := migrate.New("file://migrations", "sqlite://test.db")
m.Up()
db.AutoMigrate(&User{})
// تست مدلها
}
func SetupTestDB(t *testing.T) *gorm.DB {
db, _ := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{})
db.Create(&User{Name: "Ali"})
return db
}
t.Parallel()
// Package models defines database models for the application.
// All models include standard fields for auditing and soft deletion.
package models
// User represents a user in the system.
// It includes fields for identification, authentication, and auditing.
type User struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"type:varchar(100);not null" json:"name"`
Email string `gorm:"type:varchar(100);uniqueIndex" json:"email"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` // Hidden in JSON
}
godoc -http=:6060
http://localhost:6060/pkg/github.com/username/app/models/
.استفاده از ابزارهایی مثل dbml
یا pgAdmin
:
Table users {
id integer [primary key]
name varchar(100)
email varchar(100) [unique]
}
dbml2svg schema.dbml > schema.svg
// @Summary Create a new user
// @Description Creates a user with name and email
// @Tags Users
// @Accept json
// @Produce json
// @Param user body User true "User data"
// @Success 201 {object} User
// @Router /users [post]
func CreateUser(w http.ResponseWriter, r *http.Request) {}
git tag v1.0.0
go list -m github.com/username/app@v1.0.0
https://github.com/go-gorm/gorm
go-gormigrate
: ابزار مهاجرت برای GORM.gorm-gen
: تولید کد خودکار برای GORM.entgo
: جایگزین ORM با رویکرد تولید کد.gen := gormgen.NewGenerator(gormgen.Config{
OutPath: "./dal",
Mode: gormgen.FieldSignable,
})
gen.UseDB(db)
gen.GenerateModel("users")
client, _ := ent.Open("postgres", "...")
client.User.Create().SetName("Ali").Save(context.Background())
https://gorm.io/docs/
https://github.com/go-gorm/gorm/issues
#gorm
CONTRIBUTING.md
پروژه.این جزوه تمام جنبههای GORM را به صورت پیشرفته و عمیق پوشش داد. از مدلسازی حرفهای و عملیات CRUD پیشرفته تا معماری تمیز، مهاجرتها، بهینهسازی، امنیت، تستنویسی، مستندسازی، و مشارکت در انجمن، هر بخش با مثالهای عملی و نکات تخصصی ارائه شد. برای یادگیری عمیقتر:
https://gorm.io/docs/
https://github.com/go-gorm/gorm