使用反射和泛型简化Golang查询数据库代码的方案

news2024/11/15 4:19:50

大纲

  • Postgresql数组
  • 案例
  • 常规写法
    • 定义结构体
    • 查询数据
    • 问题
  • 反射+泛型写法
    • 结构体定义
      • 接口
      • Tag
    • 实现逻辑
      • 泛型设计
      • 实例化模型结构体
      • 获取表名
      • 过滤字段
      • 组装SQL语句
      • 查询
      • 遍历读取结果
        • 实例化模型结构体
        • 组装Scan方法的参数
        • 调用Scan方法并保存结果
    • 完整代码
  • 小结

Postgresql数组

Postgresql有个很好的功能:可以设置字段为数组。这样我们就不用存储使用特定字符连接的数据,更不需要在取出数据后使用代码逻辑进行切分。举一个例子,我们需要存储一个数组[1,2,3,4]。常规做法是我们将该字段设计为字符串或者文本类型,存储“1,2,3,4”;在业务逻辑中,数据取出后,我们使用“,”进行切分,并将字符串“1”“2”“3”转换为整型,最后组成数组[1,2,3,4]。
为了更好表述这个问题,我们看个Demo。

案例

假设我们要新建一张用来保存员工信息的表——employee

CREATE TABLE "public"."employee" (
  "id" int8 NOT NULL,
  "name" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
  "address" varchar(255) COLLATE "pg_catalog"."default",
  "title" varchar(255)[] COLLATE "pg_catalog"."default",
  "salary" float8 NOT NULL,
  "leader_id" int8,
  "subordinate_id" int8[],
  "valid" bool NOT NULL
)
;
ALTER TABLE "public"."employee" ADD CONSTRAINT "employee_pkey" PRIMARY KEY ("id");

title字段是头衔,一个员工可能有多个头衔。
subordinate_id是下属员工的ID。
上述两者都是数组类型。
我们再构建部分数据。

-- ----------------------------
-- Records of employee
-- ----------------------------
INSERT INTO "public"."employee" VALUES (3, '丁', '北京', '{Assistant}', 1234.5, 1, NULL, 't');
INSERT INTO "public"."employee" VALUES (0, '甲', '北京望京', '{CEO}', 12345.6, NULL, '{1,2}', 't');
INSERT INTO "public"."employee" VALUES (4, '戊', NULL, '{Assistant}', 234.5, 2, NULL, 't');
INSERT INTO "public"."employee" VALUES (1, '乙', '北京', '{CTO,VP}', 2345.6, 0, '{3}', 't');
INSERT INTO "public"."employee" VALUES (2, '丙', '北京', '{CFO,VP}', 3456.7, 0, '{4}', 't');

更直观的展现是
在这里插入图片描述

常规写法

定义结构体

type Employee struct {
	Id            int64
	Name          string
	Address       sql.NullString
	Title         []string
	Salary        float64
	LeaderId      sql.NullInt64
	SubordinateId []int64
	Valid         bool
}

查询数据

func Select(conditions string, sqlDB *sql.DB) (models []Employee, err error) {
	sql := `SELECT employee.id,
			name,
			address,
			title,
			salary,
			leader_id,
			subordinate_id,
			valid 
			FROM employee`

	if conditions != "" {
		sql += " WHERE " + conditions
	}

	rows, errQuerySql := sqlDB.Query(sql)
	if errQuerySql != nil {
		err = errQuerySql
		return
	}
	defer rows.Close()
	for rows.Next() {
		employee := Employee{}
		scanErr := rows.Scan(
			&employee.Id,
			&employee.Name,
			&employee.Address,
			pq.Array(&employee.Title),
			&employee.Salary,
			&employee.LeaderId,
			pq.Array(&employee.SubordinateId),
			&employee.Valid,
		)
		if scanErr != nil {
			err = errQuerySql
			return
		}
		models = append(models, employee)
	}
	return
}

问题

对于数组类型的Title和SubordinateId,我们使用pq.Array进行转换。
这种写法算是硬编码。因为如果对查询字段进行新增或者删除,都要对Scan方法的调用进行调整。比如我们不需要Address,则需要同时调整SQL语句和Scan方法。

反射+泛型写法

结构体定义

type Model interface {
	GetTableName() string
}

type Employee struct {
	Id            int64          `column:"id"`
	Name          string         `column:"name"`
	Address       sql.NullString `column:"address"`
	Title         []string       `column:"title"`
	Salary        float64        `column:"salary"`
	LeaderId      sql.NullInt64  `column:"leader_id"`
	SubordinateId []int64        `column:"subordinate_id"`
	Valid         bool           `column:"valid"`
}

