使用反射和泛型简化Golang查询数据库代码的方案
创始人
2024-05-13 10:36:29
0

大纲

  • 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            int64Name          stringAddress       sql.NullStringTitle         []stringSalary        float64LeaderId      sql.NullInt64SubordinateId []int64Valid         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 = errQuerySqlreturn}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 = errQuerySqlreturn}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 []stringvar selectedColumnsIndex []intfor 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 = errQuerySqlreturn}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 []stringvar selectedColumnsIndex []intfor 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 = errQuerySqlreturn}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接口的方法。就不用**“硬编码”**般去写查询语句了。

相关内容

热门资讯

常用商务英语口语   商务英语是以适应职场生活的语言要求为目的,内容涉及到商务活动的方方面面。下面是小编收集的常用商务...
六年级上册英语第一单元练习题   一、根据要求写单词。  1.dry(反义词)__________________  2.writ...
复活节英文怎么说 复活节英文怎么说?复活节的英语翻译是什么?复活节:Easter;"Easter,anniversar...
2008年北京奥运会主题曲 2008年北京奥运会(第29届夏季奥林匹克运动会),2008年8月8日到2008年8月24日在中华人...
英语道歉信 英语道歉信15篇  在日常生活中,道歉信的使用频率越来越高,通过道歉信,我们可以更好地解释事情发生的...
六年级英语专题训练(连词成句... 六年级英语专题训练(连词成句30题)  1. have,playhouse,many,I,toy,i...
上班迟到情况说明英语   每个人都或多或少的迟到过那么几次,因为各种原因,可能生病,可能因为交通堵车,可能是因为天气冷,有...
小学英语教学论文 小学英语教学论文范文  引导语:英语教育一直都是每个家长所器重的,那么有关小学英语教学论文要怎么写呢...
英语口语学习必看的方法技巧 英语口语学习必看的方法技巧如何才能说流利的英语? 说外语时,我们主要应做到四件事:理解、回答、提问、...
四级英语作文选:Birth ... 四级英语作文范文选:Birth controlSince the Chinese Governmen...
金融专业英语面试自我介绍 金融专业英语面试自我介绍3篇  金融专业的学生面试时,面试官要求用英语做自我介绍该怎么说。下面是小编...
我的李老师走了四年级英语日记... 我的李老师走了四年级英语日记带翻译  我上了五个学期的小学却换了六任老师,李老师是带我们班最长的语文...
小学三年级英语日记带翻译捡玉... 小学三年级英语日记带翻译捡玉米  今天,我和妈妈去外婆家,外婆家有刚剥的`玉米棒上带有玉米籽,好大的...
七年级英语优秀教学设计 七年级英语优秀教学设计  作为一位兢兢业业的人民教师,常常要写一份优秀的教学设计,教学设计是把教学原...
我的英语老师作文 我的英语老师作文(通用21篇)  在日常生活或是工作学习中,大家都有写作文的经历,对作文很是熟悉吧,...
英语老师教学经验总结 英语老师教学经验总结(通用19篇)  总结是指社会团体、企业单位和个人对某一阶段的学习、工作或其完成...
初一英语暑假作业答案 初一英语暑假作业答案  英语练习一(基础训练)第一题1.D2.H3.E4.F5.I6.A7.J8.C...
大学生的英语演讲稿 大学生的英语演讲稿范文(精选10篇)  使用正确的写作思路书写演讲稿会更加事半功倍。在现实社会中,越...
VOA美国之音英语学习网址 VOA美国之音英语学习推荐网址 美国之音网站已经成为语言学习最重要的资源站点,在互联网上还有若干网站...
商务英语期末试卷 Part I Term Translation (20%)Section A: Translate ...