Skip to content

Commit

Permalink
feat(tag): tag相关 service和dao处理逻辑,及所有模块共有的字段回调
Browse files Browse the repository at this point in the history
  • Loading branch information
minibear2333 committed Jun 15, 2021
1 parent 7cbbef2 commit 6ccfc9d
Show file tree
Hide file tree
Showing 6 changed files with 254 additions and 9 deletions.
11 changes: 11 additions & 0 deletions internal/dao/dao.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package dao

import "github.com/jinzhu/gorm"

type Dao struct {
engine *gorm.DB
}

func New(engine *gorm.DB) *Dao {
return &Dao{engine: engine}
}
43 changes: 43 additions & 0 deletions internal/dao/tag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Package dao dao 层进行了数据访问对象的封装,并针对业务所需的字段进行了处理。
package dao

import (
"github.com/golang-minibear2333/gin-blog/internal/model"
"github.com/golang-minibear2333/gin-blog/pkg/app"
)

func (d *Dao) CountTag(name string, state uint8) (int, error) {
tag := model.Tag{Name: name, State: state}
return tag.Count(d.engine)
}

func (d *Dao) GetTagList(name string, state uint8, page, pageSize int) ([]*model.Tag, error) {
tag := model.Tag{Name: name, State: state}
pageOffset := app.GetPageOffset(page, pageSize)
return tag.List(d.engine, pageOffset, pageSize)
}

func (d *Dao) CreateTag(name string, state uint8, createdBy string) error {
tag := model.Tag{
Name: name,
State: state,
Model: &model.Model{CreatedBy: createdBy},
}

return tag.Create(d.engine)
}

func (d *Dao) UpdateTag(id uint32, name string, state uint8, modifiedBy string) error {
tag := model.Tag{
Name: name,
State: state,
Model: &model.Model{ID: id, ModifiedBy: modifiedBy},
}

return tag.Update(d.engine)
}

func (d *Dao) DeleteTag(id uint32) error {
tag := model.Tag{Model: &model.Model{ID: id}}
return tag.Delete(d.engine)
}
97 changes: 94 additions & 3 deletions internal/model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"github.com/golang-minibear2333/gin-blog/global"
"github.com/golang-minibear2333/gin-blog/pkg/setting"
"github.com/jinzhu/gorm"
"time"

// 必须以此引入mysql驱动库
_ "github.com/jinzhu/gorm/dialects/mysql"
)
Expand All @@ -14,9 +16,9 @@ type Model struct {
ID uint32 `gorm:"primary_key" json:"id"`
CreatedBy string `json:"created_by"`
ModifiedBy string `json:"modified_by"`
CreatedOn uint32 `json:"created_on"`
ModifiedOn uint32 `json:"modified_on"`
DeletedOn uint32 `json:"deleted_on"`
CreatedOn uint32 `json:"created_on"` // 创建时间
ModifiedOn uint32 `json:"modified_on"` // 更新时间
DeletedOn uint32 `json:"deleted_on"` // 删除时间
IsDel uint8 `json:"is_del"`
}

Expand All @@ -37,8 +39,97 @@ func NewDBEngine(databaseSetting *setting.DatabaseSettingS) (*gorm.DB, error) {
db.LogMode(true)
}
db.SingularTable(true)
// 注册回调函数,会在执行相应语句之前回掉执行
// gorm 把所有的执行都注册为callback,"gorm:xxx" 的字符串注册成不同的方法或者步骤阶段,可以看源码的init()函数
db.Callback().Create().Replace("gorm:update_time_stamp", updateTimeStampForCreateCallback)
db.Callback().Update().Replace("gorm:update_time_stamp", updateTimeStampForUpdateCallback)
db.Callback().Delete().Replace("gorm:delete", deleteCallback)
db.DB().SetMaxIdleConns(databaseSetting.MaxIdleConns)
db.DB().SetMaxOpenConns(databaseSetting.MaxOpenConns)

return db, nil
}

// updateTimeStampForCreateCallback 创建时的回调
func updateTimeStampForCreateCallback(scope *gorm.Scope) {
if !scope.HasError() {
nowTime := time.Now().Unix()
// 通过调用 scope.FieldByName 方法,获取当前是否包含所需的字段
if createTimeField, ok := scope.FieldByName("CreatedOn"); ok {
// 通过判断 Field.IsBlank 的值,可以得知该字段的值是否为空
if createTimeField.IsBlank {
// 若为空,则会调用 Field.Set 方法给该字段设置值,入参类型为 interface{},内部也就是通过反射进行一系列操作赋值。
_ = createTimeField.Set(nowTime)
}
}

if modifyTimeField, ok := scope.FieldByName("ModifiedOn"); ok {
if modifyTimeField.IsBlank {
_ = modifyTimeField.Set(nowTime)
}
}
}
}

