• 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.执行查询,可以看到控制台输出了数据库内容,并像你发出了祖安问候

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

    想实现自动装配就要解决三个问题: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
    }
    
    

    方法封装完毕,我们跑个单元测试看一下效果

    目前这个小架子刚开始写,到发布这篇文档为止仅封装出了最基础的查询,接下来会实现Insert/Update等功能,并且会支持参数化查询,请关注后续文章,希望能给个星星,谢谢~

  • 相关阅读:
    2019牛客暑期多校训练营(第二场)H Second Large Rectangle
    HDU -1506 Largest Rectangle in a Histogram&&51nod 1158 全是1的最大子矩阵 (单调栈)
    吉哥系列故事——完美队形II(马拉车算法)
    Theme Section
    激光雷达和毫米波雷达
    模型压缩95%:Lite Transformer,MIT韩松等人
    分布式深度学习DDL解析
    TOF摄像机可以替代Flash激光雷达吗?
    毫米波雷达分类和技术方案
    高精地图与自动驾驶(下)
  • 原文地址:https://www.cnblogs.com/Tassdar/p/13373289.html
Copyright © 2020-2023  润新知