Gen是一个基于GORM的安全ORM框架,其主要通过代码生成方式实现GORM代码封装。使用Gen框架能够自动生成Model结构体和类型安全的CRUD代码,极大提升CRUD效率。

Gen介绍

Gen是由字节跳动无恒实验室与GORM作者联合研发的一个基于GORM的安全ORM框架,主要通过代码生成方式实现GORM代码封装。

Gen框架在GORM框架的基础上提供了以下能力:

  • 基于原始SQL语句生成可重用的CRUD API
  • 生成不使用interface{}的100%安全的DAO API
  • 依据数据库生成遵循GORM约定的结构体Model
  • 支持GORM的所有特性

简单来说,使用Gen框架后我们无需手动定义结构体Model,同时Gen框架也能帮我们生成类型安全的CRUD代码。

更多详细介绍请查看Gen官方文档

此外,Facebook开源的ent也是社区中常用的类似框架,大家可按需选择使用。

如何使用Gen

Gen框架的使用非常简单,如果你熟悉GORM框架,那么你可以通过以下教程快速上手。

安装依赖

go get -u gorm.io/gen

快速指南

想要在项目中使用Gen框架,通常只需三步。本节将通过一个简单示例快速带大家熟悉Gen框架的使用。

首先,我们假设数据库中已经有一张book表,建表语句如下。