func (d Employee) GetTableName() string {
	return "employee"
}

接口

定义一个接口Model。所有数据库模型结构体都实现它的接口方法,返回表名。后续我们通过返回Model数组,将不同模型结构体数据在同一个函数中返回出来。

Tag

因为数据库字段名和模型结构体结构体名不一定一样,所以我们需要另外一个位置来做衔接。比如模型结构体Employee的Id首字母要大写,以表示它可以直接访问。而在数据库中我们要求字段都是小写命名,即id。

实现逻辑

泛型设计

func Select[T Model](conditions string, ignoreColumns []string, sqlDB *sql.DB) (models []Model, err error) {

调用Select方法时,可以指明T是哪个具体的模型结构体。同时也限制了模型结构体必须实现Model接口的方法。
返回值models是Model数组。这样我们就可以使用一种写法,返回各种模型结构体的查询结果了。
ignoreColumns 是忽略的字段名字。这样就可以动态调整查询语句和结果了。

实例化模型结构体

model := new(T)

后面泛型会使用这个实例

获取表名

	modelValue := reflect.ValueOf(model)
	getTableNameOut := modelValue.MethodByName("GetTableName").Call([]reflect.Value{})
	if len(getTableNameOut) != 1 {
		err = fmt.Errorf(fmt.Sprintf("%s GetTableName Return %d values, need only 1", modelValue.Type().Name(), len(getTableNameOut)))
		return
	}
	tableName := getTableNameOut[0].String()

这个地方使用了反射的方法进行了GetTableName方法的调用。

过滤字段

	modelType := reflect.TypeOf(model)
	var columnNamesInSql []string
	var selectedColumnsIndex []int
	for i := 0; i < modelType.Elem().NumField(); i++ {
		field := modelType.Elem().Field(i)
		columnName := field.Tag.Get("column")
		if columnName == "" {
			continue
		}
		if In(columnName, ignoreColumns) {
			continue
		}
		columnNamesInSql = append(columnNamesInSql, columnName)
		selectedColumnsIndex = append(selectedColumnsIndex, i)
	}

	columnsCount := len(selectedColumnsIndex)
	if columnsCount == 0 {
		err = fmt.Errorf(fmt.Sprintf("%s Selected columns is 0", tableName))
		return
	}

columnNamesInSql用来存储所有通过过滤的字段名;selectedColumnsIndex用来保存通过过滤的字段索引号。

组装SQL语句

	columnsInSql := strings.Join(columnNamesInSql, ",")
	sql := fmt.Sprintf("SELECT %s FROM %s", columnsInSql, tableName)
	if len(conditions) != 0 {
		sql = fmt.Sprintf("%s WHERE %s", sql, conditions)
	}

查询

	rows, errQuerySql := sqlDB.Query(sql)
	if errQuerySql != nil {
		err = errQuerySql
		return
	}
	defer rows.Close()

遍历读取结果

	for rows.Next() {

实例化模型结构体

		singleRow := new(T)

后面我们需要用这个实例去接收数据。

组装Scan方法的参数

		paramsIn := make([]reflect.Value, columnsCount)
		for i := 0; i < len(selectedColumnsIndex); i++ {
			selectedColumnIndex := selectedColumnsIndex[i]
			elem := modelType.Elem().Field(selectedColumnIndex)
			if !refValue.Field(selectedColumnIndex).CanAddr() {
				err = fmt.Errorf(fmt.Sprintf("%s Field %s can't addr", modelValue.Type().Name(), elem.Name))
				return
			}
			columnType := elem.Type.Name()
			if columnType == "" {
				kindString := elem.Type.Kind().String()
				if strings.Compare("slice", kindString) == 0 {
					param := reflect.NewAt(refValue.Field(selectedColumnIndex).Type(), unsafe.Pointer(refValue.Field(selectedColumnIndex).UnsafeAddr()))
					paramsIn[i] = reflect.ValueOf(pq.Array(param.Interface()))
				} else {
					err = fmt.Errorf(fmt.Sprintf("%s Field %s Type is unkown:%s", modelValue.Type().Name(), elem.Name, kindString))
					return
				}
			} else {
				paramsIn[i] = reflect.NewAt(refValue.Field(selectedColumnIndex).Type(), unsafe.Pointer(refValue.Field(selectedColumnIndex).UnsafeAddr()))
			}
		}

这儿有一个非常重要的函数:reflect.NewAt。因为Scan函数的参数需要对结构体成员进行取址,而refValue.Field(selectedColumnIndex)的类型是reflect.Value,对它取址并不是对模型结构体成员取址,所以要使用它的裸指针。而裸指针的类型是uintptr,就需要使用reflect.NewAt函数对其进行转换。

调用Scan方法并保存结果

		errScan := reflect.ValueOf(rows).MethodByName("Scan").Call(paramsIn)
		if errScan[0].Interface() != nil {
			err = errScan[0].Interface().(error)
			return
		}

		models = append(models, *singleRow)
	}
	return
}

