Result Mapping
Result mapping is a core feature of Juice. It converts database query results into structured Go values. Juice supports multiple mapping styles, from simple single-row cases to more advanced collection-oriented patterns.
Native sql.Rows Support
Juice is fully compatible with *database/sql.Rows through its own small Rows interface:
type Rows interface {
Next() bool
Scan(dest ...any) error
Close() error
Err() error
Columns() ([]string, error)
}
In the function signatures below, sql.Rows refers to github.com/go-juicedev/juice/sql.Rows. *database/sql.Rows implements this interface.
Basic example:
rows, err := engine.Object("main.SelectUser").QueryContext(context.TODO(), nil)
if err != nil {
panic(err)
}
defer rows.Close()
for rows.Next() {
var user User
if err := rows.Scan(&user.Id, &user.Name, &user.Age); err != nil {
panic(err)
}
fmt.Println(user)
}
if err = rows.Err(); err != nil {
panic(err)
}
About Object
Object supports multiple kinds of identifiers:
String
engine.Object("main.SelectUser")
Function
Use the function’s location in code as the statement ID.
Note
Object ID generation rules:
package.FunctionNamefor plain functionspackage.TypeName.MethodNamefor struct methodsKeep interface methods and struct methods distinct
Generic Result Mapping
Juice provides strong support for generics, making result mapping more type-safe.
Mapping Scenarios
Single column, single row
count, err := juice.NewGenericManager[int](engine). Object("CountUsers").QueryContext(context.TODO(), nil)
Multiple columns, single row
type User struct { ID int64 `column:"id"` Name string `column:"name"` } user, err := juice.NewGenericManager[User](engine). Object("GetUser").QueryContext(context.TODO(), nil)
Single column, multiple rows
ids, err := juice.NewGenericManager[[]int64](engine). Object("GetUserIDs").QueryContext(context.TODO(), nil)
Multiple columns, multiple rows
users, err := juice.NewGenericManager[[]User](engine). Object("GetUsers").QueryContext(context.TODO(), nil)
Attention
Structs should use the
columntag to map database columns.Multi-row results should be received with slice types.
Mapping directly into
mapis not supported by design.
Custom Result Mapping
Juice provides four main result-mapping helpers: Bind, List, List2, and Iter.
Bind
Bind is the most flexible option and can map many result shapes:
func Bind[T any](rows sql.Rows) (result T, err error)
Characteristics:
supports arbitrary mapping targets such as structs, slices, and scalar types
offers the most flexibility
can handle both single-row and multi-row data
Example:
type User struct {
ID int `column:"id"`
Name string `column:"name"`
}
rows, _ := db.Query("SELECT id, name FROM users")
defer rows.Close()
users, err := juice.Bind[[]User](rows)
user, err := juice.Bind[User](rows)
List
List is specialized for mapping into slice types:
func List[T any](rows sql.Rows) (result []T, err error)
Characteristics:
always returns
[]Tperforms better than
Bindfor slice-oriented use casesreturns an empty slice rather than
nilfor empty resultsincludes optimizations for non-pointer element types
Example:
rows, _ := db.Query("SELECT id, name FROM users")
defer rows.Close()
users, err := juice.List[User](rows)
List2
List2 is a List variant that returns a slice of pointers:
func List2[T any](rows sql.Rows) ([]*T, error)
It is mainly intended for pointer-oriented generic result sets.
Characteristics:
returns
[]*Tuseful when elements need to be modified later
useful for large structs
avoids value-copy overhead
Example:
rows, _ := db.Query("SELECT id, name FROM users")
defer rows.Close()
users, err := juice.List2[User](rows)
// users is []*User
Iter
Iter converts a result set into an iterator so that you do not need to load everything into memory at once.
rows, _ := db.Query("SELECT id, name FROM users")
defer rows.Close()
iterator, err := juice.Iter[User](rows)
if err != nil {
return err
}
for user, err := range iterator {
if err != nil {
return err
}
fmt.Println(user)
}
Iter returns an iter.Seq2[T, error]. Keep the rows open while iterating and close them when you are done.
Context Shortcuts
When a context carries a manager via juice.ContextWithManager, you can use shortcut helpers instead of manually creating an executor:
ctx := juice.ContextWithManager(context.Background(), engine)
user, err := juice.QueryContext[User](ctx, "GetUser", juice.H{"id": 1})
users, err := juice.QueryListContext[User](ctx, "ListUsers", nil)
userPtrs, err := juice.QueryList2Context[User](ctx, "ListUsers", nil)
iter, err := juice.QueryIterContext[User](ctx, "ListUsers", nil)
The iterator shortcut closes the underlying rows when iteration finishes or stops. The returned iterator must still be consumed; otherwise rows cannot be closed by the wrapper.
Selection Guide
Use
Bindwhen:you need maximum flexibility
you are not sure about the final shape yet
you need to handle single-row results
Use
Listwhen:you know the result is a slice
you want better performance for value slices
you prefer empty slices over
nil
Use
List2when:you need to modify elements after mapping
you are dealing with large structs
you want to avoid value copying
Use
Iterwhen:you need to process large result sets incrementally
Note
Performance notes:
Listis specially optimized for non-pointer element types.List2adds another choice for pointer-oriented code generation and usage.Bindis the most flexible, but not always the fastest.Iteris the best fit for large streaming-style workloads, but it holds a connection while you are iterating.
Auto-Increment Primary Keys
Juice supports automatically retrieving generated primary key values:
<insert id="CreateUser" useGeneratedKeys="true" keyProperty="ID">
INSERT INTO users (name, age) VALUES (#{name}, #{age})
</insert>
Requirements:
The database driver must support
LastInsertId.The parameter must be a struct pointer.
useGeneratedKeys="true"must be enabled.You must specify
keyPropertyor useautoincr:"true".The key field type must support integer assignment.
Batch Insert Optimization
Juice supports efficient batch inserts:
<insert id="BatchInsertUsers" batchSize="100">
INSERT INTO users (name, age) VALUES
<foreach collection="users" item="user" open="(" separator="," close=")">
(#{user.name}, #{user.age})
</foreach>
</insert>
For batch inserts, the parameter type must be a slice, an array, or a map with exactly one key whose value is a slice or array.
Optimization features:
Smart batching
automatically splits large datasets into batches
batch size is configurable via
batchSizesingle execution is the default behavior
Prepared-statement optimization
prepared statements are reused
at most two prepared statements are generated
database pressure is reduced effectively
Performance suggestions
recommended batch size: 50-1000
tune according to data volume and database capacity
avoid oversized batches that overload the database
Tip
Batch insert best practices:
Set batch sizes carefully.
Monitor database performance.
Consider transaction boundaries.
Handle errors thoroughly.