Go语言系列之手把手教你撸一个ORM(一)

项目地址:https://github.com/yoyofxteam/yoyodata
欢迎星星,感谢

前言:最近在学习Go语言,就出于学习目的手撸个小架子,欢迎提出宝贵意见,项目使用Mysql数据库进行开发
我们还使用Go遵循ASP.NET Core的设计理念开发出了对应的Web框架:https://github.com/yoyofxteam/yoyogo
遵循C#命名规范开发出的反射帮助类库:https://github.com/yoyofxteam/yoyo-reflect
欢迎Star
首先,我们来看一下在Go中如果我想查询出数据库的数据都需要干些什么
1.引入MySQL驱动github.com/go-sql-driver/mysql
2.执行查询,可以看到控制台输出了数据库内容

Go语言系列之手把手教你撸一个ORM(一)


但是这个驱动的自带方法十分原始,我们需要自己创建与数据库类型一致的变量,然后取值在给字段赋值,十分麻烦,所以我们要动手把这步搞成自动化的

想实现自动装配就要解决三个问题:1.自动创建变量来获取数据库值;2.把接受到值赋值给结构体对象;3.把对象拼接成一个对象数组进行返回
因为rows.Scan()方法要求我们必须传入和查询sql中:字段顺序和数量以及类型必须一致的变量,才可以成功接受到返回值,所以我们必须按需创建变量进行绑定,具体设计见下文

1. 创建两个结构体分别用来保存结构体和结构体的字段信息

//类型缓存 type TypeInfo struct { //类型名称 TypeName string //类型下的字段 FieldInfo []FieldInfo } //字段缓存 type FieldInfo struct { //字段索引值 Index int //字段名称 FieldName string FieldValue reflect.Value FieldType reflect.StructField }

2.封装一个方法用于获取结构体的元数据,保存到我们上面定义的结构体中

