Code Generation
Juice provides a code-generation tool to simplify development.
This page introduces the basic workflow.
Installing juicecli
go install github.com/go-juicedev/juicecli@latest
After installation, run juicecli in the terminal to verify that it is available.
Quick Start
Prepare a user table. The example below uses MySQL:
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
In most cases, you define an interface first:
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)
}
Next, write the mapper.
Create 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>
Then run:
juicecli impl --type=UserRepository --config=config.xml --namespace=main.UserRepository --output=user_repo.go
After the command finishes, a new user_repo.go file will be generated in the current directory.
Generated example:
// 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
}
func NewUserRepository() UserRepository {
return &UserRepositoryImpl{}
}
The tool automatically generates the concrete implementation for the interface you defined.
Using the Generated Code
Now complete the earlier example:
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()
user := &User{Name: "eatmoreapple"}
result, err := userRepo.CreateUser(ctx, user)
if err != nil {
panic(err)
}
id, err := result.LastInsertId()
if err != nil {
panic(err)
}
user, err = userRepo.GetUserByID(ctx, id)
if err != nil {
panic(err)
}
fmt.Println(user)
}
Run the code:
go run .
Do not use go run main.go here.
Console output:
[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}
Command Breakdown
The command:
juicecli impl --type=UserRepository --config=config.xml --namespace=main.UserRepository --output=user_repo.go
means:
impl: generate an implementation for an interfacetype: the interface name to implementconfig: the path to the configuration filenamespace: the namespace where Juice should look for actionsoutput: the generated file name
Attention
The interface method names must match the action IDs under the specified namespace.
The command can often be simplified.
config can be omitted if juicecli can automatically find config.xml or config/config.xml near the execution path.
namespace can also be omitted. Juice can infer it from the relative path between go.mod and the Go file containing the interface.
So you can shorten the command to:
juicecli impl --type=UserRepository --output=user_repo.go
or:
juicecli impl -t UserRepository -o user_repo.go
If output is omitted, the generated code is written to standard output.
Interface Constraints
juicecli can parse interface signatures and generate implementations automatically, but the interface must follow a few rules.
Context Parameter
Every method must take context.Context as the first argument:
type UserRepository interface {
GetUser(ctx context.Context, id int64) (*User, error)
GetUserWithoutContext(id int64) (*User, error)
}
The first method is valid; the second is not.
Error Return Value
Following Go conventions, error must be the last return value:
type UserRepository interface {
CreateUser(ctx context.Context, user *User) error
UpdateUser(ctx context.Context, user *User) (sql.Result, error)
DeleteUser(ctx context.Context, id int64) (error, bool)
}
The first two signatures are valid; the third is not.
Query Operations
For select actions, the method must return data before 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
}
The first two signatures are valid. The last one is not, because a query action must return data.
Non-Query Operations
For INSERT, UPDATE, and DELETE actions, the method may return only error or sql.Result plus error:
type UserRepository interface {
DeleteUser(ctx context.Context, id int64) error
UpdateUser(ctx context.Context, user *User) (sql.Result, error)
CreateUser(ctx context.Context, user *User) (int64, error)
}
The first two are valid. The last one is not.
Parameter Handling
When a method has more than two parameters, all parameters except context are packaged into a map:
type UserRepository interface {
SearchUsers(ctx context.Context, name string, age int, city string) ([]*User, error)
}
// Internally:
params := map[string]any{
"name": name,
"age": age,
"city": city,
}
Context Requirements
Calls must use a context that carries a manager implementation:
ctx := juice.ContextWithManager(context.Background(), manager)
users, err := repo.SearchUsers(ctx, "John", 25, "New York")
Using a plain context.Background() directly is not sufficient.
Notes
juicecligenerates implementations according to these rules automatically.Invalid interface definitions may cause generation failures or runtime errors.
Parameter names affect SQL parameter mapping, so name them carefully.
go generate
You can also integrate generation with go generate:
//go:generate juicecli impl -t UserRepository -o user_repo.go
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)
}
Add that line next to your interface definition and run go generate to produce the implementation.