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等功能,并且会支持参数化查询,请关注后续文章,希望能给个星星,谢谢~
Go语言系列之手把手教你撸一个ORM(一)的更多相关文章
- 手把手教你撸一个 Webpack Loader
		文:小 boy(沪江网校Web前端工程师) 本文原创,转载请注明作者及出处 经常逛 webpack 官网的同学应该会很眼熟上面的图.正如它宣传的一样,webpack 能把左侧各种类型的文件(webpa ... 
- C#多线程(16):手把手教你撸一个工作流
		目录 前言 节点 Then Parallel Schedule Delay 试用一下 顺序节点 并行任务 编写工作流 接口构建器 工作流构建器 依赖注入 实现工作流解析 前言 前面学习了很多多线程和任 ... 
- WebRTC系列(1)-手把手教你实现一个浏览器拍照室Demo
		1.WebRTC开发背景 由于业务需求,需要在项目中实现实时音视频通话功能,之前基于浏览器开发的Web项目要进行音视频通话,需要安装flash插件才能实现或者使用C/S客户端进行通信.随着互联网技术的 ... 
- Android应用系列:手把手教你做一个小米通讯录(附图附源码)
		前言 最近心血来潮,突然想搞点仿制品玩玩,很不幸小米成为我苦逼的第一个试验品.既然雷布斯的MIUI挺受欢迎的(本人就是其的屌丝用户),所以就拿其中的一些小功能做一些小demo来玩玩.小米的通讯录大家估 ... 
- 手把手教你撸一个简易的 webpack
		背景 随着前端复杂度的不断提升,诞生出很多打包工具,比如最先的grunt,gulp.到后来的webpack和Parcel.但是目前很多脚手架工具,比如vue-cli已经帮我们集成了一些构建工具的使用. ... 
- Vue手把手教你撸一个 beforeEnter 钩子函数
		地址 :https://www.jb51.net/article/138821.htm 地址 :https://www.jb51.net/article/108964.htm 
- 学以致用:手把手教你撸一个工具库并打包发布,顺便解决JS浮点数计算精度问题
		本文讲解的是怎么实现一个工具库并打包发布到npm给大家使用.本文实现的工具是一个分数计算器,大家考虑如下情况: \[ \sqrt{(((\frac{1}{3}+3.5)*\frac{2}{9}-\fr ... 
- 手把手教你撸个vue2.0弹窗组件
		手把手教你撸个vue2.0弹窗组件 在开始之前需要了解一下开发vue插件的前置知识,推荐先看一下vue官网的插件介绍 预览地址 http://haogewudi.me/kiko/inde... 源码地 ... 
- 只有20行Javascript代码!手把手教你写一个页面模板引擎
		http://www.toobug.net/article/how_to_design_front_end_template_engine.html http://barretlee.com/webs ... 
随机推荐
- postman使用小结(一)
			postman可以用来做接口测试. 下面是使用的基本步骤: 1新建http请求: 2设置请求类型get/post/put/delete...: 3设置请求的url: 4设置请求的Header头部信息, ... 
- Oracle 存储过程中的临时表数据自动清空
			问题叙述: 用 EXECUTE IMMEDIATE 动态往临时表插入数据,跟踪发现插入临时表后数据会立马清空,按理说等存储过程执行完才会清空临时表才对,现在是执行插入语句后下一步验证就发现临时表就没有 ... 
- 【差分】Tallest Cow
			题目 FJ's N(1≤N≤10,000) cows conveniently indexed 1..N are standing in a line. Each cow has a positive ... 
- TreeMap实现
- 【MyBtis】获取数据插入postgresql后返回的自增id
			问题描述 数据库采用的是postgresql,以下面的rule表为例,该表的id设置为自增,那么经常有这样的需求,在执行insert操作后,紧接着需要获取该记录的自增id往中间表中插入数据,或者是再根 ... 
- STL标准函数库学习小总结
			浅谈STL(未完)前言:(1)学习资料——<算法笔记>第六章.<常用基础数据结构>文档 (2)测试平台——Codeup新家(<算法笔记>对应OJ).洛谷.YCOJ ... 
- SharePoint删除图片库文件
			SPSecurity.RunWithElevatedPrivileges(delegate() { using (SPSite site = new SPSite(SPContext.Current. ... 
- JavaScript图形实例:迭代函数系统生成图形
			迭代函数系统(Iterated Function System,IFS)可以用来创建分形图案,它是分形理论的重要分支,也是分形图形处理中最富生命力而且最具有广阔应用前景的领域之一.这一工作最早可以追溯 ... 
- 如何基于 echarts 实现区间柱状图(包括横向)?
			目录 需求 借鉴 echarts 的 demo 最终实现思路 实现效果 遇到的问题: 代码映射 源码 最后 始终如一 需求 需要利用 echarts 实现区间柱状图,效果如下: 效果来源于:g2-柱状 ... 
- Django---进阶16<XSS攻击>
			目录 后台管理 添加文章 kindeditor富文本编辑器 编辑器上传图片 修改用户头像 bbs项目总结 后台管理 """ 当一个文件夹下文件比较多的时候 你还可以继续创 ... 