CREATE TABLE book
(
    `id`     bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
    `title`  varchar(128) NOT NULL COMMENT '书籍名称',
    `author` varchar(128) NOT NULL COMMENT '作者',
    `price`  int NOT NULL DEFAULT '0' COMMENT '价格',
    `publish_date` datetime COMMENT '出版日期',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='书籍表';

本教程演示的为先有数据表的业务场景,通常这也是比较主流的工程实现流程。

定义Gen配置

配置即代码。我们通常会在项目的cmd目录下定义好Gen框架生成代码的配置。例如,我们的项目名称为gen_demo,那么我们就在gen_demo/cmd/gen/generate.go文件。

package main

// gorm gen configure

import (
	"fmt"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"

	"gorm.io/gen"
)

const MySQLDSN = "root:root1234@tcp(127.0.0.1:13306)/db2?charset=utf8mb4&parseTime=True"

func connectDB(dsn string) *gorm.DB {
	db, err := gorm.Open(mysql.Open(dsn))
	if err != nil {
		panic(fmt.Errorf("connect db fail: %w", err))
	}
	return db
}

func main() {
	// 指定生成代码的具体相对目录(相对当前文件),默认为:./query
	// 默认生成需要使用WithContext之后才可以查询的代码,但可以通过设置gen.WithoutContext禁用该模式
	g := gen.NewGenerator(gen.Config{
		// 默认会在 OutPath 目录生成CRUD代码,并且同目录下生成 model 包
		// 所以OutPath最终package不能设置为model,在有数据库表同步的情况下会产生冲突
		// 若一定要使用可以通过ModelPkgPath单独指定model package的名称
		OutPath: "../../dal/query",
		/* ModelPkgPath: "dal/model"*/

		// gen.WithoutContext:禁用WithContext模式
		// gen.WithDefaultQuery:生成一个全局Query对象Q
		// gen.WithQueryInterface:生成Query接口
		Mode: gen.WithDefaultQuery | gen.WithQueryInterface,
	})

	// 通常复用项目中已有的SQL连接配置db(*gorm.DB)
	// 非必需,但如果需要复用连接时的gorm.Config或需要连接数据库同步表信息则必须设置
	g.UseDB(connectDB(MySQLDSN))

	// 从连接的数据库为所有表生成Model结构体和CRUD代码
	// 也可以手动指定需要生成代码的数据表
	g.ApplyBasic(g.GenerateAllTable()...)

	// 执行并生成代码
	g.Execute()
}

为什么要放到 cmd目录下?👉 Go官方模块布局说明

生成代码

进入项目下的cmd/gen目录下,执行以下命令。

go run generate.go

上述命令会在项目目录下生成dal目录,其中dal/query中是CRUD代码,dal/model下则是生成Model结构体。

├── cmd
│   └── gen
│       └── generate.go
├── dal
│   ├── model
│   │   └── book.gen.go
│   └── query
│       ├── book.gen.go
│       └── gen.go
├── go.mod
├── go.sum
└── main.go

我们可以在dal下新建db.go文件,保存如下初始化数据库连接的代码。

package dal

import (
	"fmt"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

var DB *gorm.DB

func ConnectDB(dsn string) *gorm.DB {
	db, err := gorm.Open(mysql.Open(dsn))
	if err != nil {
		panic(fmt.Errorf("connect db fail: %w", err))
	}
	return db
}

注意:通常不建议直接修改Gen框架生成的代码。

使用生成的代码

Gen会生成基础的查询方法,并且绑定到结构体上,我们可以在项目中使用了它们。

package main

import (
	"context"
	"fmt"
	"gen_demo/dal"
	"gen_demo/dal/model"
	"gen_demo/dal/query"
	"time"
)

// gen demo

// MySQLDSN MySQL data source name
const MySQLDSN = "root:root1234@tcp(127.0.0.1:13306)/db2?charset=utf8mb4&parseTime=True"

func init() {
	dal.DB = dal.ConnectDB(MySQLDSN).Debug()
}

func main() {
	// 设置默认DB对象
	query.SetDefault(dal.DB)

	// 创建
	b1 := model.Book{
		Title:       "《七米的Go语言之路》",
		Author:      "七米",
		PublishDate: time.Date(2023, 11, 15, 0, 0, 0, 0, time.UTC),
		Price:       100,
	}
	err := query.Book.WithContext(context.Background()).Create(&b1)
	if err != nil {
		fmt.Printf("create book fail, err:%v\n", err)
		return
	}

	// 更新
	ret, err := query.Book.WithContext(context.Background()).
		Where(query.Book.ID.Eq(1)).
		Update(query.Book.Price, 200)
	if err != nil {
		fmt.Printf("update book fail, err:%v\n", err)
		return
	}
	fmt.Printf("RowsAffected:%v\n", ret.RowsAffected)

	// 查询
	book, err := query.Book.WithContext(context.Background()).First()
	// 也可以使用全局Q对象查询
	//book, err := query.Q.Book.WithContext(context.Background()).First()
	if err != nil {
		fmt.Printf("query book fail, err:%v\n", err)
		return
	}
	fmt.Printf("book:%v\n", book)

	// 删除
	ret, err = query.Book.WithContext(context.Background()).Where(query.Book.ID.Eq(1)).Delete()
	if err != nil {
		fmt.Printf("delete book fail, err:%v\n", err)
		return
	}
	fmt.Printf("RowsAffected:%v\n", ret.RowsAffected)
}

通过上述教程,基本即可掌握Gen框架的基本使用,大家可点击查看Gen官方最佳实践示例代码

自定义SQL查询

Gen框架使用模板注释的方法支持自定义SQL查询,我们只需要按对应规则将SQL语句注释到interface的方法上即可。Gen将对其进行解析,并为应用的结构生成查询API。

通常建议将自定义查询方法添加到model模块下。

注释语法

Gen 为动态条件 SQL 支持提供了一些约定语法,分为三个方面:

  • 返回结果
  • 模板占位符
  • 模板表达式
返回结果
占位符 含义
gen.T 用于返回数据的结构体,会根据生成结构体或者数据库表结构自动生成
gen.M 表示map[string]interface{},用于返回数据
gen.RowsAffected 用于执行SQL进行更新或删除时候,用于返回影响行数
error 返回错误(如果有)

示例

// dal/model/querier.go

package model

import "gorm.io/gen"

// 通过添加注释生成自定义方法

type Querier interface {
	// SELECT * FROM @@table WHERE id=@id
	GetByID(id int) (gen.T, error) // 返回结构体和error

	// GetByIDReturnMap 根据ID查询返回map
	//
	// SELECT * FROM @@table WHERE id=@id
	GetByIDReturnMap(id int) (gen.M, error) // 返回 map 和 error

	// SELECT * FROM @@table WHERE author=@author
	GetBooksByAuthor(author string) ([]*gen.T, error) // 返回数据切片和 error
}

在Gen配置处(cmd/gen/generate.go)添加自定义方法绑定关系。

// 通过ApplyInterface添加为book表添加自定义方法
g.ApplyInterface(func(model.Querier) {}, g.GenerateModel("book"))

重新生成代码后,即可使用自定义方法。

// 使用自定义的GetBooksByAuthor方法
rets, err := query.Book.WithContext(context.Background()).GetBooksByAuthor("七米")
if err != nil {
	fmt.Printf("GetBooksByAuthor fail, err:%v\n", err)
	return
}
for i, b := range rets {
	fmt.Printf("%d:%v\n", i, b)
}
模板占位符
名称 描述
@@table 转义和引用表名
@@<name> 从参数中转义并引用表/列名
@<name> 参数中的SQL查询参数

示例

// Filter 自定义Filter接口
type Filter interface {
  // SELECT * FROM @@table WHERE @@column=@value
  FilterWithColumn(column string, value string) (gen.T, error)
}

// 为`Book`添加 `Filter`接口
g.ApplyInterface(func(model.Filter) {}, g.GenerateModel("book"))
模板表达式

Gen 为动态条件 SQL 提供了强大的表达式支持,目前支持以下表达式:

  • if/else
  • where
  • set
  • for

示例

// Searcher 自定义接口
type Searcher interface {
	// Search 根据指定条件查询书籍
	//
	// SELECT * FROM book
	// WHERE publish_date is not null
	// {{if book != nil}}
	//   {{if book.ID > 0}}
	//     AND id = @book.ID
	//   {{else if book.Author != ""}}
	//     AND author=@book.Author
	//   {{end}}
	// {{end}}
	Search(book *gen.T) ([]*gen.T, error)
}

// 通过ApplyInterface添加为book表添加Searcher接口
g.ApplyInterface(func(model.Searcher) {}, g.GenerateModel("book"))

重新生成代码后,即可直接使用自定义的Search方法进行查询。

b := &model.Book{Author: "Q1mi"}
rets, err = query.Book.WithContext(context.Background()).Search(b)
if err != nil {
	fmt.Printf("Search fail, err:%v\n", err)
	return
}
for i, b := range rets {
	fmt.Printf("%d:%v\n", i, b)
}

数据库到结构体

Gen支持根据GORM约定依据数据库生成结构体,在之前的示例中我们已经使用过类似的代码。

// 根据`users`表生成对应结构体`User`
g.GenerateModel("users")

// 基于`users`表生成名为`Employee`的结构体
g.GenerateModelAs("users", "Employee")

// 在生成结构体时还可指定额外的生成选项
// gen.FieldIgnore("address"):忽略 address 字段
// gen.FieldType("id", "int64"):id字段使用 int64 类型
g.GenerateModel("users", gen.FieldIgnore("address"), gen.FieldType("id", "int64"))

// 为连接的数据库中的所有表生成对应结构体
g.GenerateAllTable()

方法模板

当从数据库生成结构体时,还可以为它们生成事先配置的模板方法,例如:

type CommonMethod struct {
    ID   int32
    Name *string
}

func (m *CommonMethod) IsEmpty() bool {
    if m == nil {
        return true
    }
    return m.ID == 0
}

func (m *CommonMethod) GetName() string {
    if m == nil || m.Name == nil {
        return ""
    }
    return *m.Name
}

// 当生成 `People` 结构体时添加 IsEmpty 方法
g.GenerateModel("people", gen.WithMethod(CommonMethod{}.IsEmpty))

// 生成`User`结构体时添加 `CommonMethod` 的所有方法
g.GenerateModel("user", gen.WithMethod(CommonMethod{}))

最终将生成类下面的代码。

// Generated Person struct
type Person struct {
  // ...
}

func (m *Person) IsEmpty() bool {
  if m == nil {
    return true
  }
  return m.ID == 0
}


// Generated User struct
type User struct {
  // ...
}

func (m *User) IsEmpty() bool {
  if m == nil {
    return true
  }
  return m.ID == 0
}

func (m *User) GetName() string {
  if m == nil || m.Name == nil {
    return ""
  }
  return *m.Name
}

数据映射

可以自行指定字段类型和数据库列类型之间的数据类型映射。

在某些业务场景下,这个功能非常有用,例如,我们希望将数据库中数字列在生成结构体时都定义为int64类型。

var dataMap = map[string]func(gorm.ColumnType) (dataType string){
  // int mapping
  "int": func(columnType gorm.ColumnType) (dataType string) {
    if n, ok := columnType.Nullable(); ok && n {
      return "*int32"
    }
    return "int32"
  },

  // bool mapping
  "tinyint": func(columnType gorm.ColumnType) (dataType string) {
    ct, _ := columnType.ColumnType()
    if strings.HasPrefix(ct, "tinyint(1)") {
      return "bool"
    }
    return "byte"
  },
}

g.WithDataTypeMap(dataMap)

从 SQL语句生成结构体

Gen 支持遵循 GORM 约定从 sql 生成结构体,具体用法如下。

package main

import (
	"gorm.io/gen"
	"gorm.io/gorm"
	"gorm.io/rawsql"
)

func main() {
	g := gen.NewGenerator(gen.Config{
		OutPath: "../query",
		Mode:    gen.WithoutContext | gen.WithDefaultQuery | gen.WithQueryInterface, // generate mode
	})
	// https://github.com/go-gorm/rawsql/blob/master/tests/gen_test.go
	gormdb, _ := gorm.Open(rawsql.New(rawsql.Config{
		//SQL:      rawsql,     // create table sql
		FilePath: []string{
			//"./sql/user.sql", // create table sql file
			"./test_sql", // create table sql file directory
		},
	}))
	g.UseDB(gormdb) // reuse your gorm db

	// Generate basic type-safe DAO API for struct `model.User` following conventions

	g.ApplyBasic(
		// Generate struct `User` based on table `users`
		g.GenerateModel("users"),

		// Generate struct `Employee` based on table `users`
		g.GenerateModelAs("users", "Employee"),
	)
	g.ApplyBasic(
		// Generate structs from all tables of current database
		g.GenerateAllTable()...,
	)
	// Generate the code
	g.Execute()
}

关于Gen框架的更多技巧,推荐查看官方文档


扫码关注微信公众号