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等功能,并且会支持参数化查询,请关注后续文章,希望能给个星星,谢谢~

posted @ 2020-07-24 18:34  Tassdar  阅读(1603)  评论(1编辑  收藏  举报