IT评测·应用市场-qidao123.com

标题: 16 | 实现简洁架构的 Store 层 [打印本页]

作者: 大号在练葵花宝典    时间: 2025-3-14 10:50
标题: 16 | 实现简洁架构的 Store 层
提示:
  
  上一节课我们实现了 Store 层依赖的数据结构。本节课,我们就可以来实现 Store 层的代码。
Store 层代码用来跟数据库或者其他第三方微服务进行交互,实现 Store 层的思路如下:
IStore 接口界说及实现

Store 层的代码实现位于 feature/s12 分支的 internal/apiserver/store/ 目录下。核心实现位于 internal/apiserver/store/store.go 文件中,代码如代码清单 16-1 所示:
   代码清单 16-1:Store 层代码实现
  1. package store
  2. import (
  3.     "context"
  4.     "sync"
  5.     "github.com/onexstack/onexstack/pkg/store/where"
  6.     "gorm.io/gorm"
  7. )
  8. var (
  9.     once sync.Once
  10.     // 全局变量,方便其它包直接调用已初始化好的 datastore 实例.
  11.     S *datastore
  12. )
  13. // IStore 定义了 Store 层需要实现的方法.
  14. type IStore interface {
  15.     // 返回 Store 层的 *gorm.DB 实例,在少数场景下会被用到.
  16.     DB(ctx context.Context, wheres ...where.Where) *gorm.DB
  17.     TX(ctx context.Context, fn func(ctx context.Context) error) error
  18.     User() UserStore
  19.     Post() PostStore
  20. }
  21. // transactionKey 用于在 context.Context 中存储事务上下文的键.
  22. type transactionKey struct{}
  23. // datastore 是 IStore 的具体实现.
  24. type datastore struct {
  25.     core *gorm.DB
  26.     // 可以根据需要添加其他数据库实例
  27.     // fake *gorm.DB
  28. }
  29. // 确保 datastore 实现了 IStore 接口.
  30. var _ IStore = (*datastore)(nil)
  31. // NewStore 创建一个 IStore 类型的实例.
  32. func NewStore(db *gorm.DB) *datastore {
  33.     // 确保 S 只被初始化一次
  34.     once.Do(func() {
  35.         S = &datastore{db}
  36.     })
  37.     return S
  38. }
  39. // DB 根据传入的条件(wheres)对数据库实例进行筛选.
  40. // 如果未传入任何条件,则返回上下文中的数据库实例(事务实例或核心数据库实例).
  41. func (store *datastore) DB(ctx context.Context, wheres ...where.Where) *gorm.DB {
  42.     db := store.core
  43.     // 从上下文中提取事务实例
  44.     if tx, ok := ctx.Value(transactionKey{}).(*gorm.DB); ok {
  45.         db = tx
  46.     }
  47.     // 遍历所有传入的条件并逐一叠加到数据库查询对象上
  48.     for _, whr := range wheres {
  49.         db = whr.Where(db)
  50.     }
  51.     return db
  52. }
  53. // TX 返回一个新的事务实例.
  54. // nolint: fatcontext
  55. func (store *datastore) TX(ctx context.Context, fn func(ctx context.Context) error) error {
  56.     return store.core.WithContext(ctx).Transaction(
  57.         func(tx *gorm.DB) error {
  58.             ctx = context.WithValue(ctx, transactionKey{}, tx)
  59.             return fn(ctx)
  60.         },
  61.     )
  62. }
  63. // Users 返回一个实现了 UserStore 接口的实例.
  64. func (store *datastore) User() UserStore {
  65.     return newUserStore(store)
  66. }
  67. // Posts 返回一个实现了 PostStore 接口的实例.
  68. func (store *datastore) Post() PostStore {
  69.     return newPostStore(store)
  70. }
复制代码
DB 方法会实验从 context 中获取事务实例,假如没有则直接返回 *gorm.DB 范例的实例。TX 方法则将*gorm.DB 范例的实例注入到 context 中。通过 DB 和 TX 在 Biz 层实现事务,在 Store 层执行事务。
IStore 接口中的 User() 方法和 Post() 方法,分别返回 User 资源的 Store 层方法和 Post 资源的 Store 层方法。这种计划方式实在是软件开发中的抽象工厂模式。通过使用差异的方法,创建差异的对象,对象之间相互独立,以此进步代码的可维护性。另外使用这种方法,也能有用进步资源接口的尺度化。尺度化的资源接口更易理解和使用。
代码清单 16-1 中,使用以下代码创建 *datastore 范例的实例:
  1. // NewStore 创建一个 IStore 类型的实例.
  2. func NewStore(db *gorm.DB) *datastore {
  3.     // 确保 S 只被初始化一次
  4.     once.Do(func() {
  5.         S = &datastore{db}
  6.     })
  7.     return S
  8. }
复制代码
上述代码使用了 sync.Once 来确保实例只被初始化一次。实例创建完成后,将其赋值给包级变量 S,以便通过 store.S.User().Create() 来调用 Store 层接口。在 Go 的最佳实践中,发起尽量减少使用包级变量,由于包级变量的状态通常难以感知,会增长维护的复杂度。然而,这并非绝对规则,可以根据实际需要选择是否设置包级变量来简化开发。
UserStore 接口界说及实现

