代码生成 ========= juice提供了一个代码生成工具来方便开发者简化开发。 下面我们了解一下它的用法。 安装juicecli --------------- .. code-block:: shell go install github.com/go-juicedev/juicecli@latest 执行完成之后,在终端输入 `juicecli` 来验证是否安装完成。 简单使用 -------------- 准备一张user表,我这里演示的是mysql数据库 .. code-block:: sql CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 我们操作数据库的时候一般先定义一个接口 .. code-block:: go 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 .. code-block:: xml root:qwe123@tcp(localhost:3306)/database?charset=utf8mb4&parseTime=true mysql insert into user (name) values (#{name}) delete from user where id = #{param} update user set name = #{name} where id = #{id} 好,接下来就是见证奇迹的时刻。 我们执行命令 .. code-block:: shell juicecli impl --type=UserRepository --config=config.xml --namespace=main.UserRepository --output=user_repo.go 执行完毕之后,你会发现当前的目录下面多了一个user_repo.go的文件,它的具体内容如下所示。 .. code-block:: 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{} } 它自动帮我们实现了一个刚刚的定义的接口的具体的实现。 那怎么用呢? 我们补全我们刚刚的代码 .. code-block:: go 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) } 运行代码 .. code-block:: go go run . 这里注意不要 ``go run main.go`` 控制台输出 .. code-block:: shell [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 所以这个命令我们可以简化写成 .. code-block:: go juicecli impl --type=UserRepository --output=user_repo.go 或者 .. code-block:: go juicecli impl -t UserRepository -o user_repo.go 其实output也可以不写,它会默认输出到控制台。 版本参数 ---------- ``juicecli impl`` 支持通过 ``--version`` 指定生成代码的版本: .. code-block:: shell 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。 .. code-block:: go 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: .. code-block:: go 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`` 作为第一个参数: .. code-block:: go type UserRepository interface { // ✓ 正确:context.Context 作为第一个参数 GetUser(ctx context.Context, id int64) (*User, error) // ✗ 错误:缺少 context.Context GetUser(id int64) (*User, error) } 错误返回值 ~~~~~~~~~~~~~~~~~~~~ 遵循 Go 语言规范,error 必须是最后一个返回值: .. code-block:: go 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 之前: .. code-block:: go 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: .. code-block:: go 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: .. code-block:: go 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。 .. code-block:: go // 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 ----------------- .. code-block:: go //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``。