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:
.. code-block:: go
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:
.. code-block:: go
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:
1. **String**
.. code-block:: go
engine.Object("main.SelectUser")
2. **Function**
Use the function's location in code as the statement ID.
.. note::
``Object`` ID generation rules:
- ``package.FunctionName`` for plain functions
- ``package.TypeName.MethodName`` for struct methods
- Keep interface methods and struct methods distinct
Generic Result Mapping
----------------------
Juice provides strong support for generics, making result mapping more type-safe.
Mapping Scenarios
"""""""""""""""""
1. **Single column, single row**
.. code-block:: go
count, err := juice.NewGenericManager[int](engine).
Object("CountUsers").QueryContext(context.TODO(), nil)
2. **Multiple columns, single row**
.. code-block:: go
type User struct {
ID int64 `column:"id"`
Name string `column:"name"`
}
user, err := juice.NewGenericManager[User](engine).
Object("GetUser").QueryContext(context.TODO(), nil)
3. **Single column, multiple rows**
.. code-block:: go
ids, err := juice.NewGenericManager[[]int64](engine).
Object("GetUserIDs").QueryContext(context.TODO(), nil)
4. **Multiple columns, multiple rows**
.. code-block:: go
users, err := juice.NewGenericManager[[]User](engine).
Object("GetUsers").QueryContext(context.TODO(), nil)
.. attention::
- Structs should use the ``column`` tag to map database columns.
- Multi-row results should be received with slice types.
- Mapping directly into ``map`` is 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:
.. code-block:: go
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:
.. code-block:: go
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:
.. code-block:: go
func List[T any](rows sql.Rows) (result []T, err error)
Characteristics:
- always returns ``[]T``
- performs better than ``Bind`` for slice-oriented use cases
- returns an empty slice rather than ``nil`` for empty results
- includes optimizations for non-pointer element types
Example:
.. code-block:: go
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:
.. code-block:: go
func List2[T any](rows sql.Rows) ([]*T, error)
It is mainly intended for pointer-oriented generic result sets.
Characteristics:
- returns ``[]*T``
- useful when elements need to be modified later
- useful for large structs
- avoids value-copy overhead
Example:
.. code-block:: go
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.
.. code-block:: go
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:
.. code-block:: go
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
"""""""""""""""
1. Use ``Bind`` when:
- you need maximum flexibility
- you are not sure about the final shape yet
- you need to handle single-row results
2. Use ``List`` when:
- you know the result is a slice
- you want better performance for value slices
- you prefer empty slices over ``nil``
3. Use ``List2`` when:
- you need to modify elements after mapping
- you are dealing with large structs
- you want to avoid value copying
4. Use ``Iter`` when:
- you need to process large result sets incrementally
.. note::
Performance notes:
- ``List`` is specially optimized for non-pointer element types.
- ``List2`` adds another choice for pointer-oriented code generation and usage.
- ``Bind`` is the most flexible, but not always the fastest.
- ``Iter`` is 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:
.. code-block:: xml
INSERT INTO users (name, age) VALUES (#{name}, #{age})
Requirements:
1. The database driver must support ``LastInsertId``.
2. The parameter must be a struct pointer.
3. ``useGeneratedKeys="true"`` must be enabled.
4. You must specify ``keyProperty`` or use ``autoincr:"true"``.
5. The key field type must support integer assignment.
Batch Insert Optimization
-------------------------
Juice supports efficient batch inserts:
.. code-block:: xml
INSERT INTO users (name, age) VALUES
(#{user.name}, #{user.age})
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:
1. **Smart batching**
- automatically splits large datasets into batches
- batch size is configurable via ``batchSize``
- single execution is the default behavior
2. **Prepared-statement optimization**
- prepared statements are reused
- at most two prepared statements are generated
- database pressure is reduced effectively
3. **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:
1. Set batch sizes carefully.
2. Monitor database performance.
3. Consider transaction boundaries.
4. Handle errors thoroughly.