// updateTimeStampForUpdateCallback 更新时回调
func updateTimeStampForUpdateCallback(scope *gorm.Scope) {
// 通过调用 scope.Get("gorm:update_column") 去获取当前设置了标识 gorm:update_column 的字段属性
if _, ok := scope.Get("gorm:update_column"); !ok {
// 若不存在,也就是没有自定义设置 update_column,那么将会在更新回调内设置默认字段 ModifiedOn 的值为当前的时间戳
// TODO 如果是这样的逻辑,就只能更新一次了,假如下一次更新数据怎么控制刷新这个字段呢?
_ = scope.SetColumn("ModifiedOn", time.Now().Unix())
}
}
// 这里参考了源码 https://gitea.com/jinzhu/gorm/src/branch/master/callback_delete.go
func deleteCallback(scope *gorm.Scope) {
if !scope.HasError() {
var extraOption string
// 通过调用 scope.Get("gorm:delete_option") 去获取当前设置了标识 gorm:delete_option 的字段属性。
if str, ok := scope.Get("gorm:delete_option"); ok {
extraOption = fmt.Sprint(str)
}

// 判断是否存在 DeletedOn 和 IsDel 字段,若存在则调整为执行 UPDATE 操作进行软删除
//(修改 DeletedOn 和 IsDel 的值),否则执行 DELETE 进行硬删除。
deletedOnField, hasDeletedOnField := scope.FieldByName("DeletedOn")
isDelField, hasIsDelField := scope.FieldByName("IsDel")
if !scope.Search.Unscoped && hasDeletedOnField && hasIsDelField {
now := time.Now().Unix()
// 软删除
scope.Raw(fmt.Sprintf(
"UPDATE %v SET %v=%v,%v=%v%v%v",
// 表名
scope.QuotedTableName(),
// TODO 这里为什么用的DBName,而不是Name,需要debug
scope.Quote(deletedOnField.DBName),
// 删除时间
scope.AddToVars(now),
scope.Quote(isDelField.DBName),
// 设置软删除参数 1 是软删除
scope.AddToVars(1),
// 组装sql 判断 deleted_on 和 is_del 是否存在
// TODO 还未理解此处,待查询资料解释
addExtraSpaceIfExist(scope.CombinedConditionSql()),
addExtraSpaceIfExist(extraOption),
)).Exec()
} else {
// 调用 scope.QuotedTableName 方法获取当前所引用的表名,并调用一系列方法针对 SQL 语句的组成部分进行处理和转移
// 最后在完成一些所需参数设置后调用 scope.CombinedConditionSql 方法完成 SQL 语句的组装。
scope.Raw(fmt.Sprintf(
"DELETE FROM %v%v%v",
scope.QuotedTableName(),
addExtraSpaceIfExist(scope.CombinedConditionSql()),
addExtraSpaceIfExist(extraOption),
)).Exec()
}
}
}

// 为了拼接sql增加额外的空格
func addExtraSpaceIfExist(str string) string {
if str != "" {
return " " + str
}
return ""

}
55 changes: 54 additions & 1 deletion internal/model/tag.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package model

import "github.com/golang-minibear2333/gin-blog/pkg/app"
import (
"github.com/golang-minibear2333/gin-blog/pkg/app"
"github.com/jinzhu/gorm"
)

