WEB應用的業務邏輯,基本都在model層,但我習慣將model層分紅 Service和Dao的兩層結構。本篇主要講述框架中Dao及Service的實現及使用,重點是MySQL經常使用操做的封裝。php
DAOmysql
即數據訪問對象,在個人框架中,定義爲直接對指定的「某個數據源」的增刪改查的封裝。git
對於簡單經常使用的增刪改查,開發者能夠直接使用框架的Dao對象來操做。github
對於較爲複雜的操做,好比複雜的條件語句,連表等,也保留執行手寫SQL語句的功能。redis
Servicesql
service層主要是封裝較爲複雜的業務邏輯,包括事務封裝。mongodb
通常地,service基於dao進行封裝,目的是將一個須要屢次數據交互來完成的完整操做進行整合。 好比讀取用戶todo任務列表的邏輯:數據庫
先從 memcache中讀取緩存,若有,直接返回數組
緩存沒有,從數據庫中讀取,讀取成功後保存到緩存並返回緩存
也能夠在service層封裝與dao操做無關的業務邏輯。好比 而後發送網絡請求和請求結果的加工。
根椐協議方式,對數據源的訪問,一般是分爲SQL語句類和GET/SET類,前者表明是關係數據庫,後者的表明是memcache。個人框架也是優先實現了MySQL和Memcache的Dao,而對於目前比較火的redis和mongodb由於本人用得比較少,就暫不支持。
對於memcache的訪問,github.com/bradfitz/gomemcache/ 已支持的很好了,惟一不習慣的是 方法傳遞和返回的參數是items結構,我又手多的按php的使用習慣,在gomemcache的基礎上作了個wrapper:
將Get/GetMulti的返回類型改爲了[]byte,能夠直接使用。
對於Add/Set/Replace方法,則將單個參數的item結構分拆爲多個參數
原來是這個樣子的:
//add item := &memcache.Item{Key: key, Value: data, Expiration: expire} Client.Add(item) //get item, _ := Client.Get(key) value:= item.Value
我改爲了這樣:
func(this *Mc) Add(key string, data []byte, s ...int32){} func (this *Mc) Get(key string) ([]byte, error) {}
題外:
php用多了,仍是挺喜歡在函數定義中使用默認參數的,因此在go裏,就有不少 ...xxx類型的參數,好比在Add時,不指定第三參數的話,會有一個默認的expire
mc的協議是比較簡單的基於文本的協議,有興趣的也能夠徹底本身實現MC的整個操做。
對mysql的操做,我選擇了基於github.com/go-sql-driver/mysql來封裝。需求是:
增 將一個數據字典插入
刪 刪除指定條件的數據
改 修改指定條件的數據爲新指定的data字典
查 支持單記錄和多記錄查找,支持排序和limit,支持指定查詢的字段名
where支持多種條件
支持執行手動拼寫的SQL語句
支持事務
type MySQL struct { DB *sql.DB //mysql的鏈接句柄 tx *sql.Tx //事務句柄 Table string //表名 order string //排序,「id desc」 limit string //分頁,「offset,nums」 field string //查詢的字段 err error maxOpenConns int maxIdleConns int }
func NewMySQL(dsn string, table string, openConn int, idleConn int) (mysql *MySQL, Err error) { db, err := sql.Open("mysql", dsn) if err == nil { db.Ping() mysql = &MySQL{DB: db, Table: table, maxOpenConns: openConn, maxIdleConns: idleConn, field: "*"} } return } //切換操做的表名 func (this *MySQL) SetTable(table string) *MySQL { this.Table = table return this //SetXxx系列方法,都返回receiver自己,以支持鏈式調用 }
where參數,在get,update,delete中都會用到,方法原型是:
//where是一個map[string]interface{} //wh是可變參數,即支持多組 func (this *MySQL) Get(wh ...map[string]interface{}) func (this *MySQL) Update(data map[string]interface{}, wh ...map[string]interface{})
下面定義一下where的使用方式:
支持多個,能夠and或or鏈接 (用多組where條件來實現,組內是and,組間是or)
wh["a"] = "a1" wh["b"] = "b1" wh1["aa"] = "aa1" wh1["bb"] "bb1" //wh = `(a = "a1" and b= "b1" ) or (aa = "aa1" and bb = "bb1")`
除了「=」外,支持常見的 > ,< ,>=, <=, <>等操做符
//默認的普通的方式是「=」 wh["a"] = "1" //對於 >,<,<>,>=這類,將運算符放到key中 wh["a >"] = "3" wh["b >="] = "5" wh["c <>"] = "6" //where in方式,in放到key,值列表用分隔字符串表示 wh["a in"] = "1,2,3,4" // where a in(1,2,3,4)
完整的實現(傳入wh字典,解析後返回帶有佔位符的where子句和佔位對應的值列表):
func (this *MySQL) _parseWhere(wh ...map[string]interface{}) (string, []interface{}) { var cond []string var vals []interface{} for _, w := range wh { var c1 []string for k, v := range w { if strings.HasSuffix(strings.ToLower(k), "in") { val, ok := v.(string) if !ok { panic("where in must be string separate with \",\"") } inVals := strings.Split(val, ",") c1 = append(c1, k+" (?"+strings.Repeat(",?", len(inVals)-1)+")") for _, val := range inVals { vals = append(vals, val) } } else { r := []rune(k) last := string(r[len(r)-1:]) if last == "<" || last == ">" || last == "=" { c1 = append(c1, k+" ?") } else { c1 = append(c1, k+" = ?") } vals = append(vals, v) } } cStr := strings.Join(c1, " and ") if cStr != "" { cond = append(cond, "("+cStr+")") } } return strings.Join(cond, " or "), vals }
Insert和Update時,會用到data參數, data參數也是一個數據字典,傳遞字段及其值:
data["a"] = "aaa" data["b"] = "bb"
最終發送的sql是:
insert into table set a=?,b=? update table set a=?,b=? where ...
而後使用["aaa","bb"]來Exec
//insert for k, v := range data { fields = append(fields, k+"= ?") vals = append(vals, v) } sqlStr := fmt.Sprintf("insert into `%s` set %s", this.Table, strings.Join(fields, ",")) //update只是改一下語句的串接 return this.Exec(sqlStr, vals...)
Udate/Insert/ delete 最終都會調用Exec方法來執行,這個方法一樣能夠用來執行手寫的update/delete/insert等語句
func (this *MySQL) Exec(sqlStr string, vals ...interface{}) (id int64, err error) { stmt, err = this.DB.Prepare(sqlStr) //check err res, err := stmt.Exec(vals...) //check err if strings.HasPrefix(strings.ToLower(sqlStr), "insert") {//根椐前綴判斷語句類型,決定不一樣的返回值 id, err = res.LastInsertId() } else { id, err = res.RowsAffected() } return }
select語句,最終的表達形式是:
select [fields] from table [where] [order by ] [limit]
fields: 要查詢的字段,可經過SetField(fields string)方法設置,不設置時默認「*」
where: 條件子句,經過上述的where解釋得到
order by: 排序子句, 經過SetOrder(order string)設置,默認爲空,執行Query後會重置爲空(即做用範圍是單次查詢)
limit: limit子句, 經過SetLimti(limit string)方法設置,默認爲空(GetRow方法強制爲 "limit 1"),做用範圍一樣是單次查詢
SetXxx只是一系列簡單的Setter方法,用來設置相關的屬性值,同時,都返回receiver自己,以支持鏈式調用。 好比:
this.SetOrder("id desc").SetField("name,uid").Get(wh)
一樣地,Get/GetRow方法在組合後sql語句後,都會調用Query方法來執行,該方法一樣適用於執行手寫的SELECT語句,返回以字段名爲key,字段值爲value的map數組
func (this *MySQL) Query(sqlStr string, vals ...interface{}) (result []map[string]string, err error) { var rows *sql.Rows if this.tx != nil { rows, err = this.tx.Query(sqlStr, vals...) } else { rows, err = this.DB.Query(sqlStr, vals...) } if err == nil { //處理結果 defer rows.Close() cols, _ := rows.Columns() l := len(cols) rawResult := make([][]byte, l) rowResult := make(map[string]string) dest := make([]interface{}, l) // A temporary interface{} slice for i, _ := range rawResult { dest[i] = &rawResult[i] // Put pointers to each string in the interface slice } for rows.Next() { err = rows.Scan(dest...) if err == nil { for i, raw := range rawResult { key := cols[i] if raw == nil { rowResult[key] = NULL_VAL } else { rowResult[key] = string(raw) } } result = append(result, rowResult) } } } this.err = err return }
經過簡單的封裝,以更方便的使用事務
//開啓事務 func (this *MySQL) TransStart() error { tx, err := this.DB.Begin() if err != nil { return err } this.err = nil this.tx = tx return nil } //提交事務,若是事務中有錯誤發生,則自動回滾,並返回錯誤 func (this *MySQL) TransCommit() (err error) { if this.err != nil { err = this.err this.tx.Rollback() } else { err = this.tx.Commit() } this.tx = nil return } //手工回滾事務 func (this *MySQL) TransRollback() (err error) { err = this.tx.Rollback() this.tx = nil return }
啓用事務後,Exec/Query方法須要用 this.tx代替this.DB來調用 Prepare(sqlStr) 和 Query(sqlStr, vals...),即:
var rows *sql.Rows if this.tx != nil { rows, err = this.tx.Query(sqlStr, vals...) } else { rows, err = this.DB.Query(sqlStr, vals...) }
若是是Dao可以單獨完成的,不須要增長service層的,能夠直接在Controller中調用
func (this *Controller) User(){ id := 1 // User := this.NewMySQLDao("user") //創建對user表的dao操做,該方法其實用了dao.NewMySQL()來得到一個mysql的dao,並指定了table=user this.Render("user_list.tpl", User.GetRow(map[string]{"uid":id})) }
對於須要多個操做來完成一個邏輯的,則建議在Service層進行封裝。
package service type Service struct{ *ecgo.MySQL //ecgo爲框架的包名 } func NewService(mysql *MySQL) *Service{ return &Service{mysql} } //添加用戶,並返回用戶列表 func (this *Service) AddUser(uName string) (users []int){ } //刪除用戶,同時刪除用戶的其它數據 func (this *Service) DelUser(uid int){ //須要時,能夠調用this.SetTable切換當前操做的表名 this.TransStart() //開啓事務 //do something err := this.TransCommit() //提交,若是有錯則自動回滾 //Commit若是失敗,可使用this.LastError()查看最後產生的執行錯誤 //若是須要手工回滾,使用this.TransRollback()方法 }
import "service" func (this *Controller) User(){ User:=this.NewMySQLDao("user") u := service.NewService(User) //實際用時,可在PreControler中建立,或者加入單例模式,避免屢次建立 //u.AddUser() //u.DelUser() }