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 ...
随机推荐
- SpringBoot--使用socket搭建聊天室
1.添加依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId> ...
- PID各环节的意义和功能,自带PID的matlab编程实例
这是PID的标准形式包括比例/积分/微分三部分,e为偏差 下面我们分析三个环节的作用,设:当前系统状态A,目标状态B, e=B-A,初始状态e>0 (以下是个人的理解,欢迎读者评论) 1 比例环 ...
- JS判断 函数是否定义/变量是否定义
函数是否定义: <script type="text/javascript"> try { if(typeof FunName === "function&q ...
- 【UVA11383】 Golden Tiger Claw 【二分图KM算法(板子)】
题目 题目传送门:https://www.luogu.com.cn/problem/UVA11383 分析 最近刚刚学了二分图,然后来了一个这样的题,看完题意之后,稍微想一想就能想出来是一个二分图,然 ...
- 猿灯塔-Phaser 使用介绍
原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 本文将介绍 java.util.concurrent.Phaser,一个常常被大家忽略的并发工具.它和 CyclicBarrier 以及 Co ...
- 使用原生js来控制、修改CSS伪元素的方法总汇, 例如:before和:after
在网页中,如果需要使用辅助性/装饰性的内容的时候,我们不应该直接写在HTML中,这样会影响真正的内容,这就需要使用伪元素了,这是由于css的纯粹语义化是没有意义的.在使用伪元素的时候,会发现js并不真 ...
- 你从来没了解过的CSS浮动
浮动到底是做什么呢?他们是如何影响相关元素的盒模型的呢?浮动的元素与内联元素有什么不同呢?制定浮动元素的位置的具体规则是什么?clear属性是如何工作的,并且它的作用是什么? 即使是经验丰富的开发者也 ...
- 线性DP之小烈送菜
小烈送菜 小烈一下碰碰车就被乐满地的工作人员抓住了.作为扰乱秩序的惩罚,小烈必须去乐满地里的"漓江村"饭店端盘子. 服务员的工作很繁忙.他们要上菜,同时要使顾客们尽量高兴.一位服务 ...
- 小师妹学JVM之:JVM中的Safepoints
目录 简介 GC的垃圾回收器 分代回收器中的问题 safepoints safepoint一般用在什么地方 总结 简介 java程序员都听说过GC,大家也都知道GC的目的是扫描堆空间,然后将那些标记为 ...
- day32 异常处理、网络编程
目录 一.异常处理 1 什么是异常 2 为什么要处理异常 3 如何处理异常 3.1 语法错误 3.2 逻辑错误 3.3 两种处理逻辑异常的方式 3.3.1 可预知型错误 3.3.2 不可预知型错误 4 ...