type Tag struct {
*Model
Expand All @@ -13,6 +16,56 @@ type TagSwagger struct {
Pager *app.Pager
}

// TableName 解析表名,如果不写默认解析结构体名
func (t Tag) TableName() string {
return "blog_tag"
}

func (t Tag) Count(db *gorm.DB) (int, error) {
var count int
if t.Name != "" {
db = db.Where("name = ?", t.Name)
}
// 传入查询参数 sql 的 where 有防sql注入功能
db = db.Where("state = ?", t.State)
// Count 统计行为,用于统计模型的记录数。
if err := db.Model(&t).Where("is_del = ?", 0).Count(&count).Error; err != nil {
return 0, err
}

return count, nil
}

func (t Tag) List(db *gorm.DB, pageOffset, pageSize int) ([]*Tag, error) {
var tags []*Tag
var err error
if pageOffset >= 0 && pageSize > 0 {
// Offset 偏移量,用于指定开始返回记录之前要跳过的记录数 Limit 限制检索的记录数
db = db.Offset(pageOffset).Limit(pageSize)
}
if t.Name != "" {
db = db.Where("name = ?", t.Name)
}
db = db.Where("state = ?", t.State)
// Find 查找符合筛选条件的记录,用来赋值的
if err = db.Where("is_del = ?", 0).Find(&tags).Error; err != nil {
return nil, err
}

return tags, nil
}

func (t Tag) Create(db *gorm.DB) error {
return db.Create(&t).Error
}

func (t Tag) Update(db *gorm.DB) error {
// Updates 更新所选字段
return db.Model(&Tag{}).Where("id = ? AND is_del = ?", t.ID, 0).Update(t).Error
}

func (t Tag) Delete(db *gorm.DB) error {
// Delete 删除数据
// TODO 这里是硬删除,后期考虑换成软删除和增加已删除tag找回功能
return db.Where("id = ? AND is_del = ?", t.Model.ID, 0).Delete(&t).Error
}
18 changes: 18 additions & 0 deletions internal/service/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package service

import (
"context"
"github.com/golang-minibear2333/gin-blog/global"
"github.com/golang-minibear2333/gin-blog/internal/dao"
)

type Service struct {
ctx context.Context
dao *dao.Dao
}

func New(ctx context.Context) Service {
svc := Service{ctx: ctx}
svc.dao = dao.New(global.DBEngine)
return svc
}
39 changes: 34 additions & 5 deletions internal/service/tag.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
package service

import (
"github.com/golang-minibear2333/gin-blog/internal/model"
"github.com/golang-minibear2333/gin-blog/pkg/app"
)

// CountTagRequest 定义了 Request 结构体作为接口入参的基准,而本项目由于并不会太复杂,所以直接放在了 service 层中便于使用
// 若后续业务不断增长,程序越来越复杂,service 也冗杂了,可以考虑将抽离一层接口校验层,便于解耦逻辑。
type CountTagRequest struct {
Name string `form:"name" binding:"max=100"`
State uint8 `form:"state,default=1" binding:"oneof=0 1"`
Expand All @@ -11,18 +18,40 @@ type TagListRequest struct {
}

type CreateTagRequest struct {
Name string `form:"name" binding:"required,min=3,max=100"`
CreatedBy string `form:"created_by" binding:"required,min=3,max=100"`
Name string `form:"name" binding:"required,min=2,max=100"`
CreatedBy string `form:"created_by" binding:"required,min=2,max=100"`
State uint8 `form:"state,default=1" binding:"oneof=0 1"`
}

type UpdateTagRequest struct {
ID uint32 `form:"id" binding:"required,gte=1"`
Name string `form:"name" binding:"min=3,max=100"`
State uint8 `form:"state" binding:"required,oneof=0 1"`
ModifiedBy string `form:"modified_by" binding:"required,min=3,max=100"`
Name string `form:"name" binding:"max=100"`
State uint8 `form:"state" binding:"oneof=0 1"`
ModifiedBy string `form:"modified_by" binding:"required,min=2,max=100"`
}

type DeleteTagRequest struct {
ID uint32 `form:"id" binding:"required,gte=1"`
}

// 如下 在应用分层中,service 层主要是针对业务逻辑的封装,如果有一些业务聚合和处理可以在该层进行编码,同时也能较好的隔离上下两层的逻辑

func (svc *Service) CountTag(param *CountTagRequest) (int, error) {
return svc.dao.CountTag(param.Name, param.State)
}

func (svc *Service) GetTagList(param *TagListRequest, pager *app.Pager) ([]*model.Tag, error) {
return svc.dao.GetTagList(param.Name, param.State, pager.Page, pager.PageSize)
}

func (svc *Service) CreateTag(param *CreateTagRequest) error {
return svc.dao.CreateTag(param.Name, param.State, param.CreatedBy)
}

func (svc *Service) UpdateTag(param *UpdateTagRequest) error {
return svc.dao.UpdateTag(param.ID, param.Name, param.State, param.ModifiedBy)
}

func (svc *Service) DeleteTag(param *DeleteTagRequest) error {
return svc.dao.DeleteTag(param.ID)
}

0 comments on commit 6ccfc9d

Please sign in to comment.