Go1.18ジェネリックを使用したGormのページネーションラッパー

1はじめに

先月、ブログ「リフレクションメカニズムを使用してゴームページングをカプセル化する」を作成し、Goのリフレクションメカニズムを使用して、春にMybatis-Plusスタイルに似たページング機能を実装する方法を記録しました。 Go 1.18がリリースされました。ジェネリックはカプセル化を実装するために使用されます。先週、Go 1.18の公式バージョンがリリースされました。リフレクションの代わりにジェネリックを使用して、Gormのページングカプセル化を実装してみましょう。セクション4では、リストの例を使用してジェネリックスの基本的な使用法を紹介します。セクション5では、ジェネリックスを使用してページングのコアコードをカプセル化します。この記事の完全な実行可能コードについては、私のGitHubを参照してください。

2プロジェクトの構造

今回のデモ構造は前回と同様で、Ginで構築されていますが、ある程度最適化されています。これは、MySQL8で構築されたワールドデータベースのcountryandcityテーブルのページネーションクエリでもあります。Mysqlにそのようなワールドデータベースがない場合は、MySqlの公式Webサイト( MySQL ::その他のMySQLドキュメント)から入手できますデータベースはデータベースの初期化です。モデルは都市と国のテーブルに対応する構造と条件付きクエリ構造を記録し、response.goはフロントエンドに返される構造を記録します。サービスは特定のビジネスクエリロジックです。 main.goルートは2つだけです。1つはページごとに都市をクエリするルートで、もう1つはページごとに国をクエリするルートです。GitHubで完全なコードを参照してください

|——gorm_page
|    |——database
|        |——mysql.go   // 初始化连接
|        |——model.go  // page[T]结构体及分页封装
|    |——model
|        |——city.go  // city表对应结构体
|        |——country.go  // country表对应结构体
|        |——page.go  // 分页条件
|        |——response.go // 返回给前段的结构体
|    |——service
|        |——city.go
|        |——country.go
|    |——go.mod
|    |——go.sum
|    |——main.go
复制代码

3構造

City、Country、分页查询等结构体的具体属性可以查看我之前那篇博客,这里主要详细阐述一下Page[T any]和Response结构体。Page[T any]用于存储从数据库中查询到的分页总数、记录总数,Data字段用泛型存储数据列表。Page一般不直接返回给前端,因为Page中的list可能需要一定的转换(比如从数据库中返回的用户信息包含first_name和last_name,但前端只需要full name,所以要将名字拼接一下),所以再定义一个PageResponse来接收转换后的Page结构体。Response结构体返回给前端,Data字段可能是string、int、map、PageResponse等任意类型的数据,所以不用泛型而用接口。

/* database/model.go */
type Page[T any] struct {
	CurrentPage int64
	PageSize    int64
	Total       int64
	Pages       int64
	Data        []T
}

/* model/response.go */
type Response struct {
	Code int         `json:"code"`
	Msg  string      `json:"msg"`
	Data interface{} `json:"data"`
}

type PageResponse[T any] struct {
	CurrentPage int64 `json:"currentPage"`
	PageSize    int64 `json:"pageSize"`
	Total       int64 `json:"total"`
	Pages       int64 `json:"pages"` // 总页数
	Data        []T   `json:"data"`
}

// 将原始的Page结构体转换为前端需要的PageResponse结构体
func NewPageResponse[T any](page *database.Page[T]) *PageResponse[T] {
	return &PageResponse[T]{
		CurrentPage: page.CurrentPage,
		PageSize:    page.PageSize,
		Total:       page.Total,
		Pages:       page.Pages,
                // 通常要将Data中的元素进行转换,比如拼接名字、时间格式,但这里没有要转换的字段
		Data:        page.Data, 
	}
}
复制代码

4 泛型的基本使用

4.1 函数中使用泛型

之前我们想使用max函数可能需要自己写int、float、string的不同情况,现在有了泛型就可以在函数前面用中括号说明该函数支持所有可比较的类型,参数的类型是T,和其他语言用法差不多。具体用法如下:

// 支持的泛型是comparable
func max[T comparable](T a, b) {
    if a > b {
        return a
    } else {
        return b
    }
}
复制代码

4.2 结构体中使用泛型

这里用泛型定义一下链表结构体,在定义List时用中括号说明该结构体支持任意类型的T,并在要用泛型的字段Element后面用中括号说明该字段是T类型。定义结构体方法时在接收器上也要加[T],参数和返回值如果需要也要指明是T。

type List[T any] struct {
	Len  int
	root *Element[T] // 伪头节点
}
type Element[T any] struct {
	next  *Element[T]
	Value T
}

