編寫GO的WEB開發框架 (九): Dao和Service

WEB應用的業務邏輯,基本都在model層,但我習慣將model層分紅 Service和Dao的兩層結構。本篇主要講述框架中Dao及Service的實現及使用,重點是MySQL經常使用操做的封裝。php

數據模型分層

  • DAOmysql

    即數據訪問對象,在個人框架中,定義爲直接對指定的「某個數據源」的增刪改查的封裝。git

    對於簡單經常使用的增刪改查,開發者能夠直接使用框架的Dao對象來操做。github

    對於較爲複雜的操做,好比複雜的條件語句,連表等,也保留執行手寫SQL語句的功能。redis

  • Servicesql

    service層主要是封裝較爲複雜的業務邏輯,包括事務封裝。mongodb

    通常地,service基於dao進行封裝,目的是將一個須要屢次數據交互來完成的完整操做進行整合。 好比讀取用戶todo任務列表的邏輯:數據庫

    • 先從 memcache中讀取緩存,若有,直接返回數組

    • 緩存沒有,從數據庫中讀取,讀取成功後保存到緩存並返回緩存

也能夠在service層封裝與dao操做無關的業務邏輯。好比 而後發送網絡請求和請求結果的加工。

Dao實現

根椐協議方式,對數據源的訪問,一般是分爲SQL語句類和GET/SET類,前者表明是關係數據庫,後者的表明是memcache。個人框架也是優先實現了MySQL和Memcache的Dao,而對於目前比較火的redis和mongodb由於本人用得比較少,就暫不支持。

memcache dao

對於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) {}

題外:

  1. php用多了,仍是挺喜歡在函數定義中使用默認參數的,因此在go裏,就有不少 ...xxx類型的參數,好比在Add時,不指定第三參數的話,會有一個默認的expire

  2. mc的協議是比較簡單的基於文本的協議,有興趣的也能夠徹底本身實現MC的整個操做。

mysql dao

核心需求

對mysql的操做,我選擇了基於github.com/go-sql-driver/mysql來封裝。需求是:

  • 增 將一個數據字典插入

  • 刪 刪除指定條件的數據

  • 改 修改指定條件的數據爲新指定的data字典

  • 查 支持單記錄和多記錄查找,支持排序和limit,支持指定查詢的字段名

  • where支持多種條件

  • 支持執行手動拼寫的SQL語句

  • 支持事務

實現的過程

  • 首先,定義一個結構,做爲Dao方法的receiver。
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
}
  • 定義建立Dao對象的方法
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參數

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,Exec 和data參數

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語句,最終的表達形式是:

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...)
}

框架中怎麼使用Model

直接使用

若是是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

對於須要多個操做來完成一個邏輯的,則建議在Service層進行封裝。

  • 先編寫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()方法
}
  • 接下來,在Controler中就能夠這樣使用service的方法
import "service"

func (this *Controller) User(){
	User:=this.NewMySQLDao("user")
	u := service.NewService(User) //實際用時,可在PreControler中建立,或者加入單例模式,避免屢次建立
	//u.AddUser()
	//u.DelUser()
}
相關文章
相關標籤/搜索