Go語言系列之手把手教你擼一個ORM(一)

Tassdar發表於2020-07-24

專案地址: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等功能,並且會支援引數化查詢,請關注後續文章,希望能給個星星,謝謝~

相關文章