完整代码


type Model interface {
	GetTableName() string
}

type Employee struct {
	Id            int64          `column:"id"`
	Name          string         `column:"name"`
	Address       sql.NullString `column:"address"`
	Title         []string       `column:"title"`
	Salary        float64        `column:"salary"`
	LeaderId      sql.NullInt64  `column:"leader_id"`
	SubordinateId []int64        `column:"subordinate_id"`
	Valid         bool           `column:"valid"`
}

func (d Employee) GetTableName() string {
	return "employee"
}

func In[T string | int | float64 | float32 | int64 | int32, A []T](target T, arr A) bool {
	for _, v := range arr {
		if target == v {
			return true
		}
	}
	return false
}

func Select[T Model](conditions string, ignoreColumns []string, sqlDB *sql.DB) (models []Model, err error) {
	model := new(T)

	modelValue := reflect.ValueOf(model)
	getTableNameOut := modelValue.MethodByName("GetTableName").Call([]reflect.Value{})
	if len(getTableNameOut) != 1 {
		err = fmt.Errorf(fmt.Sprintf("%s GetTableName Return %d values, need only 1", modelValue.Type().Name(), len(getTableNameOut)))
		return
	}
	tableName := getTableNameOut[0].String()

	modelType := reflect.TypeOf(model)
	var columnNamesInSql []string
	var selectedColumnsIndex []int
	for i := 0; i < modelType.Elem().NumField(); i++ {
		field := modelType.Elem().Field(i)
		columnName := field.Tag.Get("column")
		if columnName == "" {
			continue
		}
		if In(columnName, ignoreColumns) {
			continue
		}
		columnNamesInSql = append(columnNamesInSql, columnName)
		selectedColumnsIndex = append(selectedColumnsIndex, i)
	}

	columnsCount := len(selectedColumnsIndex)
	if columnsCount == 0 {
		err = fmt.Errorf(fmt.Sprintf("%s Selected columns is 0", tableName))
		return
	}

	columnsInSql := strings.Join(columnNamesInSql, ",")
	sql := fmt.Sprintf("SELECT %s FROM %s", columnsInSql, tableName)
	if len(conditions) != 0 {
		sql = fmt.Sprintf("%s WHERE %s", sql, conditions)
	}

	rows, errQuerySql := sqlDB.Query(sql)
	if errQuerySql != nil {
		err = errQuerySql
		return
	}
	defer rows.Close()
	for rows.Next() {
		singleRow := new(T)
		refValue := reflect.ValueOf(singleRow).Elem()
		paramsIn := make([]reflect.Value, columnsCount)
		for i := 0; i < len(selectedColumnsIndex); i++ {
			selectedColumnIndex := selectedColumnsIndex[i]
			elem := modelType.Elem().Field(selectedColumnIndex)
			if !refValue.Field(selectedColumnIndex).CanAddr() {
				err = fmt.Errorf(fmt.Sprintf("%s Field %s can't addr", modelValue.Type().Name(), elem.Name))
				return
			}
			columnType := elem.Type.Name()
			if columnType == "" {
				kindString := elem.Type.Kind().String()
				if strings.Compare("slice", kindString) == 0 {
					param := reflect.NewAt(refValue.Field(selectedColumnIndex).Type(), unsafe.Pointer(refValue.Field(selectedColumnIndex).UnsafeAddr()))
					paramsIn[i] = reflect.ValueOf(pq.Array(param.Interface()))
				} else {
					err = fmt.Errorf(fmt.Sprintf("%s Field %s Type is unkown:%s", modelValue.Type().Name(), elem.Name, kindString))
					return
				}
			} else {
				paramsIn[i] = reflect.NewAt(refValue.Field(selectedColumnIndex).Type(), unsafe.Pointer(refValue.Field(selectedColumnIndex).UnsafeAddr()))
			}
		}
		errScan := reflect.ValueOf(rows).MethodByName("Scan").Call(paramsIn)
		if errScan[0].Interface() != nil {
			err = errScan[0].Interface().(error)
			return
		}

		models = append(models, *singleRow)
	}
	return
}

