导语 | 当我深入的学习和了解了GORM、XORM后,我觉得它们不够简洁和优雅,有些笨重,有很大的学习成本。本着学习和探索的目的,于是我自己实现了一个简单且优雅的go语言版本的ORM。本文主要从基础原理开始介绍,到一步一步步骤实现,继而完成整个简单且优雅的MySQL ORM。

一、前置学习

(一)为什么要用ORM

我们在使用各种语言去做需求的时候,不管是PHP,Golang还是C++等语言,应该都接触使用过用ORM去链接数据库,这些ORM有些是项目组自己整合实现的,也有些是用的开源的组件。特别在1个全新的项目中,我们都会用一个ORM框架去连接数据库,而不是直接用原生代码去写SQL链接,原因有很多,有安全考虑,有性能考虑,但是,更多的我觉得还是懒(逃)和开发效率低,因为有时候一些SQL写起来也是很复杂很累的,特别是查询列表的时候,又是分页,又是结果集,还需要自己for next去判断和遍历,是真的有累,开发效率非常低。如果有个ORM,数据库config一配,几个链式函数一调,咔咔咔,结果就出来了。

所以ORM就是我们和数据库交互的中间件,我们通过ORM提供的各种快捷的方法去和数据库产生交互,继而更加方便高效的实现功能。

一句话总结什么是ORM: 提供更加方便快捷的curd方法去和数据库产生交互

(二)Golang里面是如何原生连接MySQL的

说完了啥是ORM,以及为啥用ORM之后,我们再看下Golang里面是如何原生连接MySQL的,这对于我们开发一个ORM帮助很大,只有弄清楚了它们之间交互的原理,我们才能更好的开始造。

原生代码连接MySQL,一般是如下步骤。

首先是导入sql引擎和mysql的驱动:

import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)

连接MySQL:

db, err := sql.Open("mysql", "root:123456@tcp(127.0.0.1:3306)/ApiDB?charset=utf8") //第一个参数数驱动名
if err != nil {panic(err.Error())
}

然后,我们快速过一下,如何增删改查:

增:

//方式一:
result, err := db.Exec("INSERT INTO userinfo (username, departname, created) VALUES (?, ?, ?)","lisi","dev","2020-08-04")//方式二:
stmt, err := db.Prepare("INSERT INTO userinfo (username, departname, created) VALUES (?, ?, ?)")result2, err := stmt.Exec("zhangsan", "pro", time.Now().Format("2006-01-02"))

删:

//方式一:
result, err := db.Exec("delete from userinfo where uid=?", 10795)//方式二:
stmt, err := db.Prepare("delete from userinfo where uid=?")result3, err := stmt.Exec("10795")

改:

//方式一:
result, err := db.Exec("update userinfo set username=? where uid=?", "lisi", 2)//方式二:
stmt, err := db.Prepare("update userinfo set username=? where uid=?")result, err := stmt.Exec("lisi", 2)

查:

//单条
var username, departname, status string
err := db.QueryRow("select username, departname, status from userinfo where uid=?", 4).Scan(&username, &departname, &status)
if err != nil {fmt.Println("QueryRow error :", err.Error())
}
fmt.Println("username: ", username, "departname: ", departname, "status: ", status)//多条:
rows, err := db.Query("select username, departname, status from userinfo where username=?", "yang")
if err != nil {fmt.Println("QueryRow error :", err.Error())
}//定义一个结构体,存放数据模型
type UserInfo struct {Username   string `json:"username"`Departname string `json:"departname"`Status    string `json:"status"`
}//初始化
var user []UserInfofor rows.Next() {var username1, departname1, status1 stringif err := rows.Scan(&username1, &departname1, &status1); err != nil {fmt.Println("Query error :", err.Error())}user = append(user, UserInfo{Username: username1, Departname: departname1, Status: status1})
}

更多具体详细的课程和说明,可以参考我写的这篇文章:https://blog.csdn.net/think2me/article/details/108317492

所以,总结一下,Golang里面原生连接MySQL的方法,非常简单,就是直接写sql嘛,简单粗暴点就直接Exec,复杂点但是效率会高一些就先Prepare再Exec。总体而言,这个学习成本是非常低的,最大的问题嘛,就是麻烦和开发效率点。

所以我在想?我是不是可以基于原生代码库的这个优势,自己开发1个ORM呢,第一:它能提供了各式各样的方法来提高开发效率,第二:底层直接转换拼接成最终的SQL,去调用这个原生的组件,来和MySQL去交互。这样岂不是一箭双雕,既能提高开发效率,又能保持足够的高效和简单。完美!

(三)ORM框架构想

本ORM库原理是简单的SQL拼接。暴露各种CURD方法,并在底层逻辑拼接成Prepare和Eexc占位符部分,继而来调用“github.com/go-sql-driver/mysql”驱动的方法来实现和数据库交互。

首先,先取个厉害的名字吧:smallorm,嗯,还行!

然后,整个调用过程采用链式的方法,这样比较方便,比如这样子

db.Where().Where().Order().Limit().Select()

其次,暴露的CURD方法,使用起来要简单,名字要清晰,无歧义,不要搞一大堆复杂的间接调用。

OK,我们梳理一下,sql里面常用到的一些curd的方法,把他们整理成ORM的一个个方法,并按照这个一步一步来实现,如下:

  • 连接Connect

  • 设置表名Table

  • 新增/替换Insert/Replace

  • 条件Where

  • 删除Delete

  • 修改Update

  • 查询Select

  • 执行原生SQLExec/Query

  • 设置查询字段Field

  • 设置大小Limit

  • 聚合查询Count/Max/Min/Avg/Sum

  • 排序Order

  • 分组Group

  • 分组后判断Having

  • 获取执行生成的完整SQLGetLastSql

  • 事务Begin/Commit/Rollback/