为了进步代码的可维护性,将 User 资源的 Store 代码单独存放在 internal/apiserver/store/user.go 文件中。UserStore 接口界说如下:
  1. // UserStore 定义了 user 模块在 store 层所实现的方法.
  2. type UserStore interface {
  3.     Create(ctx context.Context, obj *model.UserM) error
  4.     Update(ctx context.Context, obj *model.UserM) error
  5.     Delete(ctx context.Context, opts *where.Options) error
  6.     Get(ctx context.Context, opts *where.Options) (*model.UserM, error)
  7.     List(ctx context.Context, opts *where.Options) (int64, []*model.UserM, error)
  8.     UserExpansion
  9. }
  10. // UserExpansion 定义了用户操作的附加方法.
  11. type UserExpansion interface{}
复制代码
UserStore 接口中的方法分为两大类:资源尺度 CURD 方法和扩展方法。通过将方法分类,可以进一步进步接口中方法的可维护性和代码的可读性。将方法分为尺度方法和扩展方法的开发本领,在 Kuberentes client-go 项目中被大量使用,fastgo 的计划思路正是泉源于 client-go 的计划。
创建用户:Create 方法实现

userStore 结构体实现了 UserStore 接口。userStore 结构体的 Create 方法实现如下:
  1. // userStore 是 UserStore 接口的实现.
  2. type userStore struct {
  3.     store *datastore
  4. }
  5. // 确保 userStore 实现了 UserStore 接口.
  6. var _ UserStore = (*userStore)(nil)
  7. // newUserStore 创建 userStore 的实例.
  8. func newUserStore(store *datastore) *userStore {
  9.     return &userStore{store}
  10. }
  11. // Create 插入一条用户记录.
  12. func (s *userStore) Create(ctx context.Context, obj *model.UserM) error {
  13.     if err := s.store.DB(ctx).Create(&obj).Error; err != nil {
  14.         slog.Error("Failed to insert user into database", "err", err, "user", obj)
  15.         return errorsx.ErrDBWrite.WithMessage(err.Error())
  16.     }
  17.     return nil
  18. }
复制代码
在 Create 方法中,会调用 s.store.DB(ctx) 方法实验从 context 中获取事务实例,假如没有则直接返回*gorm.DB 范例的实例,并调用 *gorm.DB 提供的 Create 方法,完成数据库表记录的插入操作。
在 Create 方法中,假如插入失败,根据本课程第 13 节课的 fastgo 错误返回规范,直接返回了 errorsx.ErrorX 范例的错误 errno.ErrDBWrite,并设置了自界说错误消息:err.Error()。
在 Go 项目开发中,直接返回 gorm 包的错误,可能会袒露一些敏感信息,但绝大部门项目中,可以直接返回 gorm 包的报错信息。将 gorm 包的报错信息,直接在接口中返回,可以大幅进步接口失败时的排障服从。
删除用户:Delete 方法实现

userStore 结构体的 Delete 接口代码实现如下:
  1. // Delete 根据条件删除用户记录.
  2. func (s *userStore) Delete(ctx context.Context, opts *where.Options) error {
  3.     err := s.store.DB(ctx, opts).Delete(new(model.UserM)).Error
  4.     if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
  5.         slog.Error("Failed to delete user from database", "err", err, "conditions", opts)
  6.         return errorsx.ErrDBWrite.WithMessage(err.Error())
  7.     }
  8.     return nil
  9. }
复制代码
在 Delete 方法中,会判断删除记录时的错误范例是否是 gorm.ErrRecordNotFound,假如不是会返回自界说错误,假如是则返回删除成功,以此实现幂等删除。在 Go 项目开发中,删除活动在绝大部门项目中是一个幂等的操作。
查询用户列表:List 方法实现

userStore 结构体的 List 方法实现如下:
  1. // List 返回用户列表和总数.
  2. // nolint: nonamedreturns
  3. func (s *userStore) List(ctx context.Context, opts *where.Options) (count int64, ret []*model.UserM, err error) {
  4.     err = s.store.DB(ctx, opts).Order("id desc").Find(&ret).Offset(-1).Limit(-1).Count(&count).Error
  5.     if err != nil {
  6.         slog.Error("Failed to list users from database", "err", err, "conditions", opts)
  7.         err = errorsx.ErrDBRead.WithMessage(err.Error())
  8.     }
  9.     return
  10. }
复制代码
在 List 方法中,会调用 s.store.DB(ctx, opts) 方法,根据 opts 入参来配置 *gorm.DB 实例的查询条件。查询的记录会按数据库 id 字段降序排列(从大到小的顺序)。将最新的记录放在返回列表的最前面可以进步返回数据的阅读体验,也是大部门平台的默认排序规则。
整个 UserStore 接口中的方法实现较为简洁,仅直接对数据库记录进行增删改查操作,并未封装任何业务逻辑。Post 资源的 Store 层代码实现与 User 资源保持一致,故本课程不再解读 Post 资源的 Store 层实现。
在 Go 项目开发中,不少开发者会在 Store 层封装业务代码,为了实现差异的查询条件,会在 Store 层封装很多查询类方法,比方:ListUser、ListUserByName、ListUserByID 等。这都会使 Store 层代码变得痴肥,难以维护。实在 Store 层只需要对数据库记录进行简朴的增删改查即可。对插入数据或查询数据的处理可以放在业务逻辑层。对查询条件的定制,可以通过提供灵活的查询参数来实现。
where 查询条件实现

为了实现在查询数据库记录时,配置灵活的查询参数,fastgo 项目引用了 onexstack 项目的 where 包,包名为 github.com/onexstack/onexstack/pkg/store/where。整个 OneX 技术体系,都通过 where 包来定制化查询条件。
where 包更多的计划方法、思考及使用方法可以参考 云原生 AI 实战营 中 Go 项目开发中级实战课 的第 25 | 讲 业务实现(2):实现 Store 层代码。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 IT评测·应用市场-qidao123.com (https://dis.qidao123.com/) Powered by Discuz! X3.4