WEB应用的业务逻辑,基本都在model层,但我习惯将model层分成 Service和Dao的两层结构。本篇主要讲述框架中Dao及Service的实现及使用,重点是MySQL常用操作的封装。
数据模型分层
-
DAO
即数据访问对象,在我的框架中,定义为直接对指定的“某个数据源”的增删改查的封装。
对于简单常用的增删改查,开发者可以直接使用框架的Dao对象来操作。
对于较为复杂的操作,比如复杂的条件语句,连表等,也保留执行手写SQL语句的功能。
-
Service
service层主要是封装较为复杂的业务逻辑,包括事务封装。
一般地,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结构分拆为多个参数
原来是这个样子的:
//additem := &memcache.Item{Key: key, Value: data, Expiration: expire}Client.Add(item)//getitem, _ := 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 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
//insertfor 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.Rowsif 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 servicetype 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()}