其中Insert/Replace/Delete/Select/Update是整个链式操作的最后一步。是真正的和MySQL交互的方法,后面不能再链式接其他的操作方法。

所以,我们可以畅享一下,这个完成后的ORM,是如何调用的:

增:

type User1 struct {Username   string `sql:"username"`Departname string `sql:"departname"`Status     int64  `sql:"status"`
}user2 := User1{Username:   "EE",Departname: "22", Status:     1,
}// insert into userinfo (username,departname,status) values ('EE', '22', 1)id, err := e.Table("userinfo").Insert(user2)

删:

// delete from userinfo where (uid = 10805)result1, err := e.Table("userinfo").Where("uid", "=", 10805).Delete()

改:

// update userinfo set departname=110 where (uid = 10805) result1, err := e.Table("userinfo").Where("uid", "=", 10805).Update("departname", 110)

查:

// select uid, status from userinfo where (departname like '%2') or (status=1)  order by uid desc limit 1result, err := e.Table("userinfo").Where("departname", "like", "%2").OrWhere("status", 1).Order("uid", "desc").Limit(1).Field("uid, status").Select()//select uid, status from userinfo where (uid in (1,2,3,4,5)) or (status=1)  order by uid desc limit 1result, err := e.Table("userinfo").Where("uid", "in", []int{1,2,3,4,5}).OrWhere("status", 1).Order("uid", "desc").Limit(1).Field("uid, status").SelectOne()type User1 struct {Username   string `sql:"username"`Departname string `sql:"departname"`Status     int64  `sql:"status"`
}user2 := User1{Username:   "EE",Departname: "22", Status:     1,
}user3 := User1{Username:   "EE",Departname: "22",Status:     2,
}// select * from userinfo where (Username='EE' and Departname='22' and Status=1) or (Username='EE' and Departname='22' and Status=2)  limit 1
id, err := e.Table("userinfo").Where(user2).OrWhere(user3).SelectOne()

二、开始造

(一)连接Connect

连接MySQL比较简单,直接把原生的sql.Open(“mysql”, dsn)方法套一个函数壳即可,但是需要考虑协程和长连接的保持以及ping失败的情况。我们这里第一版本就先不考虑了。

第一步,先构造1个变量引擎SmallormEngine,它是结构体类型的,用来存储各种各样的数据,其他的对外暴露的CURD方法也是基于这个结构体来继承的。

type SmallormEngine struct {Db           *sql.DBTableName    stringPrepare      stringAllExec      []interface{}Sql          stringWhereParam   stringLimitParam   stringOrderParam   stringOrWhereParam stringWhereExec    []interface{}UpdateParam  stringUpdateExec   []interface{}FieldParam   stringTransStatus  intTx           *sql.TxGroupParam   stringHavingParam  string
}

因为我们这ORM的底层本质是SQL拼接,所以,我们需要把各种操作方法生成的数据,都保存到这个结构体的各个变量上,方便最后一步生成SQL。

其中需要简单说明的是这2个字段:Db字段的类型是*sql.DB,它用于直接进行CURD操作,Tx是*sql.Tx类型的,它是数据库的事务操作,用于回滚和提交。这个后面会详细讲,这里有一个大致的概念即可。

接下来就可以写连接操作了:

//新建Mysql连接
func NewMysql(Username string, Password string, Address string, Dbname string) (*SmallormEngine, error) {dsn := Username + ":" + Password + "@tcp(" + Address + ")/" + Dbname + "?charset=utf8&timeout=5s&readTimeout=6s"db, err := sql.Open("mysql", dsn)if err != nil {return nil, err}//最大连接数等配置,先占个位//db.SetMaxOpenConns(3)//db.SetMaxIdleConns(3)return &SmallormEngine{Db:         db,FieldParam: "*",}, nil
}

创建了一个方法NewMysql来创建1个新的连接,参数是(用户名,密码,ip和端口,数据库名)。之所以用这个名字的考虑是:1. 万一2.0版本支持了其他数据库呢(手动狗头)2. 后续连接池的加入。

其次,如何实现链式的方式调用呢?只需要在每个方法返回实例本身即可,比如:

func (e *SmallormEngine) Where (name string) *SmallormEngine {return e
}func (e *SmallormEngine) Limit (name string) *SmallormEngine {return e
}

这样我们就可以链式的调用了:

e.Where().Where().Limit()

(二)设置/读取表名Table/GetTable

我们需要1个设置和读取数据库表名字的方法,因为我们所有的CURD都是基于某张表的:

//设置表名
func (e *SmallormEngine) Table(name string) *SmallormEngine {e.TableName = name//重置引擎e.resetSmallormEngine()return e
}//获取表名
func (e *SmallormEngine) GetTable() string {return e.TableName
}

这样我们每一次调用Table()方法,就给本次的执行设置了一个表名。并且会清空SmallormEngine节点上挂载的所有数据。

(三)新增/替换Insert/Replace

  • 单个数据插入

下面就是本ORM第一个重头戏和挑战点了,如何往数据库里插入数据?在如何用ORM实现本功能之前,我们先回忆下上面讲的原生的代码是如何插入的:

我们用先Prepare再Exec这种方式,高效且安全:

stmt, err := db.Prepare("INSERT INTO userinfo (username, departname, created) VALUES (?, ?, ?)")result2, err := stmt.Exec("zhangsan", "pro", time.Now().Format("2006-01-02"))

我们分析下它的做法:

  • 先在Prepare里,把插入的数据的value值用?占位符代替,有几个value就用几个?

  • 再Exec里面,把value值给补上,和?的数量一直即可。

