代码生成
juice提供了一个代码生成工具来方便开发者简化开发。
下面我们了解一下它的用法。
安装juicecli
go install github.com/go-juicedev/juicecli@latest
执行完成之后,在终端输入 juicecli 来验证是否安装完成。
简单使用
准备一张user表,我这里演示的是mysql数据库
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
我们操作数据库的时候一般先定义一个接口
package main
import (
"context"
"database/sql"
"github.com/go-juicedev/juice"
_ "github.com/go-sql-driver/mysql"
)
type User struct {
ID int64 `column:"id" autoincr:"true"`
Name string `param:"name" column:"name"`
}
type UserRepository interface {
CreateUser(ctx context.Context, user *User) (sql.Result, error)
DeleteUserByID(ctx context.Context, id int64) (sql.Result, error)
UpdateUserNameByID(ctx context.Context, id int64, name string) (sql.Result, error)
GetUserByID(ctx context.Context, id int64) (*User, error)
}
如上所示,我们定义user表的增删改查接口,接下来我们去写mapper
新建一个config.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<environments default="prod">
<environment id="prod">
<dataSource>root:qwe123@tcp(localhost:3306)/database?charset=utf8mb4&parseTime=true</dataSource>
<driver>mysql</driver>
</environment>
</environments>
<mappers>
<mapper namespace="main.UserRepository">
<insert id="CreateUser">
insert into user (name) values (#{name})
</insert>
<delete id="DeleteUserByID">
delete from user where id = #{param}
</delete>
<update id="UpdateUserNameByID">
update user set name = #{name} where id = #{id}
</update>
<select id="GetUserByID">
select * from user where id = #{param}
</select>
</mapper>
</mappers>
</configuration>
好,接下来就是见证奇迹的时刻。
我们执行命令
juicecli impl --type=UserRepository --config=config.xml --namespace=main.UserRepository --output=user_repo.go
执行完毕之后,你会发现当前的目录下面多了一个user_repo.go的文件,它的具体内容如下所示。
// Code generated by "juicecli impl --type=UserRepository --config=config.xml --namespace=main.UserRepository --output=user_repo.go"; DO NOT EDIT.
package main
import (
"context"
"database/sql"
"github.com/go-juicedev/juice"
)
type UserRepositoryImpl struct{}
func (u UserRepositoryImpl) CreateUser(ctx context.Context, user *User) (result0 sql.Result, result1 error) {
manager, err := juice.ManagerFromContext(ctx)
if err != nil {
return nil, err
}
var iface UserRepository = u
executor := juice.NewGenericManager[any](manager).Object(iface.CreateUser)
return executor.ExecContext(ctx, user)
}
func (u UserRepositoryImpl) DeleteUserByID(ctx context.Context, id int64) (result0 sql.Result, result1 error) {
manager, err := juice.ManagerFromContext(ctx)
if err != nil {
return nil, err
}
var iface UserRepository = u
executor := juice.NewGenericManager[any](manager).Object(iface.DeleteUserByID)
return executor.ExecContext(ctx, id)
}
func (u UserRepositoryImpl) UpdateUserNameByID(ctx context.Context, id int64, name string) (result0 sql.Result, result1 error) {
manager, err := juice.ManagerFromContext(ctx)
if err != nil {
return nil, err
}
var iface UserRepository = u
executor := juice.NewGenericManager[any](manager).Object(iface.UpdateUserNameByID)
return executor.ExecContext(ctx, juice.H{"id": id, "name": name})
}
func (u UserRepositoryImpl) GetUserByID(ctx context.Context, id int64) (result0 *User, result1 error) {
manager, err := juice.ManagerFromContext(ctx)
if err != nil {
return nil, err
}
var iface UserRepository = u
executor := juice.NewGenericManager[User](manager).Object(iface.GetUserByID)
ret, err := executor.QueryContext(ctx, id)
return &ret, err
}
// NewUserRepository returns a new UserRepository.
func NewUserRepository() UserRepository {
return &UserRepositoryImpl{}
}
它自动帮我们实现了一个刚刚的定义的接口的具体的实现。
那怎么用呢?
我们补全我们刚刚的代码
package main
import (
"context"
"database/sql"
"fmt"
"github.com/go-juicedev/juice"
_ "github.com/go-sql-driver/mysql"
)
type User struct {
ID int64 `column:"id" autoincr:"true"`
Name string `param:"name" column:"name"`
}
type UserRepository interface {
CreateUser(ctx context.Context, user *User) (sql.Result, error)
DeleteUserByID(ctx context.Context, id int64) (sql.Result, error)
UpdateUserNameByID(ctx context.Context, id int64, name string) (sql.Result, error)
GetUserByID(ctx context.Context, id int64) (*User, error)
}
func main() {
cfg, err := juice.NewXMLConfiguration("config.xml")
if err != nil {
panic(err)
}
engine, err := juice.Default(cfg)
if err != nil {
panic(err)
}
ctx := juice.ContextWithManager(context.Background(), engine)
userRepo := NewUserRepository()
// create user
user := &User{
Name: "eatmoreapple",
}
result, err := userRepo.CreateUser(ctx, user)
if err != nil {
panic(err)
}
id, err := result.LastInsertId()
if err != nil {
panic(err)
}
// get user
user, err = userRepo.GetUserByID(ctx, id)
if err != nil {
panic(err)
}
fmt.Println(user)
}
运行代码
go run .
这里注意不要 go run main.go
控制台输出
[juice] 2023/06/13 14:41:05 [main.UserRepository.CreateUser] insert into user (name) values (?) [eatmoreapple] 6.745625ms
[juice] 2023/06/13 14:41:05 [main.UserRepository.GetUserByID] select * from user where id = ? [1] 483.166µs
&{1 eatmoreapple}
好了,现在来解释一下我们刚刚的命令是什么意思?
impl: 表示我们需要生成接口的实现
type: 指定我们要生成哪个接口的实现,这里填接口的名字。
config: 指定我们配置文件的路径名。
namespace: 表示我们去配置文件的那里去找我们需要实现的action。
output: 我们生成的文件的名字。
version: 指定生成代码面向的 Juice 版本,目前支持
v1和v2,默认值是v1。
Attention
接口定义的名字必须和指定的namespace下的action的id一致。
其实这个命令我们可以简化一下。
config 可以指定,它会自动从执行命令的路径的同级目录下去找有没有 config.xml 或者 config/config.xml 这个文件。
namespace 也可以不指定,它会自动去找go.mod这个文件和你接口定义的go文件中间的相对路径,将它作为namespace
所以这个命令我们可以简化写成
juicecli impl --type=UserRepository --output=user_repo.go
或者
juicecli impl -t UserRepository -o user_repo.go
其实output也可以不写,它会默认输出到控制台。
版本参数
juicecli impl 支持通过 --version 指定生成代码的版本:
juicecli impl -t UserRepository -o user_repo.go --version v2
--version 当前支持两个值:
v1:默认值。生成的实现结构体不保存juice.Manager,方法执行时会从传入的context.Context中读取 manager。因此调用接口方法前,需要先使用juice.ContextWithManager把 manager 放进 context。v2:生成的实现结构体会保存juice.Manager,构造函数也会接收 manager,例如NewUserRepository(manager juice.Manager)。方法内部会自动把 manager 注入到 context,再调用juice.QueryContext、juice.QueryListContext、juice.QueryList2Context或juice.ExecContext。
也就是说,v2 更适合依赖注入场景:repository 创建时绑定 manager,业务代码调用方法时可以直接传普通的 context。
manager, err := juice.DefaultFromFile("config.xml")
if err != nil {
panic(err)
}
repo := NewUserRepository(manager)
user, err := repo.GetUserByID(context.Background(), 1)
if err != nil {
panic(err)
}
如果使用默认的 v1,则需要在调用前手动注入 manager:
manager, err := juice.DefaultFromFile("config.xml")
if err != nil {
panic(err)
}
repo := NewUserRepository()
ctx := juice.ContextWithManager(context.Background(), manager)
user, err := repo.GetUserByID(ctx, 1)
--version 只影响生成代码的结构和 manager 获取方式,不改变接口方法约束、mapper namespace 匹配规则和 SQL 参数映射规则。
接口约束
juicecli 工具可以自动解析接口签名并生成实现,但接口定义必须遵循以下规范。
Context 参数
所有接口方法必须将 context.Context 作为第一个参数:
type UserRepository interface {
// ✓ 正确:context.Context 作为第一个参数
GetUser(ctx context.Context, id int64) (*User, error)
// ✗ 错误:缺少 context.Context
GetUser(id int64) (*User, error)
}
错误返回值
遵循 Go 语言规范,error 必须是最后一个返回值:
type UserRepository interface {
// ✓ 正确:error 作为最后一个返回值
CreateUser(ctx context.Context, user *User) error
UpdateUser(ctx context.Context, user *User) (sql.Result, error)
// ✗ 错误:error 不是最后一个返回值
DeleteUser(ctx context.Context, id int64) (error, bool)
}
查询操作 (action=”select”)
必须有数据返回值,且在 error 之前:
type UserRepository interface {
// ✓ 正确:有查询结果返回值
GetUser(ctx context.Context, id int64) (*User, error)
ListUsers(ctx context.Context) ([]*User, error)
// ✗ 错误:查询操作缺少结果返回值
FindUser(ctx context.Context, id int64) error
}
非查询操作 (INSERT/UPDATE/DELETE)
可以只返回 error,或返回 sql.Result:
type UserRepository interface {
// ✓ 正确:只返回 error
DeleteUser(ctx context.Context, id int64) error
// ✓ 正确:返回 sql.Result 和 error
UpdateUser(ctx context.Context, user *User) (sql.Result, error)
// ✗ 错误:非查询操作返回值类型错误
CreateUser(ctx context.Context, user *User) (int64, error)
}
参数处理
当参数超过两个时,除 context 外的参数会被封装为 map:
type UserRepository interface {
// 定义的接口方法
SearchUsers(ctx context.Context, name string, age int, city string) ([]*User, error)
}
// Juice 内部处理为:
params := map[string]any{
"name": name,
"age": age,
"city": city,
}
Context 要求
接口方法的第一个参数仍然必须是 context.Context。manager 的传递方式取决于生成版本:
v1:调用前必须使用juice.ContextWithManager把 manager 注入到 context。v2:repository 创建时已经绑定 manager,方法内部会自动注入,调用时可以直接传普通 context。
// v1:需要手动注入 manager
repo := NewUserRepository()
ctx := juice.ContextWithManager(context.Background(), manager)
users, err := repo.SearchUsers(ctx, "John", 25, "New York")
// v2:构造时传入 manager,调用时可直接使用普通 context
repo := NewUserRepository(manager)
users, err := repo.SearchUsers(context.Background(), "John", 25, "New York")
注意事项
juicecli 工具会根据这些规范自动生成实现代码
不符合规范的接口定义可能导致生成失败或运行时错误
参数名称会影响生成的 SQL 参数映射,请确保命名准确
go generate
//go:generate juicecli impl -t UserRepository -o user_repo.go --version v2
type UserRepository interface {
CreateUser(ctx context.Context, user *User) (sql.Result, error)
DeleteUserByID(ctx context.Context, id int64) (sql.Result, error)
UpdateUserNameByID(ctx context.Context, id int64, name string) (sql.Result, error)
GetUserByID(ctx context.Context, id int64) (*User, error)
}
在你的接口定义处写上这么一句,然后在控制台执行 go generate 即可生成对应的代码。如果仍然希望生成默认的 v1 代码,可以省略 --version v2。