func (l *List[T]) Front() *Element[T] {
	return l.root.next
}
func (l *List[T]) Init() {
	l.Len = 0
	l.root = &Element[T]{}
}
func (l *List[T]) Add(a T) {
	node := new(Element[T])
	node.Value = a
	node.next = l.root.next
	l.root.next = node
}
func (e *Element[T]) Next() *Element[T] {
	return e.next
}
复制代码

定义好上述泛型List后,写一个简单的程序测试一下,分别声明两个List类型变量,一个是存储int型、一个存储float64,向这两个链表添加数据后再将其打印出来。此时元素的Value字段可以直接赋值给对应类型的变量,不用再做e.Value.(int)这种显式的断言。代码如下:

func main() {
	var list1 List[int]
	list1.Init()
	list1.Add(1)
	list1.Add(2)
	list1.Add(3)
	for e := list1.Front(); e != nil; e = e.Next() {
		var tmp int = e.Value // Value可以直接赋值给int类型变量
		fmt.Print(tmp)
		fmt.Print(" ")
	}
	fmt.Print("\n")
	var list2 List[float64]
	list2.Init()
	list2.Add(11.22)
	list2.Add(23.55)
	list2.Add(39.17)
	for e := list2.Front(); e != nil; e = e.Next() {
		var tmp float64 = e.Value // Value可以直接赋值给float64类型变量
		fmt.Print(tmp)
		fmt.Print(" ")
	}
	fmt.Print("\n")
}
复制代码

测试运行可得到如下结果,说明该泛型List可以用来创建任意类型的链表

$:go run main.go
3 2 1
39.17 23.55 11.22
复制代码

5 用泛型实现Gorm分页

之前用反射来实现分页的核心代码如下,首先要用反射从上层传来的model中提取类型信息,再用反射创建对应类型的切片,最后将查询到的list存储到page的Data字段中.

/* 用反射实现的分页封装 */
// wrapper中是查询条件,model是具体的结构体
// 查询City分页数据——database.SelectPage(page, wrapper, City{})
// 查询Country分页数据——database.SelectPage(page, wrapper, Country{})
func SelectPage(page *Page, wrapper map[string]interface{}, model interface{}) (e error) {
	e = nil
	DB.Model(&model).Where(wrapper).Count(&page.Total)
	if page.Total == 0 {
                // 没有符合条件的数据,返回空列表
		page.Data = []interface{}{}
		return
	}
	// 反射获得类型
	t := reflect.TypeOf(model)
	// 再通过反射创建创建对应类型的数组
	list := reflect.Zero(reflect.SliceOf(t)).Interface()
	e = DB.Model(&model).Where(wrapper).Scopes(Paginate(page)).Find(&list).Error
	page.Data = list
	return
}
// gorm官方提供的分页函数示例
// 可以在此设置总页数、总记录数等分类信息数据,并设置查询条件的limit、offset
func Paginate(page *Page) func(db *gorm.DB) *gorm.DB {
	return func(db *gorm.DB) *gorm.DB {
		if page.CurrentPage <= 0 {
			page.CurrentPage = 0
		}
		switch {
		case page.PageSize > 100:
			page.PageSize = 100
		case page.PageSize <= 0:
			page.PageSize = 10
		}
		page.Pages = page.Total / page.PageSize
		if page.Total%page.PageSize != 0 {
			page.Pages++
		}
		p := page.CurrentPage
		if page.CurrentPage > page.Pages {
			p = page.Pages
		}
		size := page.PageSize
		offset := int((p - 1) * size)
		return db.Offset(offset).Limit(int(size))
	}
}
复制代码

现在有了泛型我们可以对上述代码进行改造,可以避免使用反射。此时page.Data可以作为参数直接传递给Gorm的Find函数,因为在编译的时候page.Data是具有确定类型的。

type Page[T any] struct {
	CurrentPage int64
	PageSize    int64
	Total       int64
	Pages       int64
	Data        []T
}