ok,整明白了。那我们就按照这2部拆分数据即可。

为了保持方便,我们调用这个Insert方法进行插入数据的时候,参数是要传1个k-v的键值对类,比如[field1:value1,field2:value2,field3:value3],field表示表的字段,value表示字段的值。在go语言里面,这样的类型可以是Map或者Struct,但是Map必须得都是同一个类型的,显然是不符合数据库表里面,不同的字段可能是不同的类型的这一情况,所以,我们选择了Struct结构体, 它里面是可以有多种数据类型存在,也刚好符合情况。

由于go里面的数据都得是先定义类型,再去初始化1个值,所以,大致的调用过程是这样的:

type User struct {Username   string `sql:"username"`Departname string `sql:"departname"`Status     int64  `sql:"status"`
}user2 := User{Username:   "EE",Departname: "22", Status:     1,
}id, err := e.Table("userinfo").Insert(user2)

我们注意下,User结构体的每一个元素后面都有一个sql:“xxx”,这个叫Tag标签。这是干啥用的呢?是因为go里面首字母大写表示是可见的变量,所以如果是可见的变量都是大写字母开头,而sql语句表里面的字段首字母名一般是小写,所以,为了照顾这个特殊的关系,进行转换和匹配,才用了这个标签特性。如果你的表的字段类型也是大小字母开头,那就可以不需要这个标签,下面我们会具体说到如何转换匹配的。

所以,接下来的难点就是把user2进行解析,拆分成这2步:

第一步:将sql:“xxx”标签进行解析和匹配,依次替换成全小写的,解析成(username, departname, status),并且依次生成对应数量的。

stmt, err := db.Prepare("INSERT INTO userinfo (username, departname, status) VALUES (?, ?, ?)")

第二步:将user2的子元素的值都拆出来,放入到Exec中。

result2, err := stmt.Exec("EE", "22", 1)

那么,user2里面的3个子元素的field,如何解析成(username,departname,status)呢?由于我们是一个通用的方法,golang是没法直接通过for循环来知道传入的数据结构参数里面包含哪些field和value的,咋办呢?这个时候,大名鼎鼎的反射就可以派上用场了。我们可以通过反射来推导出传入的结构体变量,它的field是多少,value是什么,类型是什么。tag是什么。都可以通过反射来推导出来。

我们现在试一下其中的2个函数reflect.TypeOf和reflect.ValueOf:

type User struct {Username   string `sql:"username"`Departname string `sql:"departname"`Status     int64  `sql:"status"`
}user2 := User{Username:   "EE",Departname: "22", Status:     1,
}//反射出这个结构体变量的类型
t := reflect.TypeOf(user2)//反射出这个结构体变量的值
v := reflect.ValueOf(user2)fmt.Printf("==== print type ====\n%+v\n", t)
fmt.Printf("==== print value ====\n%+v\n", v)

我们打印看看,结果是啥?

==== print type ====
main.User==== print value ====
{Username:EE Departname:22 Status:1}

通过上面的打印,我们可以知道了,他的类型是User这个类型,值也是我们想要的值。OK。第一步完成。接下来,我们接下来通过for循环遍历t.NumField()和t.Field(i)来拆分里面的值:

//反射type和value
t := reflect.TypeOf(user2)
v := reflect.ValueOf(user2)//字段名
var fieldName []string//问号?占位符
var placeholder []string//循环判断
for i := 0; i < t.NumField(); i++ {//小写开头,无法反射,跳过if !v.Field(i).CanInterface() {continue}//解析tag,找出真实的sql字段名sqlTag := t.Field(i).Tag.Get("sql")if sqlTag != "" {//跳过自增字段if strings.Contains(strings.ToLower(sqlTag), "auto_increment") {continue} else {fieldName = append(fieldName, strings.Split(sqlTag, ",")[0])placeholder = append(placeholder, "?")}} else {fieldName = append(fieldName, t.Field(i).Name)placeholder = append(placeholder, "?")}//字段的值e.AllExec = append(e.AllExec, v.Field(i).Interface())
}//拼接表,字段名,占位符
e.Prepare =  "insert into " + e.GetTable() + " (" + strings.Join(fieldName, ",") + ") values(" + strings.Join(placeholder, ",") + ")"

如上面所示:t.NumField()可以获取到这个结构体有多少个字段用于for循环,t.Field(i).Tag.Get(“sql”)可以获取到包含sql:“xxx”的tag的值,我们用来sql匹配和替换。t.Field(i).Name可以获取到字段的field名字。通过v.Field(i).Interface()可以获取到字段的value值。e.GetTable()来获取我们设置的标的名字。通过上面的这一段稍微有点复杂的反射和拼接,我们就完成了Db.Prepare部分:

e.Prepare =  "INSERT INTO userinfo (username, departname, status) VALUES (?, ?, ?)"

接下来,我们来获取stmt.Exec里面的值的部分,上面我们把所有的值都放入到了e.AllExec这个属性里面,之所以它用interface类型,是因为,结构体里面的值的类型是多变的,有可能是int型,也可能是string类型。

//申明stmt类型
var stmt *sql.Stmt//第一步:Db.prepare
stmt, err = e.Db.Prepare(e.Prepare)//第二步:执行exec,注意这是stmt.Exec
result, err := stmt.Exec(e.AllExec...)
if err != nil {//TODO
}//获取自增ID
id, _ := result.LastInsertId()
1. 批量插入,传入的数据就是一个切片数组了,`[]struct` 这样的数据类型了。
2. 我们得先用反射算出,这个数组有多少个元素。这样好算出 VALUES 后面有几个`()`的占位符。
3. 搞2个for循环,外面的for循环,得出这个子元素的type和value。里面的第二个for循环,就和单个插入的反射操作一样了,就是算出每一个子元素有几个字段,反射出field名字,以及对应`()`里面有几个?问号占位符。
4. 2层for循环把切片里面的每个元素的每个字段的value放入到1个统一的AllExec中。