func ReflectTypeInfo(model interface{}) cache.TypeInfo { modelValue := reflect.ValueOf(model) modelType := reflect.TypeOf(model) //获取包名 pkg := modelType.PkgPath() //获取完全限定类名 typeName := pkg + modelType.Name() //判断对象的类型必须是结构体 if modelValue.Kind() != reflect.Struct { panic("model must be struct !") } var fieldInfoArray []cache.FieldInfo for i := 0; i < modelValue.NumField(); i++ { fieldValue := modelValue.Field(i) //如果字段是一个结构体则不进行元数据的获取 if fieldValue.Kind() == reflect.Struct { continue } //按照索引获取字段 fieldType := modelType.Field(i) fieldName := fieldType.Name fieldInfoElement := cache.FieldInfo{ Index: i, FieldName: fieldName, FieldType: fieldType, FieldValue: fieldValue, } fieldInfoArray = append(fieldInfoArray, fieldInfoElement) } typeInfo := cache.TypeInfo{ TypeName: typeName, FieldInfo: fieldInfoArray, } return typeInfo }

3.设计一个简单的缓存,把已经获取到元数据进行缓存避免重复获取

var TypeCache TypeInfoCache type TypeInfoCache struct { sync.RWMutex Items map[string]TypeInfo } //缓存初始化 func NewTypeInfoCache() { TypeCache = TypeInfoCache{ Items: make(map[string]TypeInfo), } } //获取缓存 func (c *TypeInfoCache) GetTypeInfoCache(key string) (TypeInfo, bool) { c.RLock() defer c.RUnlock() value, ok := c.Items[key] if ok { return value, ok } return value, false } //添加缓存 func (c *TypeInfoCache) SetTypeInfoCache(key string, typeInfo TypeInfo) { c.RLock() defer c.RUnlock() c.Items[key] = typeInfo } /** 从缓存中获取类型元数据信息 */ func GetTypeInfo(model interface{}) cache.TypeInfo { //使用 包名+结构体名作为缓存的Key modelType := reflect.TypeOf(model) typeName := modelType.PkgPath() + modelType.Name() typeInfo, ok := cache.TypeCache.GetTypeInfoCache(typeName) if ok { return typeInfo } typeInfo = ReflectTypeInfo(model) cache.TypeCache.SetTypeInfoCache(typeName, typeInfo) return typeInfo }

4.封装一个方法执行SQL语句并返回对应结构体的数组(划重点)
设计思路:
执行sql语句获取到返回的数据集
获取要装配的结构体的元数据
根据sql返回字段找到对应的结构体字段进行匹配
装配要返回的结构体对象
组装一个对象数据进行返回

package queryable import ( "database/sql" "github.com/yoyofxteam/yoyodata/cache" "github.com/yoyofxteam/yoyodata/reflectx" "reflect" "sort" "strings" ) type Queryable struct { DB DbInfo Model interface{} } /** 执行不带参数化的SQL查询 */ func (q *Queryable) Query(sql string, res interface{}) { db, err := q.DB.CreateNewDbConn() if err != nil { panic(err) } rows, err := db.Query(sql) if err != nil { panic(err) } //获取返回值的原始数据类型 resElem := reflect.ValueOf(res).Elem() if resElem.Kind() != reflect.Slice { panic("value must be slice") } //获取对象完全限定名称和元数据 modelName := reflectx.GetTypeName(q.Model) typeInfo := getTypeInfo(modelName, q.Model) //获取数据库字段和类型字段的对应关系键值对 columnFieldSlice := contrastColumnField(rows, typeInfo) //创建用于接受数据库返回值的字段变量对象 scanFieldArray := createScanFieldArray(columnFieldSlice) resEleArray := make([]reflect.Value, 0) //数据装配 for rows.Next() { //创建对象 dataModel := reflect.New(reflect.ValueOf(q.Model).Type()).Interface() //接受数据库返回值 rows.Scan(scanFieldArray...) //为对象赋值 setValue(dataModel, scanFieldArray, columnFieldSlice) resEleArray = append(resEleArray, reflect.ValueOf(dataModel).Elem()) } //利用反射动态拼接切片 val := reflect.Append(resElem, resEleArray...) resElem.Set(val) //查询完毕后关闭链接 db.Close() } /** 数据库字段和类型字段键值对 */ type ColumnFieldKeyValue struct { //SQL字段顺序索引 Index int //数据库列名 ColumnName string //数据库字段名 FieldInfo cache.FieldInfo } /** 把数据库返回的值赋值到实体字段上 */ func setValue(model interface{}, data []interface{}, columnFieldSlice []ColumnFieldKeyValue) { modelVal := reflect.ValueOf(model).Elem() for i, cf := range columnFieldSlice { modelVal.Field(cf.FieldInfo.Index).Set(reflect.ValueOf(data[i]).Elem()) } } /** 创建用于接受数据库数据的对应变量 */ func createScanFieldArray(columnFieldSlice []ColumnFieldKeyValue) []interface{} { var res []interface{} for _, data := range columnFieldSlice { res = append(res, reflect.New(data.FieldInfo.FieldValue.Type()).Interface()) } return res } /** 根据SQL查询语句中的字段找到结构体的对应字段,并且记录索引值,用于接下来根据索引值来进行对象的赋值 */ func contrastColumnField(rows *sql.Rows, typeInfo cache.TypeInfo) []ColumnFieldKeyValue { var columnFieldSlice []ColumnFieldKeyValue columns, _ := rows.Columns() for _, field := range typeInfo.FieldInfo { for i, column := range columns { if strings.ToUpper(column) == strings.ToUpper(field.FieldName) { columnFieldSlice = append(columnFieldSlice, ColumnFieldKeyValue{ColumnName: column, Index: i, FieldInfo: field}) } } } //把获取到的键值对按照SQL语句查询字段的顺序进行排序,否则会无法赋值 sort.SliceStable(columnFieldSlice, func(i, j int) bool { return columnFieldSlice[i].Index < columnFieldSlice[j].Index }) return columnFieldSlice } /** 获取要查询的结构体的元数据,这个就是调用了一下第二部的那个方法 */ func getTypeInfo(key string, model interface{}) cache.TypeInfo { typeInfo, ok := cache.TypeCache.GetTypeInfoCache(key) if !ok { typeInfo = reflectx.GetTypeInfo(model) } return typeInfo }

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wsfjfd.html