- 项目地址:github.com/testerxiaod…
- 参考开源项目:github.com/HammerCloth…
1. 项目开发目前痛点
在上一篇文章末尾,我说出了目前开发过程中的一些痛点,其中一点是我作为一个刚学习后端开发的新人,在设计数据库表时,字段名和类型没有考虑好,数据表结构可能需要一些改动,而原始的gorm,如果修改了字段名调用AutoMigrate方法时,原字段不会被删除。而且gorm的Where条件查询,字段需要写死在字符串里,当修改字段时,难以全部找到要修改的点,字符串拼接也容易造成sql注入。
func GetCommentById(commentId int64) (entity.TableComment, error) {
var comment entity.TableComment
if err := Db.Where("id = ?", commentId).First(&comment).Error; err != nil {
fmt.Println("GetCommentById failed, err:", err)
return comment, err
}
return comment, nil
}
2. gorm/gen
基于以上痛点,决定使用gorm的gen工具,该工具可以根据数据库表自动生成实体类以及常用Dao方法,学过Java的人可能用过mybatisplus,用了这块工具,go对持久层的操作也可以很丝滑。
3. gorm/gen快速上手
下载
go get -u gorm.io/gen
在项目根目录新建cmd目录,在该目录下新增generate.go文件
package main
import (
"faker-douyin/global"
"faker-douyin/model/dao"
"gorm.io/gen"
"strings"
)
// generate code
func main() {
global.LoadConfig()
dao.Init()
// specify the output directory (default: "./query")
// ### if you want to query without context constrain, set mode gen.WithoutContext ###
g := gen.NewGenerator(gen.Config{
// 相对执行`go run`时的路径, 会自动创建目录
OutPath: "../model/dao", //curd代码的输出路径
ModelPkgPath: "../model/entity", //model代码的输出路径
// WithDefaultQuery 生成默认查询结构体(作为全局变量使用), 即`Q`结构体和其字段(各表模型)
// WithoutContext 生成没有context调用限制的代码供查询
// WithQueryInterface 生成interface形式的查询代码(可导出), 如`Where()`方法返回的就是一个可导出的接口类型
Mode: gen.WithDefaultQuery | gen.WithoutContext | gen.WithQueryInterface,
// 表字段可为 null 值时, 对应结体字段使用指针类型
FieldNullable: true, // generate pointer when field is nullable
// 表字段默认值与模型结构体字段零值不一致的字段, 在插入数据时需要赋值该字段值为零值的, 结构体字段须是指针类型才能成功, 即`FieldCoverable:true`配置下生成的结构体字段.
// 因为在插入时遇到字段为零值的会被GORM赋予默认值. 如字段`age`表默认值为10, 即使你显式设置为0最后也会被GORM设为10提交.
// 如果该字段没有上面提到的插入时赋零值的特殊需要, 则字段为非指针类型使用起来会比较方便.
FieldCoverable: false, // generate pointer when field has default value, to fix problem zero value cannot be assign: https://gorm.io/docs/create.html#Default-Values
// 模型结构体字段的数字类型的符号表示是否与表字段的一致, `false`指示都用有符号类型
FieldSignable: false, // detect integer field's unsigned type, adjust generated data type
// 生成 gorm 标签的字段索引属性
FieldWithIndexTag: false, // generate with gorm index tag
// 生成 gorm 标签的字段类型属性
FieldWithTypeTag: true, // generate with gorm column type tag
})
// 设置目标 db
g.UseDB(dao.Db)
// 自定义字段的数据类型
// 统一数字类型为int64,兼容protobuf和thrift
dataMap := map[string]func(detailType string) (dataType string){
"tinyint": func(detailType string) (dataType string) { return "int64" },
"smallint": func(detailType string) (dataType string) { return "int64" },
"mediumint": func(detailType string) (dataType string) { return "int64" },
"bigint": func(detailType string) (dataType string) { return "int64" },
"int": func(detailType string) (dataType string) { return "int64" },
}
// 要先于`ApplyBasic`执行
g.WithDataTypeMap(dataMap)
// 自定义模型结体字段的标签
// 将特定字段名的 json 标签加上`string`属性,即 MarshalJSON 时该字段由数字类型转成字符串类型
jsonField := gen.FieldJSONTagWithNS(func(columnName string) (tagContent string) {
toStringField := `id, `
if strings.Contains(toStringField, columnName) {
return columnName + ",string"
}
return columnName
})
// 将非默认字段名的字段定义为自动时间戳和软删除字段;
// 自动时间戳默认字段名为:`updated_at`、`created_at, 表字段数据类型为: INT 或 DATETIME
// 软删除默认字段名为:`deleted_at`, 表字段数据类型为: DATETIME
autoUpdateTimeField := gen.FieldGORMTag("update_at", "column:update_time;type:int unsigned;autoUpdateTime")
autoCreateTimeField := gen.FieldGORMTag("create_at", "column:create_time;type:int unsigned;autoCreateTime")
softDeleteField := gen.FieldType("delete_at", "soft_delete.DeletedAt")
// 模型自定义选项组
fieldOpts := []gen.ModelOpt{jsonField, autoCreateTimeField, autoUpdateTimeField, softDeleteField}
//fieldOpts := []gen.ModelOpt{jsonField, softDeleteField}
// 创建模型的结构体,生成文件在 model 目录; 先创建的结果会被后面创建的覆盖
// 这里创建个别模型仅仅是为了拿到`*generate.QueryStructMeta`类型对象用于后面的模型关联操作中
// 创建全部模型文件, 并覆盖前面创建的同名模型
allModel := g.GenerateAllTable(fieldOpts...)
// 创建有关联关系的模型文件
// 可以用于指定外键
//Score := g.GenerateModel("score",
// append(
// fieldOpts,
// // user 一对多 address 关联, 外键`uid`在 address 表中
// gen.FieldRelate(field.HasMany, "user", User, &field.RelateConfig{GORMTag: "foreignKey:UID"}),
// )...,
//)
// 创建模型的方法,生成文件在 query 目录; 先创建结果不会被后创建的覆盖
//g.ApplyBasic(User)
g.ApplyBasic(allModel...)
// execute the action of code generation
g.Execute()
}
运行函数生成代码
cd cmd
go run main.go
运行成功之后
后缀为
gen.go的文件就是gen工具自动为我们生成的,简直不要太爽
type IUserDo interface {
gen.SubQuery
Debug() IUserDo
WithContext(ctx context.Context) IUserDo
WithResult(fc func(tx gen.Dao)) gen.ResultInfo
ReplaceDB(db *gorm.DB)
ReadDB() IUserDo
WriteDB() IUserDo
As(alias string) gen.Dao
Session(config *gorm.Session) IUserDo
Columns(cols ...field.Expr) gen.Columns
Clauses(conds ...clause.Expression) IUserDo
Not(conds ...gen.Condition) IUserDo
Or(conds ...gen.Condition) IUserDo
Select(conds ...field.Expr) IUserDo
Where(conds ...gen.Condition) IUserDo
Order(conds ...field.Expr) IUserDo
Distinct(cols ...field.Expr) IUserDo
Omit(cols ...field.Expr) IUserDo
Join(table schema.Tabler, on ...field.Expr) IUserDo
LeftJoin(table schema.Tabler, on ...field.Expr) IUserDo
RightJoin(table schema.Tabler, on ...field.Expr) IUserDo
Group(cols ...field.Expr) IUserDo
Having(conds ...gen.Condition) IUserDo
Limit(limit int) IUserDo
Offset(offset int) IUserDo
Count() (count int64, err error)
Scopes(funcs ...func(gen.Dao) gen.Dao) IUserDo
Unscoped() IUserDo
Create(values ...*entity.User) error
CreateInBatches(values []*entity.User, batchSize int) error
Save(values ...*entity.User) error
First() (*entity.User, error)
Take() (*entity.User, error)
Last() (*entity.User, error)
Find() ([]*entity.User, error)
FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*entity.User, err error)
FindInBatches(result *[]*entity.User, batchSize int, fc func(tx gen.Dao, batch int) error) error
Pluck(column field.Expr, dest interface{}) error
Delete(...*entity.User) (info gen.ResultInfo, err error)
Update(column field.Expr, value interface{}) (info gen.ResultInfo, err error)
UpdateSimple(columns ...field.AssignExpr) (info gen.ResultInfo, err error)
Updates(value interface{}) (info gen.ResultInfo, err error)
UpdateColumn(column field.Expr, value interface{}) (info gen.ResultInfo, err error)
UpdateColumnSimple(columns ...field.AssignExpr) (info gen.ResultInfo, err error)
UpdateColumns(value interface{}) (info gen.ResultInfo, err error)
UpdateFrom(q gen.SubQuery) gen.Dao
Attrs(attrs ...field.AssignExpr) IUserDo
Assign(attrs ...field.AssignExpr) IUserDo
Joins(fields ...field.RelationField) IUserDo
Preload(fields ...field.RelationField) IUserDo
FirstOrInit() (*entity.User, error)
FirstOrCreate() (*entity.User, error)
FindByPage(offset int, limit int) (result []*entity.User, count int64, err error)
ScanByPage(result interface{}, offset int, limit int) (count int64, err error)
Scan(result interface{}) (err error)
Returning(value interface{}, columns ...string) IUserDo
UnderlyingDB() *gorm.DB
schema.Tabler
}
4. 使用gen工具生成的Dao方法快速开发
先修改UserServiceImpl的UserService签名方法实现,替换为User全局变量的IuserDo中的方法,在此之前注释AutoMigrate,以后不需要自动迁移,用mysql工具修改表结构重新生成实体类和Dao方法,然后调用
func SetDefault(db *gorm.DB, opts ...gen.DOOption)方法。
// err = Db.AutoMigrate(&entity.User{}, &entity.TableVideo{}, entity.TableComment{})
SetDefault(Db)
修改UserServiceImpl.go文件
package service
import (
"faker-douyin/model/dao"
"faker-douyin/model/dto/response"
"faker-douyin/model/entity"
)
type UserServiceImpl struct {
}
func NewUserService() UserService {
return &UserServiceImpl{}
}
func (u *UserServiceImpl) GetAllUser() ([]*entity.User, error) {
users, err := dao.User.Find()
if err != nil {
return nil, err
}
return users, nil
}
func (u *UserServiceImpl) GetByUsername(username string) (*entity.User, error) {
user, err := dao.User.Where(dao.User.Username.Eq(username)).First()
if err != nil {
return nil, err
}
return user, nil
}
func (u *UserServiceImpl) GetByID(id int64) (*response.UserInfoRes, error) {
user, err := dao.User.Where(dao.User.ID.Eq(id)).First()
if err != nil {
return nil, err
}
var userInfo response.UserInfoRes
userInfo.ID = user.ID
userInfo.Username = user.Username
return &userInfo, nil
}
func (u *UserServiceImpl) CreateUser(user entity.User) (*entity.User, error) {
newUser := &entity.User{
Username: user.Username,
Password: user.Password,
}
err := dao.User.Create(newUser)
if err != nil {
return nil, err
}
return newUser, nil
}
修改UserController.go文件
package v1
import (
"errors"
"faker-douyin/model/common"
"faker-douyin/model/dto/request"
"faker-douyin/model/dto/response"
"faker-douyin/model/entity"
"faker-douyin/service"
"faker-douyin/utils"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"log"
)
type UserController struct {
}
// Register POST douyin/v1/user/register/ 用户注册
func (u *UserController) Register(c *gin.Context) {
var userRegisterReq request.UserRegisterReq
if err := c.ShouldBindJSON(&userRegisterReq); err != nil {
common.FailWithMessage(err.Error(), c)
return
}
// 依赖倒转原则,面向抽象层进行开发
usi := service.NewUserService()
// 根据用户名查询用户是否存在
user, err := usi.GetByUsername(userRegisterReq.Username)
// 如果有错误,并且错误不是没有记录
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
common.FailWithMessage(err.Error(), c)
return
}
// 用户名不存在,开始注册
if user == nil {
newUser := entity.User{
Username: userRegisterReq.Username,
Password: utils.EnCoder(userRegisterReq.Password),
}
// 用户名不存在,插入数据
user, err := usi.CreateUser(newUser)
if err != nil {
println("Insert Data Fail")
common.FailWithMessage(err.Error(), c)
return
}
token := utils.GenerateToken(user)
log.Println("注册返回的id: ", user.ID)
common.OkWithDetailed(response.UserRegisterSuccessRes{ID: uint64(user.ID), Token: token}, "注册成功", c)
} else {
// 用户名存在,不允许注册
common.FailWithMessage("User already exist", c)
}
}
// Login POST douyin/v1/user/login/ 用户登陆
func (u *UserController) Login(c *gin.Context) {
var userLoginReq request.UserLoginReq
// 请求参数绑定和校验
err := c.ShouldBindJSON(&userLoginReq)
if err != nil {
common.FailWithMessage(err.Error(), c)
return
}
// 依赖倒转原则,面向抽象层进行开发
var usi service.UserService
usi = new(service.UserServiceImpl)
// 根据name查询用户
user, err := usi.GetByUsername(userLoginReq.Username)
if err != nil {
common.FailWithMessage(err.Error(), c)
return
}
// 查询成功,将密码加密后与数据库密码比较
if utils.EnCoder(userLoginReq.Password) == user.Password {
token := utils.GenerateToken(user)
common.OkWithDetailed(response.UserLoginSuccessRes{ID: uint64(user.ID), Token: token}, "登陆成功", c)
return
} else {
common.FailWithMessage("username or password error", c)
}
}
// UserInfo GET douyin/v1/user/ 获取用户信息
func (u *UserController) UserInfo(c *gin.Context) {
var userInfoReq request.UserInfoReq
// 请求参数绑定和校验
err := c.ShouldBindJSON(&userInfoReq)
if err != nil {
common.FailWithMessage(err.Error(), c)
return
}
// 依赖倒转原则,面向抽象层进行开发
var usi service.UserService
usi = new(service.UserServiceImpl)
// 根据id查询用户信息
userInfo, err := usi.GetByID(userInfoReq.UserId)
if err != nil {
common.FailWithMessage("user not exist", c)
return
}
common.OkWithData(userInfo, c)
}
有一种相见恨晚的感觉