OK,直接上代码吧:

//批量插入
func (e *SmallormEngine) BatchInsert(data interface{}) (int64, error) {return e.batchInsertData(data, "insert")
}//批量替换插入
func (e *SmallormEngine) BatchReplace(data interface{}) (int64, error) {return e.batchInsertData(data, "replace")
}//批量插入
func (e *SmallormEngine) batchInsertData(batchData interface{}, insertType string) (int64, error) {//反射解析getValue := reflect.ValueOf(batchData)//切片大小l := getValue.Len()//字段名var fieldName []string//占位符var placeholderString []string//循环判断for i := 0; i < l; i++ {value := getValue.Index(i) // Value of itemtyped := value.Type()      // Type of itemif typed.Kind() != reflect.Struct {panic("批量插入的子元素必须是结构体类型")}num := value.NumField()//子元素值var placeholder []string//循环遍历子元素for j := 0; j < num; j++ {//小写开头,无法反射,跳过if !value.Field(j).CanInterface() {continue}//解析tag,找出真实的sql字段名sqlTag := typed.Field(j).Tag.Get("sql")if sqlTag != "" {//跳过自增字段if strings.Contains(strings.ToLower(sqlTag), "auto_increment") {continue} else {//字段名只记录第一个的if i == 1 {fieldName = append(fieldName, strings.Split(sqlTag, ",")[0])}placeholder = append(placeholder, "?")}} else {//字段名只记录第一个的if i == 1 {fieldName = append(fieldName, typed.Field(j).Name)}placeholder = append(placeholder, "?")}//字段值e.AllExec = append(e.AllExec, value.Field(j).Interface())}//子元素拼接成多个()括号后的值placeholderString = append(placeholderString, "("+strings.Join(placeholder, ",")+")")}//拼接表,字段名,占位符e.Prepare = insertType + " into " + e.GetTable() + " (" + strings.Join(fieldName, ",") + ") values " + strings.Join(placeholderString, ",")//preparevar stmt *sql.Stmtvar err errorstmt, err = e.Db.Prepare(e.Prepare)if err != nil {return 0, e.setErrorInfo(err)}//执行exec,注意这是stmt.Execresult, err := stmt.Exec(e.AllExec...)if err != nil {return 0, e.setErrorInfo(err)}//获取自增IDid, _ := result.LastInsertId()return id, nil
}//自定义错误格式
func (e *SmallormEngine) setErrorInfo(err error) error {_, file, line, _ := runtime.Caller(1)return errors.New("File: " + file + ":" + strconv.Itoa(line) + ", " + err.Error())
}

开始总结一下上面这一坨关键的地方。首先是获取这个切片的大小,用于第一个for循环。可以通过下面的2行代码:

//反射解析
getValue := reflect.ValueOf(batchData)//切片大小
l := getValue.Len()

其次,在第一个for循环里面,可以通过value:= getValue.Index(i)来获取这个切片里面的第i个元素的值,类似于上面插入单个数据中,反射出结构体的值一样:v:= reflect.ValueOf(data)

然后,通过typed:= value.Type()来获取这第i个元素的类型。类似于上面插入单个数据中,反射出结构体的类型一样:t := reflect.TypeOf(data) 。这个东西被反射出来,主要是为了获取tag标签用。

第二个for循环里面的反射逻辑,基本上是和单个插入是一样的了,唯一需要注意的就是,fieldName的值,因为我们只需要1个,所以我们用i==1判断了一下。加入单次即可。

再一个就是placeholderString这个变量,因为我们为了实现多个()的效果,所以就又搞了1个切片。

这样,批量插入,批量替换插入的逻辑就完成了。

  • 单个和批量合二为一

为了使我们的ORM足够的优雅和简单,我们可以把单个插入和批量插入,搞成1个方法暴露出去。那怎么识别出传入的数据是单个结构体,还是切片结构体呢?还是得用反射:

reflect.ValueOf(data).Kind()

它能给出我们答案。如果我们传的是单个结构体,那么它的值就是Struct,如果是切片数组,那么值就是Slice和Array。这样我们就好办了,我们只需要稍做判断即可:

//插入
func (e *SmallormEngine) Insert(data interface{}) (int64, error) {//判断是批量还是单个插入getValue := reflect.ValueOf(data).Kind()if getValue == reflect.Struct {return e.insertData(data, "insert")} else if getValue == reflect.Slice || getValue == reflect.Array {return e.batchInsertData(data, "insert")} else {return 0, errors.New("插入的数据格式不正确,单个插入格式为: struct,批量插入格式为: []struct")}
}//替换插入
func (e *SmallormEngine) Replace(data interface{}) (int64, error) {//判断是批量还是单个插入getValue := reflect.ValueOf(data).Kind()if getValue == reflect.Struct {return e.insertData(data, "replace")} else if getValue == reflect.Slice || getValue == reflect.Array {return e.batchInsertData(data, "replace")} else {return 0, errors.New("插入的数据格式不正确,单个插入格式为: struct,批量插入格式为: []struct")}
}

OK,完成。

(四)条件Where

  • 结构体参数调用

下面,我们开始实现Where方法的逻辑,这个where主要是为了替换sql语句中where后面这部分的逻辑,sql语句中where用的还是非常多的,比如原生sql:

