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 ...
随机推荐
- DOM-BOM-EVENT(3)
3.Node常用属性 childNodes 获取所有子节点 <div id="wrap"> <div>1111</div> <div> ...
- Laravel表单验证提示设置多语言
默认表单提示是英文的,我们可以安装语言包构建多语言环境. 根据版本选择命令 For Laravel 7.x : run composer require caouecs/laravel-lang:~6 ...
- 客官,来看看AspNetCore的身份验证吧
开篇 这段时间潜水了太久,终于有时间可以更新一篇文章了. 通过本篇文章您将Get: Http的一些身份验证概念 在AspNetCore中实现身份验证方案 JWT等概念的基础知识 使用Bearer To ...
- 云服务器解析域名去掉Tomcat的8080端口号显示
- JavaScript基础对象创建模式之沙盘模式(026)
沙盘模式可以弥补命名空间模式中的两项不足之处: 使用唯一全局对象作为程序的全局变量入口,使得无法在同一程序中使用两个不同版本的API,因此它们使用的是同一个唯一的全局对象名,如MYAPP: 较长的嵌套 ...
- python-多任务-进程
什么是进程? 程序是静态的,当程序运行起来就叫做进程. 进程是操作系统分配资源的基本单元. 进程.线程的区别与优缺点 1. 定义的不同: 进程是系统进行资源分配的最小单位. 线程是进程的一个实体,是C ...
- 关于位图数据和标记位-P3
文章目录 1 背景 1.1 问题 2 问题1探究 2.1 没有区的情况 2.2 一个区的情况 2.3 两个区的情况 2.4 三个区的情况 2.5 四个区的情况 2.6 五个区的情况 3 问题2探究 3 ...
- 每日一题 - 剑指 Offer 38. 字符串的排列
题目信息 时间: 2019-06-29 题目链接:Leetcode tag:深度优先搜索 回溯法 难易程度:中等 题目描述: 输入一个字符串,打印出该字符串中字符的所有排列. 你可以以任意顺序返回这个 ...
- 「线段树」「单点修改」洛谷P1198 [JSOI2008]最大数
「线段树」「单点修改」洛谷P1198 [JSOI2008]最大数 题面描述 现在请求你维护一个数列,要求提供以下两种操作: 1. 查询操作. 语法:Q L 功能:查询当前数列中末尾L个数中的最大的数, ...
- 关于C++命名空间namespace的理解与使用介绍
0X00 前言 所谓namespace,是指标识符的各种可见范围.C++标准程序库中的所有标识符都被定义于一个名为std的namespace中. 0x01 与C语言区别 <iostream> ...