2019独角兽企业重金招聘Python工程师标准>>>
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结构分拆为多个参数
原来是这个样子的:
//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 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()
}