使用反射和泛型简化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接口的方法。就不用**“硬编码”**般去写查询语句了。

相关内容

热门资讯

中秋节晚会上的主持词 关于中秋节晚会上的主持词(精选5篇)  主持词需要富有情感,充满热情,才能有效地吸引到观众。在当下的...
民主生活会主持词 民主生活会主持词  一、主持词简介  由主持人于节目进行过程中串联节目的串联词。如今的各种演出活动和...
2022年央视春节联欢晚会主... 2022年央视春节联欢晚会主持词  借鉴诗词和散文诗是主持词的一种写作手法。在现今人们越来越重视活动...
少先队建队日主持词 少先队建队日主持词  什么是主持词  由主持人于节目进行过程中串联节目的串联词。如今的各种演出活动和...
晚会节目串词主持稿 晚会节目串词主持稿  在现在社会,很多地方都会使用到主持稿,通过主持稿的写作将主题贯穿于所有的节目之...
幼儿园开园揭牌剪彩仪式主持词 幼儿园开园揭牌剪彩仪式主持词  主持词要把握好吸引观众、导入主题、创设情境等环节以吸引观众。在一步步...
公司辞旧迎新晚会主持词串词   男:尊敬的各位领导、各位来宾,  女:亲爱的同事们  合:大家下午好!  男:光阴似箭,岁月如梭...
纯中式婚礼主持词 纯中式婚礼主持词(通用5篇)  主持词是主持人在台上表演的灵魂之所在。在现在的社会生活中,越来越多的...
悟空传的经典台词 悟空传的经典台词  1、我曾深爱过,我不在乎结局。  2、我知道天会愤怒,那,你知不知道,天也会颤抖...
最有创意的广告词(经典 最有创意的广告词(经典  01 钱不是问题,问题是没钱。  02 钻石恆久远,一颗就破產。  03 ...
毕业感谢致辞 关于毕业感谢致辞(精选15篇)  无论是在学校还是在社会中,大家都写过致辞吧,致辞的措词造句要考虑与...
年会嘉宾简短致辞 年会嘉宾简短致辞  在日复一日的学习、工作或生活中,大家总少不了要接触或使用致辞吧,致辞具有很强的实...
成长礼主持稿 成长礼主持稿(通用8篇)  在日常生活和工作中,需要使用主持稿的情况越来越多,主持稿是在晚会、联欢会...
电视剧《放羊的星星》经典台词 电视剧《放羊的星星》经典台词  在现实社会中,用到台词的地方越来越多,台词是一种特殊的,也是很难掌握...
抓周仪式主持词 抓周仪式主持词范文  主持词是主持人在台上表演的灵魂之所在。在如今这个中国,主持词是活动、集会等的必...
年终总结大会主持词结束语 年终总结大会主持词结束语  主持词是各种演出活动和集会中主持人串联节目的串联词。时代不断在进步,主持...
纯中式婚礼主持词(2) 让我们共同举起手中的酒杯,共同祝福我们这一对知心爱人,祝福他们在爱的旅途上风雨相承,相濡以沫,真爱一...
幼儿园园庆主持词 幼儿园园庆主持词  利用在中国拥有几千年文化的诗词能够有效提高主持词的感染力。在人们积极参与各种活动...
篮球比赛开幕式主持词 篮球比赛开幕式主持词(通用5篇)  主持词可以采用和历史文化有关的表述方法去写作以提升活动的文化内涵...
六一儿童节活动节目的主持词 六一儿童节活动节目的主持词(精选7篇)  主持词是各种演出活动和集会中主持人串联节目的串联词。在当今...