func (page *Page[T]) SelectPage(wrapper map[string]interface{}) (e error) {
	e = nil
	var model T
	DB.Model(&model).Where(wrapper).Count(&page.Total)
	if page.Total == 0 {
                // 没有符合条件的数据,直接返回一个T类型的空列表
		page.Data = []T{}
		return
	}
        // 查询结果可以直接存到Page的Data字段中,因为编译的时候page.Data是有确定类型的
	e = DB.Model(&model).Where(wrapper).Scopes(Paginate(page)).Find(&page.Data).Error
	return
}
// Paginate加上T就行
func Paginate[T any](page *Page[T]) func(db *gorm.DB) *gorm.DB {
	return func(db *gorm.DB) *gorm.DB {
		if page.CurrentPage <= 0 {
			page.CurrentPage = 0
		}
		switch {
		case page.PageSize > 100:
			page.PageSize = 100
		case page.PageSize <= 0:
			page.PageSize = 10
		}
		page.Pages = page.Total / page.PageSize
		if page.Total%page.PageSize != 0 {
			page.Pages++
		}
		p := page.CurrentPage
		if page.CurrentPage > page.Pages {
			p = page.Pages
		}
		size := page.PageSize
		offset := int((p - 1) * size)
		return db.Offset(offset).Limit(int(size))
	}
}
复制代码

在service中调用分页查询的代码如下所示,queryVo中有当前页数和分页大小以及对应的查询条件。只需要在创建Page结构体实例时指明类型是City或是Country,调用SelectPage时就不需要传入额外的参数指明要查询的类型。这个风格已经非常接近Spring中的Mybatis/Mybatis-plus了,

type CityService struct{}

// 使用泛型调用分页查询,查询符合条件的City数据
func (c *CityService) SelectPageList(queryVo model.CityQueryInfo) (*model.PageResponse[model.City], error) {
	p := &database.Page[model.City]{
		CurrentPage: queryVo.CurrentPage,
		PageSize:    queryVo.PageSize,
	} // 指明Page的具体类型为City类型
	wrapper := make(map[string]interface{}, 0)
	if queryVo.CountryCode != "" {
		wrapper["CountryCode"] = queryVo.CountryCode
	}
	if queryVo.District != "" {
		wrapper["District"] = queryVo.District
	}
        // 不用再传递额外的信息告诉SelectPage函数我们要查询的是City类型的数据
	err := p.SelectPage(wrapper)
	if err != nil {
		return nil, err
	}
        // 将Page结构体转换成前端需要的PageResponse结构体
	pageResponse := model.NewPageResponse(p)
	return pageResponse, err
}
复制代码

6 测试运行

6.1 功能测试

完整项目代码见我的Github示例,输入go run main.go运行Demo程序,这里同样使用Postman进行分别对city、country两个接口发起分页查询,得到如下结果。可以看到response中的有pages、total等信息,且data中的list数据正常,表明用泛型封装的SelectPage函数对不同的类型都有效果。

ページングcity.png

パプアニューギニア.png

6.2 性能测试

这里尝试Postman分别对反射和泛型封装的Gorm分页进行性能测试,执行多次相同条件的分页查询看哪种封装方式要高效一点。关于如何用Postman做复杂的测试,以后可能会写一篇博客来做说明。这里用外部csv文件做输入,文件一共85个查询,会将中国、美国、日本、印度等国家的城市都查询一遍。重复5次这85个查询测试,得到如下测试结果:

测试编号/封装技术 反射封装 泛型封装
1 587ms 496ms
2 540ms 509ms
3 501ms 780ms
4 478ms 459ms
5 480ms 463ms

3番目の一連のテスト(やや珍しい)を除いて、反射カプセル化の時間は一般的なカプセル化よりもわずかに長くなりますが、おそらくここの都市構造は比較的単純であり、パフォーマンスのギャップ。知乎の誰かが反射損失を2つの部分にまとめました。1つはreflect.New()オブジェクトの作成で、もう1つはvalue.Field().Set()オブジェクトのプロパティの設定です。詳細については、Golang Reflection Performance Optimization-Zhihu(zhihu.com)を参照してください。

7思考

Golangは、1.18で正式に汎用メカニズムを開始しました。これは、Golangの開発におけるマイルストーンと言えます。ジェネリックスの追加は、この記事で説明したGormページングのカプセル化など、場合によってはプロジェクトのパフォーマンスを向上させるためにリフレクションのアプリケーションを置き換えることができます。同時に、Goコミュニティでも論争がありました。ジェネリックスの追加はGoの単純さの概念を破壊すると考える開発者もいれば、角括弧の書き方は山形括弧ほどエレガントではないと考える開発者もいます。 ;一部の大物はGoogleに直接スプレーし、GoogleがGoコミュニティで分裂(怖い)を引き起こすと考えています。強迫性障害があり、最新バージョンのシステム、ソフトウェア、動作環境を使いたいので、初めてプロジェクトを1.18にアップグレードし、ジェネリック医薬品の波を体験しました。ジェネリックスは本当に私のコーディングを容易にします。私には疑問があります。あと1つか2つのバージョンが待って見るのを待つことができます。

おすすめ

転載: juejin.im/post/7078279187471679518