博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
编写GO的WEB开发框架 (九): Dao和Service
阅读量:6273 次
发布时间:2019-06-22

本文共 7588 字,大约阅读时间需要 25 分钟。

hot3.png

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

题外:

  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

//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()}

转载于:https://my.oschina.net/tim8670/blog/634459

你可能感兴趣的文章
英国网络安全公司Darktrace获6400万美元C轮融资
查看>>
CYQ.Data+EasyUI开发:几个相关的问题CheckBox、Tree、TreeGrid
查看>>
Extjs分页使用Java实现数据库数据查询
查看>>
BayWa收购光伏分销商Solarmatrix进军澳大利亚市场
查看>>
股东致函雅虎董事会要求别再烧钱 雅虎反呛
查看>>
移动OA的魅力--大众点评的“企业号”运用法则
查看>>
芯片进口额远超原油 中国芯待发力
查看>>
ARM想将芯片装进人类大脑 降低能耗是一大挑战
查看>>
Oracle数据库的备份方法
查看>>
Selenium 自动登录考勤系统
查看>>
关于如何以编程的方式执行TestNG
查看>>
智能照明造福千家万户 家居智能不再是梦
查看>>
物联网如何跳出“看起来很美”?
查看>>
浅谈MySQL 数据库性能优化
查看>>
拥抱白帽黑客,通用宣布安全漏洞报告项目
查看>>
《UNIX/Linux 系统管理技术手册(第四版)》——1.10 其他的权威文档
查看>>
灵动空间 创享生活
查看>>
《UNIX网络编程 卷1:套接字联网API(第3版)》——8.6 UDP回射客户程序:dg_cli函数...
查看>>
不要将时间浪费到编写完美代码上
查看>>
《第一桶金怎么赚——淘宝开店创业致富一册通》一一第1章 创业梦想,怎样起步...
查看>>