使用Go封裝一個便捷的ORM
來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。# 使用Go封裝一個便捷的ORM最近在用Go寫一個Web開發架構,看了一些ORM,大部分都需要自己拼接SQL,對我這種用慣了`Laravel`的人來說,確實有點彆扭,所以想自己寫一個ORM,可以方便的對資料庫進行連貫操作由於代碼太多,不貼了,只講思路,具體代碼在這裡[silsuer/bingo](https://github.com/silsuer/bingo)## 思路1. 確定最後要做出的效果 我想要做成類似`Laravel`那種,操作資料庫大概是這樣`DB::table(dbName)->Where('id',1)->get()`2. 連貫操作原理 做出這種連貫操作的效果,除了結尾的方法,中間連續調用的那些方法都必須返回一個相同的對象 3. 定義資料庫物件 既然如此,那麼先定義一個`Mysql`的結構體 ```go // mysql結構體,用來儲存sql語句並執行 type Mysql struct { connection *sql.DB // 資料庫連接對象 sql string // 最終要執行的語句 whereSql string // where語句 limitSql string // limit語句 columnSql string // select 中的列語句 tableSql string // 表名語句 orderBySql string // order by 語句 groupBySql string // group by語句 havingSql string // having語句 tableName string // 表名 TableSchema string // 所在資料庫,在緩衝表結構的時候有用 Errors []error // 儲存在連貫操作中可能發生的錯誤 Result sql.Result // 儲存執行語句後的結果 Rows *sql.Rows // 儲存執行多行查詢語句後的行 Results []sql.Result Row *sql.Row // 儲存單行查詢語句後的行 //Res []map[string]interface{} // 把查詢結果產生一個map } ``` 這樣,我們每次連續調用的時候,只需要返回當前結構體的指標就可以了. 4. 串連資料庫 為了方便,先在公用函數中定義一個`DB()`函數,這個函數通過建立資料庫驅動,來返回一個資料庫物件(指標) ```go func DB() interface{} { // 返回一個驅動的操作類 // 傳入db配置 env的driver,執行個體化不同的類,單例模式,擷取唯一驅動 if Driver == nil { DriverInit() } con := Driver.GetConnection() // 擷取資料庫連接 return con } ``` 可以看到,為了節省資源,我們的資料庫連接和驅動串連都是單例模式,只允許存在一次 下面是初始化資料庫驅動的方法`DriverInit()` ```go // 資料庫驅動 type driver struct { name string // 驅動名 dbConfig string // 配置 } // 驅動初始化 func DriverInit() { Driver = &driver{} Driver.name = strings.ToUpper(Env.Get("DB_DRIVER")) switch Driver.name { case "MYSQL": // 初始化了驅動之後,開始初始化資料庫連接 Driver.dbConfig = Env.Get("DB_USERNAME") + ":" + Env.Get("DB_PASSWORD") + "@tcp(" + Env.Get("DB_HOST") + ":" + Env.Get("DB_PORT") + ")" + "/" + Env.Get("DB_NAME") + "?" + "charset=" + Env.Get("DB_CHARSET") break default: break } } ``` 我們在初始化驅動的時候,會判斷設定檔中用的是哪種資料庫,然後根據資料庫去拼接串連資料庫的字串 這樣為我們的ORM可以支援多個資料庫提供了可能 配置的字串做好了,接下來就要開始擷取資料庫的單例串連了 ```go // 根據資料庫驅動,擷取資料庫連接 func (d *driver) GetConnection() interface{} { switch d.name { case "MYSQL": m := mysql.Mysql{} // 執行個體化結構體 m.TableSchema = Env.Get("DB_NAME") // 資料庫名 m.Init(Driver.dbConfig) // 設定表名和資料庫連接 return &m // 返回執行個體 break default: break } return nil } ``` 擷取串連時首先建立了一個資料庫的對象,然後把一些基本資料賦給了這個對象的對應屬性,`Init`方法 擷取了這個人資料庫的唯一串連: ```go func (m *Mysql) Init(config string) { // 擷取單例串連 m.connection = GetInstanceConnection(config) // 擷取資料庫連接 } var once sync.Once // 設定單例的資料庫連接 func GetInstanceConnection(config string) *sql.DB { once.Do(func() { // 只做一次 db, err := sql.Open("mysql", config) if err != nil { panic(err) } conn = db }) return conn } ``` `Once.Do`是`sync`包中提供的只允許代碼執行一次的方法 這樣我們就擷取到了資料庫的串連,然後把這個串連放到`Mysql`對象的 `connection`中,返回即可 5. 執行連貫操作 要注意由於我們使用的資料庫不同,`DB()`返回的是一個`interface{}`,所以需要我們重新轉成`*mysql.Mysql`: ```go bingo.DB().(*mysql.Mysql) ``` 接下來,只需要定義各種`Mysql`結構體的方法,然後返回這個結構體的指標就可以了, 篇幅問題,唯寫一個最簡單的在`test`表中查詢`id>1`的資料,代碼效果是這樣: ```go res:= bingo.DB().(*mysql.Mysql).Table("test").Where("id",">",1).Get() ``` 首先,寫`Table`方法,只是給Mysql對象賦個值而已: ```go // 初始化一些sql的值 func (m *Mysql) Table(tableName string) *Mysql { m.tableSql = " " + tableName + " " m.whereSql = "" m.columnSql = " * " m.tableName = tableName return m } ``` 然後寫`Where`方法,這個稍微複雜一點,如果傳入兩個參數,那麼他們之間是等於的關係,如果是三個參數 那麼第二個參數就是他們直接的關係 ```go func (m *Mysql) Where(args ... interface{}) *Mysql { // 要根據傳入欄位的名字,獲得欄位類型,然後判斷第三個參數是否要加引號 // 最多傳入三個參數 a=b // 先判斷where sql 裡有沒有 where a=b cif := m.GetTableInfo().Info // 先判斷這個欄位在表中是否存在,如果存在,擷取類型擷取值等 cType := "" if _, ok := cif[convertToString(args[0])]; ok { cType = cif[convertToString(args[0])].Type } else { m.Errors = append(m.Errors, errors.New("cannot find a column named "+convertToString(args[0])+" in "+m.tableName+" table")) return m // 終止這個函數 } var ifWhere bool whereArr := strings.Fields(m.whereSql) if len(whereArr) == 0 { // 空的 ifWhere = false } else { if strings.ToUpper(whereArr[0]) == "WHERE" { ifWhere = true // 存在where } else { ifWhere = false // 不存在where } } switch len(args) { case 2: // 有where 只需要寫 a=b if ifWhere { // 如果是字串類型,加引號 if isString(cType) { m.whereSql = m.whereSql + ` AND ` + convertToString(args[0]) + `='` + convertToString(args[1]) + `'` } else { m.whereSql = m.whereSql + ` AND ` + convertToString(args[0]) + `=` + convertToString(args[1]) } } else { // 沒有where 要寫 where a=b if isString(cType) { m.whereSql = ` WHERE ` + convertToString(args[0]) + `='` + convertToString(args[1]) + `'` } else { m.whereSql = ` WHERE ` + convertToString(args[0]) + `=` + convertToString(args[1]) } } break case 3: // 有where 只需要寫 a=b if ifWhere { // 如果是字串類型,加引號 if isString(cType) { m.whereSql = m.whereSql + ` AND ` + convertToString(args[0]) + convertToString(args[1]) + `'` + convertToString(args[2]) + `'` } else { m.whereSql = m.whereSql + ` AND ` + convertToString(args[0]) + convertToString(args[1]) + convertToString(args[2]) } } else { // 沒有where 要寫 where a=b if isString(cType) { m.whereSql = ` WHERE ` + convertToString(args[0]) + convertToString(args[1]) + `'` + convertToString(args[2]) + `'` } else { m.whereSql = ` WHERE ` + convertToString(args[0]) + convertToString(args[1]) + convertToString(args[2]) } } break default: m.Errors = append(m.Errors, errors.New("missing args length in where function")) } return m } ``` 上面的代碼中我用到了`GetTableInfo()` 方法,這個方法是擷取當前表的結構,為了快速的對錶執行操作,或者過濾掉表不存在的欄位 需要緩衝表的結構,把列名和列類型緩衝起來,具體代碼不貼了,在這裡 [緩衝表結構](https://github.com/silsuer/bingo/blob/master/drivers/db/mysql/insert.go) 然後會根據`Mysql`結構體的`whereSql`欄位,拼接新的`whereSql`,並重新賦值 接下來,寫`Get()`方法,執行最後的SQL語句即可 ```go // 查詢資料 func (m *Mysql) Get() *Mysql { m.sql = "select" + m.columnSql + "from " + m.tableSql + m.whereSql + m.limitSql + m.orderBySql rows, err := m.connection.Query(m.sql) m.checkAppendError(err) m.Rows = rows return m } ``` 首先拼接語句,然後執行原生的Query方法,把執行結果放置在結構體中即可 這樣我們就拿到了結構體,可以把它打出來看看 ```go // 擷取test表中id大於1的所有資料 res:= bingo.DB().(*mysql.Mysql).Table("test").Where("id",">",1).Get() // 判斷執行是否出錯 if len(res.Errors)!=0{ fmt.Fprintln(w,"執行出錯!") } // 執行成功,開始遍曆 for res.Rows.Next() { var id,age int var name string res.Rows.Scan(&id,&name,&age) fmt.Fprintln(w,"id:"+strconv.Itoa(id)+" name:"+name+" age:"+strconv.Itoa(age)) } ``` 6. 小結 由於篇幅問題,我唯寫了最簡單的條件查詢,其他的可以去[GitHub](https://github.com/silsuer/bingo)上看 目前實現的有: 1. 建立資料庫 2. 建立資料表 3. 插入資料(4種方法:插入一條資料,插入一條資料並過濾多餘欄位,批量插入資料,批量插入資料並過濾多餘欄位) 4. 更新資料(3種方法:更新一條資料,更新一條資料並過濾多餘欄位,批次更新資料並過濾多餘欄位) 5. 查詢資料(條件查詢、limit、order by等) 6. 刪除資料 7. 刪除資料表(2種方法:delete刪除,truncate清空) 8. 清空資料庫中的所有表資訊而不刪除表 9. 清空資料庫,刪除所有表 10. 刪除資料表 將要實現的有: 1. 關聯查詢 2. join查詢 3. 分組查詢 4. 快速分頁 5. 資料庫遷移 7. 具體用法請去看看README,時間問題,寫的有點亂,等這個玩意兒開發完了會重新整理一份文檔的 最後一句,求star,求翻牌子~~~ヾ(*´▽‘*)ノ 424 次點擊 ∙ 1 贊