实战二:文章系统
文章系统的数据模型适合练习“多表关联 + 分页 + 缓存”:文章属于分类、与标签多对多,列表要分页、详情要缓存。本章基于 gf gen dao 产物(article/category/tag/article_tag)演示官方文档中的事务、联查分页与 gcache 缓存。
一、创建:事务 + 多对多关联
创建文章 = 插入主表 + 批量写关联表,放进同一事务保证原子性。ORM 事务用 g.DB().Transaction(ctx, func(ctx, tx) error {...}):
package logic
import (
"context"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"article/internal/dao"
"article/internal/model/do"
)
func CreateArticle(ctx context.Context, in ArticleCreateInput) (err error) {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
articleId, err := tx.Model("article").Data(do.Article{
Title: in.Title, Content: in.Content, CategoryId: in.CategoryId,
}).InsertAndGetId() // 1. 插入主表,拿到自增 id
if err != nil {
return err
}
for _, tagId := range in.TagIds { // 2. 写入 文章-标签 关联
if _, err = tx.Model("article_tag").Data(g.Map{
"article_id": articleId, "tag_id": tagId,
}).Insert(); err != nil {
return err
}
}
return nil
})
}
ArticleCreateInput 为入参结构(Title/Content/CategoryId/TagIds)。事务内任一错误都会回滚全部写入。
二、列表:分类过滤 + 联查分页
type ArticleListItem struct {
Id int64 `json:"id"`
Title string `json:"title"`
CategoryName string `json:"categoryName"`
}
func GetArticleList(ctx context.Context, page, size int, categoryId int64) (list []ArticleListItem, total int, err error) {
m := dao.Article.Ctx(ctx)
if categoryId > 0 {
m = m.Where("category_id", categoryId)
}
// LeftJoin 联出分类名;同名列用表前缀消除歧义
m = m.LeftJoin(dao.Category.Table(), "c", "c.id = article.category_id").
Fields("article.id, article.title, c.name AS category_name")
// v2.5+:一条链完成 分页查询 + COUNT(1),计数不受分页影响
err = m.Page(page, size).OrderDesc("article.id").ScanAndCount(&list, &total, false)
return
}
controller 只需把 Req 的 Page/Size/CategoryId 传入并返回 List/Total,即完成分页接口(见“分页与列表接口设计”章节)。
三、详情缓存:GetOrSetFuncLock
详情读多写少,适合缓存。GetOrSetFuncLock 在锁内执行加载函数,缓存缺失时只允许一个协程查库,避免并发击穿:
// 需引入 gcache、gconv、time、gerror 与业务 code 包(import 省略)
func GetArticleDetail(ctx context.Context, id int64) (out *entity.Article, err error) {
key := "article:" + gconv.String(id)
v, err := gcache.GetOrSetFuncLock(ctx, key, func(ctx context.Context) (interface{}, error) {
var a entity.Article
if err := dao.Article.Ctx(ctx).Where("id", id).Scan(&a); err != nil {
return nil, err
}
if a.Id == 0 {
return nil, nil // 空结果也缓存,防穿透
}
return &a, nil
}, 10*time.Minute) // 过期时间
if err != nil {
return nil, err
}
if v == nil {
return nil, gerror.NewCode(code.ArticleNotFound)
}
return v.Val().(*entity.Article), nil
}
文章被修改/删除时主动失效:gcache.Remove(ctx, key),下次读取自动重载。演示用 gcache 进程内缓存;多实例共享时换 Redis 适配或直接用 g.Redis()(见 Redis 章节)。
要点回顾
- 多表写入用
Transaction保证原子性;多对多通过关联表表达。 - 列表用
LeftJoin + Fields + Page + ScanAndCount,计数不受分页影响。 - 详情用
GetOrSetFuncLock缓存 + 写后Remove失效;空结果也缓存防穿透;内存缓存只在单进程内共享,多实例需升级分布式缓存,方法细节以官方文档 goframe.org 为准。