select * from userinfo where status = 1
delete from userinfo where status = 1 or departname != "aa"
update userinfo set departname = "bb" where status = 1 and departname = "aa"

所以,把where后面的数据单独拆出来,搞成1个Where方法是很有必要的。大部分的ORM也是这样做的。

通过观察上面3句sql,我们可以得出基本的where的结构,要么只有1个条件,这个条件的比较复符是丰富的,比如:=, !=, like,<,>等等。要么是多个条件,用and或者or隔开,表示且和或的关系。

通过最上面的原生代码,我们是可以发现的,where部分也是一样的,先用Prepare生成问号占位符,再和Exce替换值的方式来操作。

stmt, err := db.Prepare("delete from userinfo where uid=?")
result3, err := stmt.Exec("10795")stmt, err := db.Prepare("update userinfo set username=? where uid=?")
result, err := stmt.Exec("lisi", 2)

所以,where部分的拆分,其实也是分2部来走。和插入的2步走的逻辑是一样的。大致的调用过程如下:

type User struct {Username   string `sql:"username"`Departname string `sql:"departname"`Status     int64  `sql:"status"`
}user2 := User{Username:   "EE",Departname: "22", Status:     1,
}result1, err1 := e.Table("userinfo").Where(user2).Delete()
result2, err2 := e.Table("userinfo").Where(user2).Select()

我们本次实现的是Where部分,where是中间层,它不会具体去执行结果的,它做的仅仅是将数据拆分出来,用2个新的子元素WhereParam和WhereExec来暂存数据,给最后的CURD操作方法来使用。

我们开始写代码,和Insert方法的反射逻辑几乎一样。

func (e *SmallormEngine) Where(data interface{}) *SmallormEngine {//反射type和valuet := reflect.TypeOf(data)v := reflect.ValueOf(data)//字段名var fieldNameArray []string//循环解析for i := 0; i < t.NumField(); i++ {//首字母小写,不可反射if !v.Field(i).CanInterface() {continue}//解析tag,找出真实的sql字段名sqlTag := t.Field(i).Tag.Get("sql")if sqlTag != "" {fieldNameArray = append(fieldNameArray, strings.Split(sqlTag, ",")[0]+"=?")} else {fieldNameArray = append(fieldNameArray, t.Field(i).Name+"=?")}//反射出Exec的值。e.WhereExec = append(e.WhereExec, v.Field(i).Interface())}//拼接e.WhereParam += strings.Join(fieldNameArray, " and ")return e
}

这样,我们就可以调用Where()反复,转换成生成了2个暂存变量。我们打印下这2个值看看:

WhereParam = "username=? and departname=? and Status=?"
WhereExec = []interface{"EE", "22", 1}

由于Where()是中间态的方法,是可以提供多次调用的,每次调用都是and的关系。比如这样:

e.Table("userinfo").Where(user2).Where(user3).XXX

所以,我们得改造一下e.WhereParam得让他拼接上一次生成的生成的数据。

