golang使用泛型将SQL行扫描到任意类型的插件库scan的使用

Golang 使用泛型将 SQL 行扫描到任意类型的插件库 scan 的使用

简介

Scan 是一个 Golang 包,它提供了一种方便灵活的方式,利用泛型的强大功能将 SQL 行扫描到任何类型中。

特性

  • 高效且可重用:避免重复代码,在一个地方定义列映射
  • 自动关闭:无需担心资源泄漏
  • 无反射:比基于反射的映射器更快
  • 健壮的错误处理:管理错误的最佳实践

使用示例

基本用法

首先定义你的数据结构,然后创建列映射:

import "github.com/wroge/scan"

type Author struct {
	ID   int64
	Name string
}

type Post struct {
	ID      int64
	Title   string
	Authors []Author
}

// 定义数据库列到结构体字段的映射
var columns = scan.Columns[Post]{
	// 将 'id' 列映射到 'Post' 结构体中的 'ID' 字段
	// 使用 'scan.Any' 函数直接赋值,无需额外处理
	"id": scan.Any(func(p *Post, id int64) { p.ID = id }),

	// 将 'title' 列映射到 'Post' 结构体中的 'Title' 字段
	// 'scan.Null' 函数允许处理可为空的数据库列
	// 如果 'title' 列为 null,则使用 'default title' 作为值
	"title": scan.Null("default title", func(p *Post, title string) { p.Title = title }),

	// 将 'authors' 列(预期为 JSON 格式)映射到 'Post' 结构体中的 'Authors' 字段
	// 'scan.JSON' 函数自动将 JSON 数据解组到 'Author' 结构体切片中
	"authors": scan.JSON(func(p *Post, authors []Author) { p.Authors = authors }),

	// 或者你可以用这个函数创建一个自定义扫描器
	// "column": scan.Func[Post, V](func(p *Post, value V) error {
	// 	return nil
	// }),
}

rows, err := db.Query("SELECT ...")
// 处理错误

扫描所有行

posts, err := scan.All(rows, columns)
// 处理错误

扫描第一行

post, err := scan.First(rows, columns)
if err != nil {
	if errors.Is(err, scan.ErrNoRows) {
		// 处理无行情况
	}

	// 处理其他错误
}

扫描恰好一行

post, err := scan.One(rows, columns)
if err != nil {
	if errors.Is(err, scan.ErrTooManyRows) {
		// 处理行数过多的情况
		// post 是有效的
	}

	if errors.Is(err, scan.ErrNoRows) {
		// 处理无行情况
	}

	// 处理其他错误
}

扫描有限数量的行

posts, err := scan.Limit(10, rows, columns)
if err != nil {
	if errors.Is(err, scan.ErrTooManyRows) {
		// 如果结果集超过 10 行则忽略
		// len(posts) == 10
	}

	// 处理其他错误
}

直接使用迭代器

iter, err := scan.Iter(rows, columns)
// 处理错误

defer iter.Close()

for iter.Next() {
	var post Post

	err = iter.Scan(&post)
	// 处理错误

	// 或者使用 Value 方法:
	post, err := iter.Value()
	// 处理错误
}

这个库提供了一种类型安全且高效的方式来处理数据库查询结果,避免了传统 ORM 或反射方法的许多缺点。通过使用泛型,它能够在编译时捕获类型错误,同时保持代码的简洁性和可读性。


更多关于golang使用泛型将SQL行扫描到任意类型的插件库scan的使用的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于golang使用泛型将SQL行扫描到任意类型的插件库scan的使用的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


使用泛型将SQL行扫描到任意类型 - scan库详解

在Go语言中,使用database/sql包时,我们需要手动将SQL查询结果扫描到结构体或变量中,这个过程通常比较繁琐。scan是一个优秀的第三方库,它利用Go 1.18引入的泛型特性,简化了这个过程。

scan库简介

scan库提供了一种类型安全的方式来将SQL行扫描到Go结构体、切片或基本类型中。主要特点包括:

  • 支持泛型,编译时类型检查
  • 自动映射列名到结构体字段
  • 支持嵌套结构体
  • 轻量级且高性能

安装

go get github.com/georgysavva/scany/v2

基本用法

1. 扫描单行到结构体

package main

import (
	"context"
	"database/sql"
	"fmt"
	"log"

	"github.com/georgysavva/scany/v2/dbscan"
	_ "github.com/lib/pq"
)

type User struct {
	ID       int
	Name     string
	Email    string
	Age      int
	IsActive bool
}

func main() {
	db, err := sql.Open("postgres", "user=postgres dbname=test sslmode=disable")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	// 创建scan适配器
	adapter := dbscan.NewDBScanAdapter(db)

	// 查询单行
	var user User
	err = adapter.ScanOne(context.Background(), &user, "SELECT * FROM users WHERE id = $1", 1)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("User: %+v\n", user)
}

2. 扫描多行到切片

func getActiveUsers(db *sql.DB) ([]User, error) {
	adapter := dbscan.NewDBScanAdapter(db)
	
	var users []User
	err := adapter.ScanAll(context.Background(), &users, "SELECT * FROM users WHERE is_active = true")
	if err != nil {
		return nil, err
	}
	
	return users, nil
}

3. 扫描到基本类型

func countUsers(db *sql.DB) (int, error) {
	adapter := dbscan.NewDBScanAdapter(db)
	
	var count int
	err := adapter.ScanOne(context.Background(), &count, "SELECT COUNT(*) FROM users")
	if err != nil {
		return 0, err
	}
	
	return count, nil
}

高级特性

自定义列名映射

默认情况下,scan会将蛇形列名(user_name)映射到驼峰结构体字段(UserName)。如果需要自定义:

type Product struct {
	ID          int    `db:"product_id"`
	Name        string `db:"product_name"`
	Price       float64
	Description string `db:"-"`
}

// 使用自定义标签
opts := &dbscan.ScanOptions{
	TagName: "db",
}
adapter := dbscan.NewDBScanAdapter(db, dbscan.WithScanOptions(opts))

处理NULL值

type NullableUser struct {
	ID        int
	Name      sql.NullString
	BirthDate sql.NullTime
	Rating    sql.NullFloat64
}

嵌套结构体

type Address struct {
	Street  string
	City    string
	Country string
}

type Customer struct {
	ID      int
	Name    string
	Address Address `db:"address_"`
}

// 查询需要返回address_street, address_city, address_country列

性能考虑

scan库在性能上做了优化:

  1. 使用反射但缓存反射结果
  2. 预分配切片内存
  3. 最小化内存分配

对于极高性能要求的场景,仍然可以考虑手写扫描代码,但对于大多数应用,scan的性能损失可以忽略不计。

错误处理

scan会返回详细的错误信息,包括:

  • 类型不匹配
  • 缺少字段
  • 数据库错误
  • NULL值处理错误

建议始终检查返回的错误并进行适当处理。

总结

scan库通过Go泛型提供了类型安全、简洁的SQL行扫描方案,大大减少了样板代码。它特别适合:

  • 快速开发CRUD应用
  • 需要处理复杂嵌套结构的场景
  • 想要减少错误和提高代码可读性的项目

虽然学习曲线略高于直接使用database/sql,但长期来看能显著提高开发效率和代码质量。

回到顶部