小结

泛型+反射的方案虽然复杂,但是后续其他表的查询则会变得非常简单。我们只要新增表对应的模板结构体,实现Model接口的方法。就不用**“硬编码”**般去写查询语句了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/171276.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

7、操作DOM对象(重点)

核心&#xff1a;浏览器网页就是一个DOM树形结构 更新&#xff1a;更新该DOM节点的内容&#xff0c;相当于更新了该DOM节点表示的HTML的内容&#xff1b; 遍历&#xff1a;遍历该DOM节点下的子节点&#xff0c;以便进行进一步操作&#xff1b; 添加&#xff1a;在该DOM节点下…

Matlab中的dsp.AudioFileReader函数的认识和学习

在Matlab中的dsp.AudioFileReader函数的认识和学习1.描述2.语法2.1 语法描述2.2 属性Properties2.3 举例Stream from audio file 来自音频文件的流 1.描述 dsp.AudioFileReader系统对象™ 从音频文件读取音频样本。 要从音频文件读取音频样本&#xff0c;请执行以下操作&…

小方制药冲刺A股上市:毛利率走低,方之光、鲁爱萍夫妇为实控人

近日&#xff0c;上海小方制药股份有限公司&#xff08;下称“小方制药”&#xff09;公开预披露更新招股书&#xff0c;准备在上海证券交易所主板上市。据贝多财经了解&#xff0c;小方制药于2022年7月1日递交招股书&#xff0c;国信证券为其保荐机构。 本次冲刺上市&#xff…

扫码器:壹码通(EMT 6621)二维码带多个回车换行处理

摘要&#xff1a;二维码运用越来越广泛了&#xff0c;目前在医院中一个二维码可以串联多个系统&#xff0c;二维码的内容也可以设置一些特殊字符去达成系统便捷性。本次遇到为二维码中开头内置了回车和空格&#xff0c;在程序判断为回车(KEY_ENTER)时就会触发业务逻辑&#xff…

mybatis之一级缓存和二级缓存

缓存&#xff1a; 查询需要连接数据库&#xff0c;非常的耗费资源&#xff0c;将一次查询的结果&#xff0c;暂存在一个可以直接取到的地方&#xff0c;我们将其称之为缓存&#xff0c;当我们需要再次查询相同的数据时&#xff0c;直接走缓存这个过程&#xff0c;就不用走数据…

【RabbitMQ三】——RabbitMQ工作队列模式(Work Queues)

RabbitMQ工作队列模式为什么要有工作队列模式如何使用工作队列模式轮询消息确认验证消息确认消息持久化公平调度验证公平调度**现在将消费者1中的Thread.sleep(1000)改为Thread.sleep(3000);不添加公平调度相关代码进行测试。**现在将消费者1中的Thread.sleep(1000)改为Thread.…

BC即将登录Coinbase Institutional,2023年以全新姿态出发

以支付为最初定位的加密资产&#xff0c;在支付领域的发展始终停滞不前&#xff0c;尤其是在2022年&#xff0c;加密行业经历了几次“至暗时刻”&#xff0c;导致加密市场资金不断出逃市场全面转熊&#xff0c;越来越多的人对加密资产市场的发展前景失去信心。 而在2021年年底开…

【GD32F427开发板试用】移植CoreMark验证0等待区Flash大小

本篇文章来自极术社区与兆易创新组织的GD32F427开发板评测活动&#xff0c;更多开发板试用活动请关注极术社区网站。作者&#xff1a;Doravmon 引言 非常荣幸能够参与到此次GD32F427开发板试用的活动中来&#xff0c;在拿到开发板之前就翻了翻手册&#xff0c;一直有个疑问困惑…

APM/STM32F072RB基于HAL库配置USB CDC虚拟串口功能

APM/STM32F072RB基于HAL库配置USB CDC虚拟串口功能&#x1f4e2;采用的自制开发板&#xff0c;开源PCB工程详情放在《极海APM32F072RB开发环境测试》✨本案例基于STM32CubeMX工具配置。&#x1f4fa;使用STM32CubeMX工具配置工程改为APMF072RB型号过程如下&#xff1a; ⛳注意…