先判断理一下,是否为空,如果不为空,则说明这是第二次调用了,我们用 “and (”来做隔离。

//多次调用判断
if e.WhereParam != "" {e.WhereParam += " and ("
} else {e.WhereParam += "("
}

//结束拼接的时候,加上结束括号“) ”。

e.WhereParam += strings.Join(fieldNameArray, " and ") + ") "

这样,就达到了我们的目的了。我们看下多次调用后的打印结果:

WhereParam = "(username=? and departname=? and status=?) and (username=? and departname=? and status=?)"
WhereExec = []interface{"EE", "22", 1, "FF", "33", 0}

需要注意的是,这样方式的调用,我们为了简化调用的结构更清晰更简单,每个条件之间默认都是=的关系。如果有其他的关系判断,可以用下面的方式。

  • 单个字符串参数的调用

上面的Where方法的参数,其实是我们和Insert一样,传入的是1个结构体,但是有时候,如果传入1个结构体,得先定义再实例化,也很麻烦。而且有时候,我们仅仅只需要查询1个字段,如果再去定义1个结构体再实例化就太麻烦了。所以,我们ORM还得提供快捷的方法调用,比如:

Where("uid", "=", 1234)
Where("uid", ">=", 1234)
Where("uid", "in", []int{2, 3, 4})

这样,我们也可以用其他非and的判断表达式,比如:!=,like,not in,in等。

OK,那我们开始写一下,这种方式怎么判断呢?对比传入结构体的方式更简单:方法有3个参数,第一个是需要查询的字段,第2个是比较符,第三个是查询的值

func (e *SmallormEngine) Where(fieldName string, opt string, fieldValue interface{}) *SmallormEngine {//区分是操作符in的情况data2 := strings.Trim(strings.ToLower(fieldName.(string)), " ")if data2 == "in" || data2 == "not in" {//判断传入的是切片reType := reflect.TypeOf(fieldValue).Kind()if reType != reflect.Slice && reType != reflect.Array {panic("in/not in 操作传入的数据必须是切片或者数组")}//反射值v := reflect.ValueOf(fieldValue)//数组/切片长度dataNum := v.Len()//占位符ps := make([]string, dataNum)for i := 0; i < dataNum; i++ {ps[i] = "?"e.WhereExec = append(e.WhereExec, v.Index(i).Interface())}//拼接e.WhereParam += fieldName.(string) + " " + fieldValue + " (" + strings.Join(ps, ",") + ")) "} else {e.WhereParam += fieldName.(string) + " " + fieldValue.(string) + " ?) "e.WhereExec = append(e.WhereExec, fieldValue)}return e
}

上面代码唯一需要注意的就是第二参数如果是in操作符的话,后面第三个参数要是切片类型,就得反射出来,用 in (?,?,?)这样的方式。

所以,我们把这2种方式,拼接一下,融合成1种方式,智能的去判断即可,下面是完整的代码:

//传入and条件
func (e *SmallormEngine) Where(data ...interface{}) *SmallormEngine {//判断是结构体还是多个字符串var dataType intif len(data) == 1 {dataType = 1} else if len(data) == 2 {dataType = 2} else if len(data) == 3 {dataType = 3} else {panic("参数个数错误")}//多次调用判断if e.WhereParam != "" {e.WhereParam += " and ("} else {e.WhereParam += "("}//如果是结构体if dataType == 1 {t := reflect.TypeOf(data[0])v := reflect.ValueOf(data[0])//字段名var fieldNameArray []string//循环解析for i := 0; i < t.NumField(); i++ {//首字母小写,不可反射if !v.Field(i).CanInterface() {continue}//解析tag,找出真实的sql字段名sqlTag := t.Field(i).Tag.Get("sql")if sqlTag != "" {fieldNameArray = append(fieldNameArray, strings.Split(sqlTag, ",")[0]+"=?")} else {fieldNameArray = append(fieldNameArray, t.Field(i).Name+"=?")}e.WhereExec = append(e.WhereExec, v.Field(i).Interface())}//拼接e.WhereParam += strings.Join(fieldNameArray, " and ") + ") "} else if dataType == 2 {//直接=的情况e.WhereParam += data[0].(string) + "=?) "e.WhereExec = append(e.WhereExec, data[1])} else if dataType == 3 {//3个参数的情况//区分是操作符in的情况data2 := strings.Trim(strings.ToLower(data[1].(string)), " ")if data2 == "in" || data2 == "not in" {//判断传入的是切片reType := reflect.TypeOf(data[2]).Kind()if reType != reflect.Slice && reType != reflect.Array {panic("in/not in 操作传入的数据必须是切片或者数组")}//反射值v := reflect.ValueOf(data[2])//数组/切片长度dataNum := v.Len()//占位符ps := make([]string, dataNum)for i := 0; i < dataNum; i++ {ps[i] = "?"e.WhereExec = append(e.WhereExec, v.Index(i).Interface())}//拼接e.WhereParam += data[0].(string) + " " + data2 + " (" + strings.Join(ps, ",") + ")) "} else {e.WhereParam += data[0].(string) + " " + data[1].(string) + " ?) "e.WhereExec = append(e.WhereExec, data[2])}}return e
}

上面的写法,参数改成1个了,但是中用到了..interface{}这个写法,它表示传入的参数是一个可变参数类型,可以是1个,2个或者3个的情况。用这种方式,方法里获取到的就是1个切片类型了。我们得用len()函数,来判断到底是切片里面有几个元素,然后依次对应上我们的分支逻辑。值得注意的是,当我们传入的是结构体的时候,也是需要用data[0]的方式来获取。

这样,我们就可以用Where方法来快捷的愉快的调用了:

// where uid = 123
e.Table("userinfo").Where("uid", 123) // where uid not in (2,3,4)
e.Table("userinfo").Where("uid", "not in", []int{2, 3, 4})// where uid in (2,3,4)
e.Table("userinfo").Where("uid", "in", []int{2, 3, 4})// where uid like '%2%'
e.Table("userinfo").Where("uid", "like", "%2%")// where uid >= 123
e.Table("userinfo").Where("uid", ">=", 123)// where (uid >= 123) and (name = 'vv')
e.Table("userinfo").Where("uid", ">=", 123).Where("name", "vv")

(五)条件OrWhere

上面的Where方法生成的数据块之间都是and的关系,其实我们有一些sql是需要or的关系的,比如:

where (uid >= 123) or (name = 'vv')
where (uid = 123 and name = 'vv') or (uid = 456 and name = 'bb')

那么这种情况,其实也是需要考虑进去的,写起来也很简单,只需要新加一个OrWhereParam参数,替换上面Where方法里面的whereParam即可,WhereExec不需要变化。然后把拼接关系改成or,其他代码一摸一样:

func (e *SmallormEngine) OrWhere(data ...interface{}) *SmallormEngine {...//判断使用顺序if e.WhereParam == "" {panic("WhereOr必须在Where后面调用")}//WhereOr条件e.OrWhereParam += " or ("...return e
}

需要注意的是,OrWhere方法是必须得先调用Where后再调用的。因为一般用到了or,前面肯定也有前置的where判断的

也是一样,有三种调用方式:

OrWhere("uid", 1234) //默认是等于
OrWhere("uid", ">=", 1234)
OrWhere(uidStruct) //传入1个结构体,结构体之间用and连接

看下使用效果:

// where (uid = 123) or (name = "vv")
e.Table("userinfo").Where("uid", 123).OrWhere("name", "vv")// where (uid not in (2,3,4)) or (uid not in (5,6,7))
e.Table("userinfo").Where("uid", "not in", []int{2, 3, 4}).OrWhere("uid", "not in", []int{5, 6, 7})// where (uid like '%2') or (uid like '%5%')
e.Table("userinfo").Where("uid", "like", "%2").OrWhere("uid", "like", "%5%")// where (uid >= 123) or (uid <= 454)
e.Table("userinfo").Where("uid", ">=", 123).OrWhere("uid", "<=", 454)// where (username = "EE" and departname = "22" and status = 1) or (name = 'vv') or (status = 1)type User struct {Username   string `sql:"username"`Departname string `sql:"departname"`Status     int64  `sql:"status"`
}user2 := User{Username:   "EE",Departname: "22", Status:     1,
}e.Table("userinfo").Where(user2).OrWhere("name", "vv").OrWhere("status", 1)

