代码生成

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&amp;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 版本,目前支持 v1v2,默认值是 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.QueryContextjuice.QueryListContextjuice.QueryList2Contextjuice.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