实战二:文章系统

文章系统的数据模型适合练习“多表关联 + 分页 + 缓存”:文章属于分类、与标签多对多,列表要分页、详情要缓存。本章基于 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 只需把 ReqPage/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 为准。
笔记加载中…