性能测试实战 | 电商业务的性能测试(一): 必备基础知识

本文为霍格沃兹测试学院优秀学员课程学习系列笔记&#xff0c;想一起系统进阶的同学文末加群交流。 1.1 测试步骤总览 需求分析与测试设计&#xff08;性能需求目标业务模型拆解&#xff09; 测试数据准备和构造(基于模型的数据准备) 性能指标预期(性能需求目标) 发压工具配…

vue2 使用@vue/composition-api依赖包 编译、打包各种报错

vue2 使用vue/composition-api依赖包 编译、打包各种报错问题来源解决办法最近在维护以前&#xff08;大概一年前&#xff09;的项目时&#xff0c;遇到个这种问题&#xff1a; 项目本身是用 vue-cli 创建的 vue 2.x.xx 版本的项目&#xff0c;然后引入 vue/composition-api 依…

MIT6.830-2022-lab5实验思路详细讲解

文章目录前言一、实验背景二、实验正文Exercise 1 &#xff1a;SearchExercise 2 &#xff1a;Insert - Splitting PagesExercise 3 &#xff1a;Delete - Redistributing pagesExercise 4&#xff1a;Delete - Redistributing pages总结前言 Datebase中很重要的一部分就是ind…

【Java面试】SpringBoot篇

注&#xff1a;本文通篇将SpringBoot以sb代替。 文章目录Spring和SpringBoot的关系和区别&#xff1f;谈谈你对SpringBoot的理解&#xff0c;它有哪些特性&#xff1f;SpringBoot的核心注解说说你对SpringBoot自动配置的理解为什么SpringBoot的jar包可以直接运行&#xff1f;Sp…

uboot启动流程分析(基于i.m6ull)

一、uboot的makefile 1.1 makefile整体解析过程 为了生成u-boot.bin这个文件&#xff0c;首先要生成构成u-boot.bin的各个库文件、目标文件。为了各个库文件、目标文件就必须进入各个子目录执行其中的Makefile。由此&#xff0c;确定了整个编译的命令和顺序。 1.2 makefile整…

10.2 初始泛型算法

文章目录只读算法find()count()accumulate()equal()写容器元素算法fill()fill_n()back_inserter()copy()copy_backward()replace()replace_copy()next_permutation()prev_permutation()重排容器元素算法sort()unique()stable_sort()除了少数例外&#xff0c;标准库算法都对一个…

pandas数据聚合与分组运算

文章目录数据聚合与分组运算分组与聚合的原理通过groupby()方法将数据拆分成组按列名进行分组按Series对象进行分组按字典进行分组按函数进行分组数据聚合与分组运算 对数据集进行分组并对各组应用一个函数&#xff08;无论是聚合还是转换&#xff09;&#xff0c;通常是数据分…

哈佛结构和冯诺依曼结构?STM32属于哈佛结构还是冯诺依曼结构?

现代的CPU基本上归为冯诺伊曼结构&#xff08;也成普林斯顿结构&#xff09;和哈佛结构。 冯诺依曼体系 冯诺依曼体系结构图如下 冯诺依曼结构也称普林斯顿结构&#xff0c;是一种将程序指令存储器和数据存储器合并在一起的存储器结构。数据与指令都存储在同一存储区中&…

大数据技术架构(组件)5——Hive:流程剖析2

1.1.2、Stage division&#xff08;不够细致&#xff0c;需要例子&#xff09;Stage理解&#xff1a;结合对前面讲到的Hive对查询的一系列执行流程的理解&#xff0c;那么在一个查询任务中会有一个或者多个Stage.每个Stage之间可能存在依赖关系。没有依赖关系的Stage可以并行执…

IIS部署应用程序连接 LocalDB 数据库

使用.net core框架创建ASP.NET Core API应用程序&#xff0c;利用Entity Framework core实体进行MS LocalDB数据库进行连接操作&#xff08;增/删/改/查运行&#xff09;。 问题&#xff1a; 在Visual Studio 2022 开发工具可以正常运行 Web API 应用程序连接 LocalDB 数据库…

R语言基于poLCA包进行潜类别分析

潜在类别分析是一种分析多元分类数据的统计技术。当观测数据以一系列分类响应的形式出现时- -例如&#xff0c;在民意调查、个人层面的投票数据、人与人之间可靠性的研究或消费者行为和决策中- -通常感兴趣的是调查观测变量之间的混淆来源&#xff0c;识别和表征相似案例的集群…