1. 程式人生 > >Go語言系列之手把手教你擼一個ORM(一)

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.執行查詢,可以看到控制檯輸出了資料庫內容,並像你發出了祖安問候 ![](https://img2020.cnblogs.com/blog/1417396/202007/1417396-20200724173121568-2044696344.png) 但是這個驅動的自帶方法十分原始,我們需要自己建立與資料庫型別一致的變數,然後取值在給欄位賦值,十分麻煩,所以我們要動手把這步搞成自動化的 想實現自動裝配就要解決三個問題:**1.自動建立變數來獲取資料庫值;2.把接受到值賦值給結構體物件;3.把物件拼接成一個物件陣列進行返回** 因為rows.Scan()方法要求我們必須傳入和查詢sql中:欄位順序和數量以及型別必須一致的變數,才可以成功接受到返回值,所以我們必須按需建立變數進行繫結,具體設計見下文 **1. 建立兩個結構體分別用來儲存結構體和結構體的欄位資訊** ```golang //型別快取 type TypeInfo struct { //型別名稱 TypeName string //型別下的欄位 FieldInfo []FieldInfo } //欄位快取 type FieldInfo struct { //欄位索引值 Index int //欄位名稱 FieldName string FieldValue reflect.Value FieldType reflect.StructField } ``` **2.封裝一個方法用於獲取結構體的元資料,儲存到我們上面定義的結構體中** ```golang 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.設計一個簡單的快取,把已經獲取到元資料進行快取避免重複獲取** ```golang 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返回欄位找到對應的結構體欄位進行匹配 裝配要返回的結構體物件 組裝一個物件資料進行返回 ```golang 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 } ``` 方法封裝完畢,我們跑個單元測試看一下效果 ![](https://img2020.cnblogs.com/blog/1417396/202007/1417396-20200724183557926-1394454963.png) 目前這個小架子剛開始寫,到釋出這篇文件為止僅封裝出了最基礎的查詢,接下來會實現Insert/Update等功能,並且會支援引數化查詢,請關注後續文章,希望能給個星星,謝謝~