|
|
package test
|
|
|
|
|
|
import (
|
|
|
"fmt"
|
|
|
"goskeleton/app/global/variable"
|
|
|
"goskeleton/app/utils/gorm_v2"
|
|
|
_ "goskeleton/bootstrap"
|
|
|
"sync"
|
|
|
"testing"
|
|
|
"time"
|
|
|
)
|
|
|
|
|
|
// gorm v2 操作数据库单元测试
|
|
|
// 单元测试为了不影响自带的数据库,我们将单元测试涉及到的表创建在了 独立的额数据库: db_ginskeleton2_test
|
|
|
// 测试本篇首先保证 config/gorm_v2.yml 文件配置正确,相关配置项 IsInitGolobalGormMysql = 1 ,并且是设置连接的数据为:db_ginskeleton2_test
|
|
|
// 单元测试涉及到的数据库下载地址:https://wwt.lanzoul.com/ivT3A06cq35i
|
|
|
// 本文件测试到的相关数据表由于数据量较大, 最终的数据库文件没有放置在本项目骨架中,如果你动手能力很强,可以通过 issue 留言获取,重新进行测试
|
|
|
// 更多使用用法参见官方文档:https://gorm.io/zh_CN/docs/v2_release_note.html
|
|
|
|
|
|
// 模拟创建 3 个数据表,请在数据库按照结构体字段自行创建,字段全部使用小写
|
|
|
type tb_users struct {
|
|
|
Id uint `json:"id" gorm:"primaryKey" `
|
|
|
UserName string `json:"user_name"`
|
|
|
Age uint8 `json:"age"`
|
|
|
Addr string `json:"addr"`
|
|
|
Email string `json:"email"`
|
|
|
Phone string `json:"phone"`
|
|
|
Remark string `json:"remark"`
|
|
|
CreatedAt string `json:"created_at"`
|
|
|
UpdatedAt string `json:"updated_at"`
|
|
|
}
|
|
|
|
|
|
func (*tb_users) TableName() string {
|
|
|
return "tb_users"
|
|
|
}
|
|
|
|
|
|
//角色表
|
|
|
type tb_role struct {
|
|
|
Id uint `json:"id" gorm:"primaryKey" `
|
|
|
Name string `json:"name"`
|
|
|
DisplayName string `json:"display_name"`
|
|
|
Description string `json:"description"`
|
|
|
Remark string `json:"remark"`
|
|
|
CreatedAt string `json:"created_at"`
|
|
|
UpdatedAt string `json:"updated_at"`
|
|
|
}
|
|
|
|
|
|
func (*tb_role) TableName() string {
|
|
|
return "tb_role"
|
|
|
}
|
|
|
|
|
|
// 用户登录日志
|
|
|
type tb_user_log struct {
|
|
|
Id int `gorm:"primaryKey" `
|
|
|
UserId int
|
|
|
Ip string
|
|
|
LoginTime string
|
|
|
Remark string
|
|
|
CreatedAt string `json:"created_at"`
|
|
|
UpdatedAt string `json:"updated_at"`
|
|
|
}
|
|
|
|
|
|
// 如果不自定义,默认使用的是表名的复数形式,即:Tb_user_logs
|
|
|
func (*tb_user_log) TableName() string {
|
|
|
return "tb_user_log"
|
|
|
}
|
|
|
|
|
|
// code_list表
|
|
|
type tb_code_list struct {
|
|
|
Code string
|
|
|
Name string
|
|
|
CompanyName string
|
|
|
Concepts string
|
|
|
ConceptsDetail string
|
|
|
Province string
|
|
|
City string
|
|
|
Status uint8
|
|
|
Remark string
|
|
|
CreatedAt string
|
|
|
UpdatedAt string
|
|
|
}
|
|
|
|
|
|
// 如果不自定义,默认使用的是表名的复数形式,即:Tb_user_logs
|
|
|
func (*tb_code_list) TableName() string {
|
|
|
return "tb_code_list"
|
|
|
}
|
|
|
|
|
|
// 查询
|
|
|
func TestGormSelect(t *testing.T) {
|
|
|
// 查询 tb_users,由于没有配置指定的主从数据库。,所以默认连接的是
|
|
|
var users []tb_users
|
|
|
var roles []tb_role
|
|
|
|
|
|
// tb_users 查询数据会从 db_test 查询, 整个语句没有指定表名,那么就会从 Find 函数参数 &users 上绑定的 tableName函数的返回值中获取表名
|
|
|
result := variable.GormDbMysql.Select("id", "user_name", "phone", "email", "remark").Where("user_name like ?", "%test%").Find(&users)
|
|
|
if result.Error != nil {
|
|
|
t.Errorf("单元测试失败,错误明细:%s\n", result.Error.Error())
|
|
|
}
|
|
|
fmt.Printf("tb_users表数据:%v\n", users)
|
|
|
|
|
|
result = variable.GormDbMysql.Where("name like ?", "%test%").Find(&roles)
|
|
|
if result.Error != nil {
|
|
|
t.Errorf("单元测试失败,错误明细:%s\n", result.Error.Error())
|
|
|
}
|
|
|
fmt.Printf("tb_roles表数据:%v\n", roles)
|
|
|
}
|
|
|
|
|
|
// 新增
|
|
|
func TestGormInsert(t *testing.T) {
|
|
|
var usrLog = &tb_user_log{
|
|
|
UserId: 3,
|
|
|
Ip: "192.168.1.110",
|
|
|
LoginTime: time.Now().Format("2006-01-02 15:04:05"),
|
|
|
Remark: "备注信息1028",
|
|
|
CreatedAt: time.Now().Format("2006-01-02 15:04:05"),
|
|
|
UpdatedAt: time.Now().Format("2006-01-02 15:04:05"),
|
|
|
}
|
|
|
|
|
|
// 方式1:相关sql 语句: insert into Tb_user_log(user_id,ip,login_time,CreatedAt,updated_at) values(1,"192.168.1.10","当前时间","当前时间")
|
|
|
result := variable.GormDbMysql.Create(usrLog)
|
|
|
if result.RowsAffected < 0 {
|
|
|
t.Error("新增失败,错误详情:", result.Error.Error())
|
|
|
}
|
|
|
|
|
|
// 方式2:相关sql 语句: insert into Tb_user_log(user_id,ip,remark) values(1,"192.168.1.10","备注信息001")
|
|
|
result = variable.GormDbMysql.Select("user_id", "ip", "remark").Create(usrLog)
|
|
|
if result.RowsAffected < 0 {
|
|
|
t.Error("新增失败,错误详情:", result.Error.Error())
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 修改
|
|
|
func TestGormUpdate(t *testing.T) {
|
|
|
var usrLog = tb_user_log{
|
|
|
Id: 13, // 更新操作一定要指定主键Id
|
|
|
UserId: 3,
|
|
|
Ip: "127.0.0.1",
|
|
|
LoginTime: "2008-08-08 08:08:08",
|
|
|
Remark: "整个结构体对应的字段全部更新",
|
|
|
CreatedAt: time.Now().Format("2006-01-02 15:04:05"),
|
|
|
UpdatedAt: time.Now().Format("2006-01-02 15:04:05"),
|
|
|
}
|
|
|
// 整个结构体全量更新
|
|
|
result := variable.GormDbMysql.Save(&usrLog)
|
|
|
if result.RowsAffected < 0 {
|
|
|
t.Error("update失败,错误详情:", result.Error.Error())
|
|
|
}
|
|
|
|
|
|
// 定义更新字段的map, 键值对
|
|
|
var relaValue = map[string]interface{}{
|
|
|
"user_id": 66,
|
|
|
"ip": "192.168.6.66",
|
|
|
"login_time": time.Now().Format("2006-01-02 15:04:05"),
|
|
|
"remark": "指定字段更新,备注信息",
|
|
|
}
|
|
|
// 指定字段更新,更新sql: update Tb_user_log set user_id=66,ip='192.168.6.66' , login_time='当前时间', remark='指定字段更新,备注信息' where id=11
|
|
|
result = variable.GormDbMysql.Model(&usrLog).Select("user_id", "ip", "login_time", "remark").Where("id=?", 13).Updates(relaValue)
|
|
|
if result.RowsAffected < 0 {
|
|
|
t.Error("update失败,错误详情:", result.Error.Error())
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 删除
|
|
|
func TestGormDelete(t *testing.T) {
|
|
|
// 定义一个只带有ID 的相关表结构体
|
|
|
var key_primary_struct = tb_role{
|
|
|
Id: 4,
|
|
|
}
|
|
|
// 方法1: sql:delete from tb_role where id =4
|
|
|
result := variable.GormDbMysql.Delete(key_primary_struct)
|
|
|
if result.RowsAffected < 0 {
|
|
|
t.Error("delete失败,错误详情:", result.Error.Error())
|
|
|
}
|
|
|
|
|
|
// 方法2: sql:delete from tb_role where id =5
|
|
|
result = variable.GormDbMysql.Delete(&tb_role{}, 5)
|
|
|
if result.RowsAffected < 0 {
|
|
|
t.Error("delete失败,错误详情:", result.Error.Error())
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 原生sql
|
|
|
|
|
|
func TestRawSql(t *testing.T) {
|
|
|
|
|
|
// 查询类
|
|
|
var receive []tb_user_log
|
|
|
variable.GormDbMysql.Raw("select * from tb_user_log where id>?", 0).Scan(&receive)
|
|
|
fmt.Printf("%v\n", receive)
|
|
|
//var dest=make([]string,0)
|
|
|
//_=sql_res_to_tree.CreateSqlResFormatFactory().ScanToTreeData(receive,&dest)
|
|
|
//执行类
|
|
|
result := variable.GormDbMysql.Exec("update tb_user_log set remark=? where id=?", "gorm原生sql执行修改操作", 17)
|
|
|
if result.RowsAffected < 0 {
|
|
|
t.Error("原生sql执行失败,错误详情:", result.Error.Error())
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func TestBatchInsertSql(t *testing.T) {
|
|
|
|
|
|
// 查询类
|
|
|
sql := `
|
|
|
INSERT INTO tb_auth_post_mount_has_menu_button(fr_auth_post_mount_has_menu_id,fr_auth_button_cn_en_id)
|
|
|
SELECT 91,4 FROM DUAL WHERE NOT EXISTS(SELECT 1 FROM tb_auth_post_mount_has_menu_button a WHERE a.fr_auth_post_mount_has_menu_id=91 AND a.fr_auth_button_cn_en_id=4)
|
|
|
`
|
|
|
for i := 0; i < 100; i++ {
|
|
|
result := variable.GormDbMysql.Exec(sql)
|
|
|
if result.Error != nil {
|
|
|
t.Error("原生sql执行失败,错误详情:", result.Error.Error())
|
|
|
}
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 性能测试(大量查询计算耗时,评测性能)
|
|
|
func TestUseTime(t *testing.T) {
|
|
|
//循环查询100次,每次查询3500条数据,累计查询数据量为 35 万, 计算总耗时
|
|
|
var receives []tb_code_list
|
|
|
var time1 = time.Now()
|
|
|
for i := 0; i < 100; i++ {
|
|
|
receives = make([]tb_code_list, 0)
|
|
|
variable.GormDbMysql.Model(tb_code_list{}).Select("code", "name", "company_name", "concepts_detail", "province", "city", "remark", "status", "created_at", "updated_at").Where("id<=?", 3500).Find(&receives)
|
|
|
|
|
|
}
|
|
|
fmt.Printf("gorm数据遍历完毕:最后一次条数:%d\n", len(receives))
|
|
|
//经过测试,遍历处理35万条数据,需要 1034 毫秒,不同配置的电脑耗时不一样
|
|
|
fmt.Printf("本次耗时(毫秒):%d\n", time.Now().Sub(time1).Milliseconds())
|
|
|
|
|
|
// 直接使用 gorm 的原生
|
|
|
//for i:=0;i<100;i++{
|
|
|
// receives=make([]tb_code_list,0)
|
|
|
// variable.GormDbMysql.Raw("SELECT `code`, `name`, `company_name`, `concepts`, `concepts_detail`, `province`, `city`, `remark`, `status`, `CreatedAt`, `updated_at` FROM `tb_code_list` where id<3500 ").Find(&receives)
|
|
|
//}
|
|
|
//fmt.Printf("gorm 原生sql数据遍历完毕:最后一次条数:%d\n",len(receives))
|
|
|
//// 经过测试,遍历处理35万条数据,需要 4.58 秒
|
|
|
//fmt.Printf("本次耗时(毫秒):%d\n",time.Now().Sub(time1).Milliseconds())
|
|
|
}
|
|
|
|
|
|
// 性能测试(并发与连接池)
|
|
|
func TestCocurrent(t *testing.T) {
|
|
|
// SELECT `code`, `name`, `company_name`, `concepts`, `concepts_detail`, `province`, `city`, `remark`, `status`, `created_at`, `updated_at` FROM `tb_code_list` where id<3500;
|
|
|
var wg sync.WaitGroup
|
|
|
// 数据库的并发最大连接数建议设置为 128, 后续测试将通过测试数据验证
|
|
|
var conNum = make(chan uint16, 128)
|
|
|
wg.Add(1000)
|
|
|
time1 := time.Now()
|
|
|
for i := 1; i <= 1000; i++ {
|
|
|
conNum <- 1
|
|
|
go func() {
|
|
|
defer func() {
|
|
|
<-conNum
|
|
|
wg.Done()
|
|
|
}()
|
|
|
var received []tb_code_list
|
|
|
variable.GormDbMysql.Table("tb_code_list").Select("code", "name", "company_name", "province", "city", "remark", "status", "created_at", "updated_at").Where("id<=?", 3500).Find(&received)
|
|
|
//fmt.Printf("本次读取的数据条数:%d\n",len(received))
|
|
|
}()
|
|
|
}
|
|
|
wg.Wait()
|
|
|
fmt.Printf("耗时(ms):%d\n", time.Now().Sub(time1).Milliseconds())
|
|
|
|
|
|
// 测试结果,2022-06-13 补充,以下测试数据为 I7-4代机器,在I7-12代机器上面耗时非常少,128并发只需要 3.13 秒就完成了本单元测试
|
|
|
// 1.数据库并发在 1000 (相当于有1000个客户端连接操作数据库,可以在数据库使用 show processlist 自行实时刷新观察、验证),
|
|
|
// 2.并发设置为 1000,累计查询、返回结果的数据条数:350万. 最终耗时:(14.28s)
|
|
|
// 3.并发设置为 500,累计查询、返回结果的数据条数:350万. 最终耗时:(14.03s)
|
|
|
// 4.并发设置为 250,累计查询、返回结果的数据条数:350万. 最终耗时:(13.57s)
|
|
|
// 5.并发设置为 128,累计查询、返回结果的数据条数:350万. 最终耗时:(13.27s) // 由此可见,数据库并发性能最优值就是同时有128个连接,该值相当于抛物线的最高性能点
|
|
|
// 6.并发设置为 100,累计查询、返回结果的数据条数:350万. 最终耗时:(13.43s)
|
|
|
// 7.并发设置为 64,累计查询、返回结果的数据条数:350万. 最终耗时:(15.10s)
|
|
|
|
|
|
}
|
|
|
|
|
|
// 面对复杂场景,需要多个客户端连接到部署在多个不同服务器的 mysql、sqlserver、postgresql 等数据库时,
|
|
|
// 由于配置文件(config/gorm_v2.yml)只提供了一份mysql连接,无法满足需求,这时您可以通过自定义参数直接连接任意数据库,获取一个数据库句柄,供业务使用
|
|
|
func TestCustomeParamsConnMysql(t *testing.T) {
|
|
|
// 定义一个查询结果接受结构体
|
|
|
type DataList struct {
|
|
|
Id int
|
|
|
Username string
|
|
|
Last_login_ip string
|
|
|
Status int
|
|
|
}
|
|
|
// 设置动态参数连接任意多个数据库,以mysql为例进行单元测试
|
|
|
// 参数结构体 Write 和 Read 只有设置了具体指,才会生效,否则程序自动使用配置目录(config/gorm_v.yml)中的参数
|
|
|
confPrams := gorm_v2.ConfigParams{
|
|
|
Write: struct {
|
|
|
Host string
|
|
|
DataBase string
|
|
|
Port int
|
|
|
Prefix string
|
|
|
User string
|
|
|
Pass string
|
|
|
Charset string
|
|
|
}{Host: "127.0.0.1", DataBase: "db_test", Port: 3306, Prefix: "tb_", User: "root", Pass: "DRsXT5ZJ6Oi55LPQ", Charset: "utf8"},
|
|
|
Read: struct {
|
|
|
Host string
|
|
|
DataBase string
|
|
|
Port int
|
|
|
Prefix string
|
|
|
User string
|
|
|
Pass string
|
|
|
Charset string
|
|
|
}{Host: "127.0.0.1", DataBase: "db_stocks", Port: 3306, Prefix: "tb_", User: "root", Pass: "DRsXT5ZJ6Oi55LPQ", Charset: "utf8"}}
|
|
|
|
|
|
var vDataList []DataList
|
|
|
|
|
|
//gorm_v2.GetSqlDriver 参数介绍
|
|
|
// sqlType : mysql 、sqlserver、postgresql 等数据库库类型
|
|
|
// readDbIsOpen : 是否开启读写分离,1表示开启读数据库的配置,那么 confPrams.Read 参数部分才会生效; 0 则表示 confPrams.Read 部分参数直接忽略(即 读、写同库)
|
|
|
// confPrams 动态配置的数据库参数
|
|
|
// 此外,其他参数,例如数据库连接池等,则直接调用配置项数据库连接池参数,动态不需要配置,这部分对实际操作影响不大
|
|
|
if gormDbMysql, err := gorm_v2.GetSqlDriver("mysql", 0, confPrams); err == nil {
|
|
|
gormDbMysql.Raw("select id,username,status,last_login_ip from tb_users").Find(&vDataList)
|
|
|
fmt.Printf("Read 数据库查询结果:%v\n", vDataList)
|
|
|
res := gormDbMysql.Exec("update tb_users set real_name='Write数据库更新' where id<=2 ")
|
|
|
fmt.Printf("Write 数据库更新以后的影响行数:%d\n", res.RowsAffected)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
//将结果集数据扫描到树形结构体
|
|
|
// 定义一个树形结构体将原始sql集数据树形化
|
|
|
|
|
|
// 将sql结果集扫描为树形结构数据
|
|
|
// 关于sql结果树形化更多的用法参考独立的文档即可:https://gitee.com/daitougege/sql_res_to_tree
|
|
|
//func TestSqlResultToTreeStruct(t *testing.T) {
|
|
|
// // 定义一个原始数据集的接受结构体
|
|
|
// var res1 []struct {
|
|
|
// Id int
|
|
|
// Name string
|
|
|
// Fid int
|
|
|
// }
|
|
|
// sql := `
|
|
|
// SELECT id,fid,name FROM tb_province_city WHERE id IN(1,25,321) OR path_info LIKE '0,1,25,321,2721%'
|
|
|
// `
|
|
|
//
|
|
|
// variable.GormDbMysql.Raw(sql).Find(&res1)
|
|
|
// fmt.Printf("%+v\n", res1)
|
|
|
//
|
|
|
// type ProvinceCity struct {
|
|
|
// Id int `primaryKey:"yes" json:"id"`
|
|
|
// Name string `json:"name"`
|
|
|
// Fid int `fid:"Id" json:"fid"`
|
|
|
// Children []ProvinceCity `json:"children"`
|
|
|
// }
|
|
|
// var dest = make([]ProvinceCity, 0)
|
|
|
// if err := sql_res_to_tree.CreateSqlResFormatFactory().ScanToTreeData(res1, &dest); err == nil {
|
|
|
//
|
|
|
// fmt.Printf("%v\n", dest)
|
|
|
// bytes, _ := json.Marshal(dest)
|
|
|
// fmt.Printf("\n%s\n", bytes)
|
|
|
// } else {
|
|
|
// t.Errorf("%s\n", err)
|
|
|
// }
|
|
|
//
|
|
|
// // 通过反射解剖结构体的字段以及父子关系
|
|
|
//
|
|
|
//}
|
|
|
|
|
|
// sqlserver 数据库测试, 以查询为例,其他操作参见mysql
|
|
|
// 请在配置项 config > gorm_v2.yml 中,sqlserver 部分,正确配置数据库参数
|
|
|
// 设置 IsInitGolobalGormSqlserver =1 ,程序自动初始化全局变量
|
|
|
func TestSqlserver(t *testing.T) {
|
|
|
var users []tb_users
|
|
|
|
|
|
// 执行类sql,如果配置了读写分离,该命令会在 write 数据库执行
|
|
|
result := variable.GormDbSqlserver.Exec("update tb_users set remark='update 操作 write数据库' where id=?", 1)
|
|
|
if result.Error != nil {
|
|
|
t.Errorf("单元测试失败,错误明细:%s\n", result.Error.Error())
|
|
|
}
|
|
|
|
|
|
// 查询类,如果配置了读写分离,该命令会在 read 数据库执行
|
|
|
result = variable.GormDbSqlserver.Table("tb_users").Select("id", "user_name", "pass", "remark").Where("id > ?", 0).Find(&users)
|
|
|
if result.Error != nil {
|
|
|
t.Errorf("单元测试失败,错误明细:%s\n", result.Error.Error())
|
|
|
}
|
|
|
fmt.Printf("sqlserver数据查询结果:%v\n", users)
|
|
|
}
|
|
|
|
|
|
// PostgreSql 数据库测试
|
|
|
// 请在配置项 config > gorm_v2.yml 中,PostgreSql 部分,正确配置数据库参数。
|
|
|
// 设置 IsInitGolobalGormPostgreSql =1 ,程序自动初始化全局变量
|
|
|
func TestPostgreSql(t *testing.T) {
|
|
|
var users []tb_users
|
|
|
|
|
|
// 执行类sql,如果配置了读写分离,该命令会在 write 数据库执行
|
|
|
result := variable.GormDbPostgreSql.Exec("update web.tb_users set remark='update 操作 write数据库' where id=?", 1)
|
|
|
if result.Error != nil {
|
|
|
t.Errorf("单元测试失败,错误明细:%s\n", result.Error.Error())
|
|
|
}
|
|
|
// 查询类,如果配置了读写分离,该命令会在 read 数据库执行
|
|
|
result = variable.GormDbPostgreSql.Table("web.tb_users").Select("").Select("id", "user_name", "age", "addr", "remark").Where("id > ?", 0).Find(&users)
|
|
|
if result.Error != nil {
|
|
|
t.Errorf("单元测试失败,错误明细:%s\n", result.Error.Error())
|
|
|
}
|
|
|
fmt.Printf("sqlserver数据查询结果:%v\n", users)
|
|
|
}
|