为了使这个方法更简单的被使用,不搞复杂,这种方式的or关系,实质上是针对于多次调用where之间的,是不支持同一个where里面的数据是or关系的。那如果需要的话,可以这样调用:

// where (username = "EE") or (departname = "22") or (status = 1)e.Table("userinfo").Where(username, "EE").OrWhere("departname", "22").OrWhere("status", 1)

(六)删除Delete

删除也是sql逻辑中的最常见的操作了,当我们完成了前面Where和OrWhere的数据逻辑绑定后,其实写Delete方法是最简单的了,为什么呢?因为Delete方法是CURD的最后一步,是直接和数据库进行操交互的了,是不需要我们再去反射各种数据进行绑定了。我们仅仅需要把Where里面绑定的2个值,往Prepare和 Exec里面套即可。

我们看下具体是怎么写:

//删除
func (e *SmallormEngine) Delete() (int64, error) {//拼接delete sqle.Prepare = "delete from " + e.GetTable()//如果where不为空if e.WhereParam != "" || e.OrWhereParam != "" {e.Prepare += " where " + e.WhereParam + e.OrWhereParam}//limit不为空if e.LimitParam != "" {e.Prepare += "limit " + e.LimitParam}//第一步:Preparevar stmt *sql.Stmtvar err errorstmt, err = e.Db.Prepare(e.Prepare)if err != nil {return 0, err}e.AllExec = e.WhereExec//第二步:执行exec,注意这是stmt.Execresult, err := stmt.Exec(e.AllExec...)if err != nil {return 0, e.setErrorInfo(err)}//影响的行数rowsAffected, err := result.RowsAffected()if err != nil {return 0, e.setErrorInfo(err)}return rowsAffected, nil
}

是不是很熟悉?和Insert方法的逻辑几乎是一样的,只是e.Prepare中的sql语句不一样。

这样看下调用方式和结果:

// delete from userinfo where (uid >= 123) or (uid <= 454)
rowsAffected, err := e.Table("userinfo").Where("uid", ">=", 123).OrWhere("uid", "<=", 454).Delete()

(七)修改Update

修改数据,也是CURD的最后一步,但是它和Delete不同的是,他是有2个数据需要绑定的,1个通过Where方法绑定的where数据,还有1个,就是需要去更新的数据,这个我们还没做。

update userinfo set status = 1 where (uid >= 123) or (uid <= 454)

其中status=1这部分的数据,我们也是需要提炼出来搞成1个对外暴露的方法。所以,最终的调用方式会是这样的:

e.Table("userinfo").Where("uid", 123).Update("status", 1)e.Table("userinfo").Where("uid", 123).Update(user2)

和Where的可变参数类似,我们也是提供了2种参数传递方式,既可以传入一个结构体变量,也可以只传入单个更新的变量,用起来会更方便更灵活。

仔细一看,Update中获取数据的方式,和Insert方法插入单个数据的方式不能说特别像吧,可以说简直一模一样啊。

直接上代码吧:

//更新
func (e *SmallormEngine) Update(data ...interface{}) (int64, error) {//判断是结构体还是多个字符串var dataType intif len(data) == 1 {dataType = 1} else if len(data) == 2 {dataType = 2} else {return 0, errors.New("参数个数错误")}//如果是结构体if dataType == 1 {t := reflect.TypeOf(data[0])v := reflect.ValueOf(data[0])var fieldNameArray []stringfor i := 0; i < t.NumField(); i++ {//首字母小写,不可反射if !v.Field(i).CanInterface() {continue}//解析tag,找出真实的sql字段名sqlTag := t.Field(i).Tag.Get("sql")if sqlTag != "" {fieldNameArray = append(fieldNameArray, strings.Split(sqlTag, ",")[0]+"=?")} else {fieldNameArray = append(fieldNameArray, t.Field(i).Name+"=?")}e.UpdateExec = append(e.UpdateExec, v.Field(i).Interface())}e.UpdateParam += strings.Join(fieldNameArray, ",")} else if dataType == 2 {//直接=的情况e.UpdateParam += data[0].(string) + "=?"e.UpdateExec = append(e.UpdateExec, data[1])}//拼接sqle.Prepare = "update " + e.GetTable() + " set " + e.UpdateParam//如果where不为空if e.WhereParam != "" || e.OrWhereParam != "" {e.Prepare += " where " + e.WhereParam + e.OrWhereParam}//limit不为空if e.LimitParam != "" {e.Prepare += "limit " + e.LimitParam}//preparevar stmt *sql.Stmtvar err errorstmt, err = e.Db.Prepare(e.Prepare)if err != nil {return 0, e.setErrorInfo(err)}//合并UpdateExec和WhereExecif e.WhereExec != nil {e.AllExec = append(e.UpdateExec, e.WhereExec...)}//执行exec,注意这是stmt.Execresult, err := stmt.Exec(e.AllExec...)if err != nil {return 0, e.setErrorInfo(err)}//影响的行数id, _ := result.RowsAffected()return id, nil
}

其中有一个地方,需要注意的是:合并UpdateExec和WhereExec这一步。需要在e.WhereExec后面加...,这样的目的就是把切片全部展开成1个1个的可变参数,追加到UpdateExec切片的后面。如果不加是会报语法报错的。

cannot use []interface{} literal (type []interface{}) as type interface{} in append

golang里面,貌似没有一个函数可以把2个切片直接合并的方法,类似于PHP中的array_merge,也可能是我还没找到。

$a1=array("red","green");
$a2=array("blue","yellow");
print_r(array_merge($a1,$a2));   // Array ( [0] => red [1] => green [2] => blue [3] => yellow )

 作者简介

