下载工作台
Gin Web 开发

Repository 与分层架构

试读上半部分 · 解锁后可读全文

第 5 章 · Repository 与分层架构

本章目标:实现 Handler → Service → Repository 三层架构;ProductRepository 完整 CRUD;Service 承载业务规则;Handler 仅处理 HTTP;将 ch02/ch04 代码接入数据库;对照 fastapi-web ch05/ch06 services/repositories 与 flask-web ch05 模型访问分层。

学时建议:4~5 小时(含 2.5 小时跟练)

前置:完成 gin-web ch04


5.1 场景说明:分层职责

随着商品 CRUD 逻辑增长,全部写在 handler 会导致难以测试与复用。api-go-demo 采用与 svc-demo 类似的分层:

HTTP Request
     ▼
┌─────────┐   bind/DTO   ┌─────────┐   业务规则   ┌──────────────┐
│ Handler │ ───────────► │ Service │ ───────────► │ Repository   │
└─────────┘              └─────────┘              └──────┬───────┘
     ▲                        │                         │
     └──── JSON 响应 ─────────┘                         ▼
                                                    GORM / MySQL
职责禁止
Handler参数绑定、HTTP 状态码、调用 Service直接 db.Where
Service校验、权限边界、领域错误依赖 gin.Context
RepositoryCRUD、查询组装、传递 context.Context返回 HTTP 响应

对照章节

主题fastapi-web ch06flask-webgin-web ch05
路由层router + depsBlueprint viewhandler
业务层service部分在 viewservice
数据层repositorymodel.queryrepository

5.2 逐步操作表

步骤操作验证
1创建 internal/repository/product.goCRUD 方法编译
2创建 internal/service/product.go领域 error 定义
3创建 internal/dto/product.go响应 DTO
4重构 handler/product.go 注入 Service去除 mock
5更新 routermain wiring启动成功
6GET 已发布 slug200 + DB 数据
7GET 未发布 slug(匿名)404
8POST 重复 slug409 CONFLICT

5.3 目录结构

internal/
├── dto/product.go
├── handler/product.go
├── service/product.go
└── repository/product.go

5.4 ProductRepository 完整 CRUD

// internal/repository/product.go
package repository

import (
    "context"

    "example.com/api-go-demo/internal/model"
    "gorm.io/gorm"
)

type ProductRepository struct {
    db *gorm.DB
}

func NewProductRepository(db *gorm.DB) *ProductRepository {
    return &ProductRepository{db: db}
}

func (r *ProductRepository) ListPublished(ctx context.Context, page, size int) ([]model.Product, int64, error) {
    var items []model.Product
    var total int64
    q := r.db.WithContext(ctx).Model(&model.Product{}).Where("is_published = ?", true)
    if err := q.Count(&total).Error; err != nil {
        return nil, 0, err
    }
    if page < 1 {
        page = 1
    }
    if size < 1 {
        size = 20
    }
    offset := (page - 1) * size
    err := q.Order("id desc").Offset(offset).Limit(size).Find(&items).Error
    return items, total, err
}

func (r *ProductRepository) GetBySlug(ctx context.Context, slug string, publishedOnly bool) (*model.Product, error) {
    var p model.Product
    q := r.db.WithContext(ctx).Where("slug = ?", slug)
    if publishedOnly {
        q = q.Where("is_published = ?", true)
    }
    err := q.First(&p).Error
    if err != nil {
        return nil, err
    }
    return &p, nil
}

func (r *ProductRepository) Create(ctx context.Context, p *model.Product) error {
    return r.db.WithContext(ctx).Create(p).Error
}

func (r *ProductRepository) UpdateBySlug(ctx context.Context, slug string, updates map[string]any) error {
    res := r.db.WithContext(ctx).Model(&model.Product{}).Where("slug = ?", slug).Updates(updates)
    if res.Error != nil {
        return res.Error
    }
    if res.RowsAffected == 0 {
        return gorm.ErrRecordNotFound
    }
    return nil
}

func (r *ProductRepository) DeleteBySlug(ctx context.Context, slug string) error {
    res := r.db.WithContext(ctx).Where("slug = ?", slug).Delete(&model.Product{})
    if res.Error != nil {
        return res.Error
    }
    if res.RowsAffected == 0 {
        return gorm.ErrRecordNotFound
    }
    return nil
}
方法说明
ListPublished分页 + 仅 is_published=true
GetBySlugpublishedOnly 控制匿名是否可见草稿
UpdateBySlugmap 更新,0 行 → NotFound
DeleteBySlug硬删(选修软删:DeletedAt

5.5 ProductService

// internal/service/product.go
package service

import (
    "context"
    "errors"

    "example.com/api-go-demo/internal/model"
    "example.com/api-go-demo/internal/repository"
    "gorm.io/gorm"
)

var (
    ErrProductNotFound = errors.New("product not found")
    ErrDuplicateSlug   = errors.New("duplicate slug")
    ErrInvalidPrice    = errors.New("invalid price")
)

type ProductService struct {
    repo *repository.ProductRepository
}

func NewProductService(repo *repository.ProductRepository) *ProductService {
    return &ProductService{repo: repo}
}

func (s *ProductService) ListPublished(ctx context.Context, page, size int) ([]model.Product, int64, error) {
    return s.repo.ListPublished(ctx, page, size)
}

func (s *ProductService) GetPublished(ctx context.Context, slug string) (*model.Product, error) {
    p, err := s.repo.GetBySlug(ctx, slug, true)
    if errors.Is(err, gorm.ErrRecordNotFound) {
        return nil, ErrProductNotFound
    }
    return p, err
}

func (s *ProductService) GetBySlugAdmin(ctx context.Context, slug string) (*model.Product, error) {
    p, err := s.repo.GetBySlug(ctx, slug, false)
    if errors.Is(err, gorm.ErrRecordNotFound) {
        return nil, ErrProductNotFound
    }
    return p, err
}

func (s *ProductService) Create(ctx context.Context, p *model.Product) error {
    if p.Price < 0 {
        return ErrInvalidPrice
    }
    _, err := s.repo.GetBySlug(ctx, p.Slug, false)
    if err == nil {
        return ErrDuplicateSlug
    }
    if !errors.Is(err, gorm.ErrRecordNotFound) {
        return err
    }
    return s.repo.Create(ctx, p)
}

func (s *ProductService) Update(ctx context.Context, slug string, updates map[string]any) error {
    if price, ok := updates["price"].(int64); ok && price < 0 {
        return ErrInvalidPrice
    }
    err := s.repo.UpdateBySlug(ctx, slug, updates)
    if errors.Is(err, gorm.ErrRecordNotFound) {
        return ErrProductNotFound
    }
    return err
}

func (s *ProductService) Delete(ctx context.Context, slug string) error {
    err := s.repo.DeleteBySlug(ctx, slug)
    if errors.Is(err, gorm.ErrRecordNotFound) {
        return ErrProductNotFound
    }
    return err
}

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。