结果集映射

结果集映射是 Juice 框架中一个核心功能,它负责将数据库查询结果转换为 Go 语言中的结构化数据。Juice 提供了多种灵活的映射方式,从简单的单行映射到复杂的嵌套对象映射都能优雅处理。

原生SQL.Rows支持

Juice 通过自己的轻量 Rows 接口兼容 *database/sql.Rows

type Rows interface {
    Next() bool
    Scan(dest ...any) error
    Close() error
    Err() error
    Columns() ([]string, error)
}

下文函数签名中的 sql.Rows 指的是 github.com/go-juicedev/juice/sql.Rows*database/sql.Rows 实现了这个接口。

基本用法示例:

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)
}

Object方法说明

Object 方法支持多种参数类型:

  1. 字符串类型

    engine.Object("main.SelectUser")
    
  2. 函数类型: 使用函数在代码中的位置作为ID

Note

Object方法的ID生成规则: - 包名.函数名(普通函数) - 包名.类型名.方法名(结构体方法) - 注意区分interface和struct

泛型结果集映射

Juice提供了强大的泛型支持,使结果集映射更加类型安全:

映射场景示例

  1. 单字段单行

    // 查询单个计数
    count, err := juice.NewGenericManager[int](engine).
        Object("CountUsers").QueryContext(context.TODO(), nil)
    
  2. 多字段单行

    type User struct {
        ID   int64  `column:"id"`
        Name string `column:"name"`
    }
    
    // 查询单个用户
    user, err := juice.NewGenericManager[User](engine).
        Object("GetUser").QueryContext(context.TODO(), nil)
    
  3. 单字段多行

    // 查询多个ID
    ids, err := juice.NewGenericManager[[]int64](engine).
        Object("GetUserIDs").QueryContext(context.TODO(), nil)
    
  4. 多字段多行

    // 查询用户列表
    users, err := juice.NewGenericManager[[]User](engine).
        Object("GetUsers").QueryContext(context.TODO(), nil)
    

Attention

  • 结构体必须使用 column 标签指定数据库字段映射

  • 多行结果必须使用切片类型接收

  • 不支持使用map接收结果(设计选择)

自定义结果集映射

Juice 提供了四个核心的结果集映射函数:BindListList2Iter,它们各自适用于不同的场景。

Bind 函数

Bind 是最灵活的映射函数,可以处理任意类型的结果集映射:

func Bind[T any](rows sql.Rows) (result T, err error)

特点: - 支持任意类型的映射(结构体、切片、基本类型等) - 灵活性最高 - 可以处理单行或多行数据

使用示例:

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 专门用于将结果集映射为切片类型:

func List[T any](rows sql.Rows) (result []T, err error)

特点: - 始终返回切片类型 []T - 性能优于 Bind (针对切片场景) - 空结果返回空切片而不是 nil - 对非指针类型做了特殊优化

使用示例:

rows, _ := db.Query("SELECT id, name FROM users")
defer rows.Close()

users, err := juice.List[User](rows)

List2 函数

List2List 的变体,专门返回指针切片:

func List2[T any](rows sql.Rows) ([]*T, error)

List2 主要是为了做一些指针类型的泛型结果集的返回。

特点: - 返回指针切片 []*T - 适合需要修改切片元素的场景 - 适合处理大型结构体 - 避免了值拷贝开销

使用示例:

rows, _ := db.Query("SELECT id, name FROM users")
defer rows.Close()

users, err := juice.List2[User](rows)
// users 类型为 []*User

Iter 函数

Iter 专门用于将结果集转换为迭代器返回,避免一次性加载所有数据到内存中。

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 返回 iter.Seq2[T, error]。迭代期间需要保持 rows 打开,并在使用结束后关闭。

Context 快捷函数

当 context 通过 juice.ContextWithManager 携带 manager 时,可以使用快捷函数,避免手动创建 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)

迭代器快捷函数会在迭代完成或提前停止时关闭底层 rows。返回的迭代器仍然需要被消费,否则包装器无法代为关闭 rows。

选择指南

  1. 使用 Bind 当: - 需要最大的灵活性 - 不确定返回类型 - 需要处理单行数据

  2. 使用 List 当: - 确定返回切片类型 - 追求更好的性能 - 处理值类型切片

  3. 使用 List2 当: - 需要修改切片元素 - 处理大型结构体 - 想避免值拷贝

  4. 使用 Iter 当: - 需要迭代处理大量数据

Note

性能提示:

  • List 对非指针类型做了特殊优化

  • List2 虽然多一次转换,在代码生成方面多了一种选择

  • Bind 最灵活但可能不是最快的选择

  • Iter 当需要迭代大量数据时,性能最好,但是如果处理数据需要很长时间的话,会持续占用一个连接

自增主键映射

支持自动获取自增主键值:

<insert id="CreateUser" useGeneratedKeys="true" keyProperty="ID">
    INSERT INTO users (name, age) VALUES (#{name}, #{age})
</insert>

使用条件:

  1. 数据库驱动支持 LastInsertId

  2. 参数必须是结构体指针

  3. useGeneratedKeys="true"

  4. 指定 keyProperty 或使用 autoincr:"true" 标签

  5. 主键字段类型必须支持整数赋值

批量插入优化

高效的批量数据插入支持:

<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>

注意:批量插入的参数类型必须是切片、数组或者是有且仅有一个 key 的 map,并且 map 的 value 类型必须是切片或者数组

优化特性:

  1. 智能批次处理: - 自动分批处理大量数据 - 可配置批次大小(batchSize) - 默认单次执行

  2. 预编译优化: - 预编译语句复用 - 最多生成两个预编译语句 - 有效减少数据库压力

  3. 性能建议: - 建议批次大小:50-1000 - 根据数据量和数据库性能调整 - 避免过大批次造成数据库压力

Tip

批量插入最佳实践:

  1. 合理设置批次大小

  2. 注意监控数据库性能

  3. 考虑事务管理

  4. 做好错误处理