杨义

腾讯高级工程师

腾讯高级工程师,主要负责IEG游戏活动运营及高可用平台的建设,对云服务、k8s以及高性能服务上也有很深的了解。

 推荐阅读

深入解析Apache Pulsar系列(一):客户端消息确认

OpenTelemetry项目解读

浅谈Golang两种线程安全的map

一探究竟!Whistle拦截HTTPS是如何实现的?

手把手带你从0搭建一个Golang ORM框架(上)!相关推荐

  1. 手把手带你用next搭建一个完善的react服务端渲染项目(集成antd、redux、样式解决方案)

    前言 本文参考了慕课网jokcy老师的React16.8+Next.js+Koa2开发Github全栈项目,也算是做个笔记吧. 源码地址 github.com/sl1673495/n- 介绍 Next ...

  2. 自己动手写一个Golang ORM框架

    作者:smallyang,腾讯 IEG 运营开发工程师 当我深入的学习和了解了 GORM,XORM 后,我还是觉得它们不够简洁和优雅,有些笨重,有很大的学习成本.本着学习和探索的目的,于是我自己实现了 ...

  3. 手把手带你从0完成医疗行业影像图像检测三大经典模型InceptionV3-RestNet50-VGG16(附python源代码及数据库)——改变世界经典人工智能项目实战(一)手把手教学迁移学习

    手把手带你从0完成医疗行业影像图像检测三大经典模型InceptionV3-RestNet50-VGG16 1.迁移学习简介 2.项目简介 3.糖尿病视网膜病变数据集 4.考虑类别不平衡问题 5.定义模 ...

  4. 从0搭建一个Springboot+vue前后端分离项目(一)安装工具,创建项目

    从0搭建一个Springboot+vue前后端分离项目(二)使用idea进行页面搭建+页面搭建 参考学习vue官网文档 https://v3.cn.vuejs.org/guide/installati ...

  5. 从 0 搭建一个工业级推荐系统

    推荐系统从来没像现在这样,影响着我们的生活.当你上网购物时,天猫.京东会为你推荐商品:想了解资讯,头条.知乎会为你准备感兴趣的新闻和知识:想消遣放松,抖音.快手会为你奉上让你欲罢不能的短视频. 而驱动 ...

  6. 手把手带你使用uni-admin搭建后台管理系统

    我们一般写应用都需要有后台管理系统,那么uni-app也不例外. 本次内容假设我们已经完成了一个uni-app+uniCloud开发的程序. 默认我们已经搭建好了服务空间. 我们的视频教程(免费)链接 ...

  7. 【React进阶-1】从0搭建一个完整的React项目(入门篇)

    这篇文章带领大家从零开始手动撸一个React项目的基础框架,集成React全家桶.万字长文,请各位有足够的时间时再来阅读和学习. 概述 平时工作中一直在用React提供的脚手架工具搭建React项目, ...

  8. 从头搭建一个深度学习框架

    从头搭建一个深度学习框架 转自:Build a Deep Learning Framework From Scratch 代码:https://github.com/borgwang/tinynn 当 ...

  9. php 框架搭建,利用composer搭建一个PHP微框架(API微项目)

    为什么搭建一个框架(搭建一个怎样的框架) 通过搭建一个框架更好的学习PHP 搭建一个专门用于构建API的微型框架. 微型框架基本上是一个封装的路由,用来转发HTTP请求至一个闭包,控制器,或方法等等, ...

  10. 用go来搭建一个简单的图片上传网站

    提前说明一下:代码参考了<Go语言编程>,稍有变动, 自己亲自玩了一遍. 之前玩过go web server, 现在来用go来搭建一个简单的图片上传网站, 工作目录是:~/photoweb ...

最新文章

  1. 学习RPG Maker MZ开发创建并发布PC和移动端游戏
  2. request和response的setCharacterEncoding()方法
  3. kali 设置中文字体
  4. async和await结合读取文件
  5. 怎样把MySQL的编码方式改为utf8?
  6. 通过JQUERY获取SELECT OPTION中选中的值
  7. 基于jquery类库的绘制二维码的插件jquery.qrcode.js
  8. vuex--mutation,action个人理解
  9. 【MySQL】MySQL 查询优化器的提示(hint)
  10. tf卡测试软件_真正的白菜价?1G不到1元,铠侠(原东芝存储)microSD卡评测
  11. oracle表空间总结,Oracle操作用户和表空间的总结
  12. poj 1068 Parencodings
  13. 有量子计算机的山西高能小说,五本大神级高能热血小说,没看过也必定听说过 ,加入书架告别书荒!...
  14. 【源码】HashMap源码及线程非安全分析
  15. 数字头盔摄像头是一个智能选项
  16. WinHttp用法(WinHttp.WinHttpRequest.5.1方法,属性)
  17. python条形堆积图_python – 带有中心标签的堆积条形图
  18. OKHttp源码详解_tony_851122
  19. 【开工】知道创宇网络安全线上服务指南
  20. 常见英文缩写小节-江晚正愁余-iteye技术网站

热门文章

  1. OpenCV Python 图像矩阵的均值和标准差
  2. 公务员面试综合分析真题解析3
  3. 手机桌面上的计算机怎么删除,怎样删除桌面图标?删除桌面图标方法教学
  4. c语言最小公倍数最简单求法,c语言最小公倍数与最大公约数的求法集锦
  5. c语言求最小公倍数——三种方法
  6. 恶意程序检测之malconv模型
  7. 数据结构导论 笔记整理
  8. 9.1 Python 绝对路径与相对路径
  9. 如何修改织梦后台登陆界面
  10. 基于Hyperworks和LSDYNA的挤压仿真