Go

Go语言封装一个便捷的ORM

Bingo框架ORM的最早版本

Posted by silsuer on April 18, 2018

使用Go封装一个便捷的ORM

首发于Go语言中文网

最近在用Go写一个Web开发框架,看了一些ORM,大部分都需要自己拼接SQL,对我这种用惯了 Laravel的人来说,确实有点别扭,所以想自己写一个ORM,可以方便的对数据库进行连贯操作

由于代码太多,不贴了,只讲思路,具体代码在这里silsuer/bingo

思路

  1. 确定最后要做出的效果

    我想要做成类似Laravel那种,操作数据库大概是这样DB::table(dbName)->Where('id',1)->get()

  2. 连贯操作原理

    做出这种连贯操作的效果,除了结尾的方法,中间连续调用的那些方法都必须返回一个相同的对象

  3. 定义数据库对象

    既然如此,那么先定义一个Mysql的结构体

    
     // mysql结构体,用来存储sql语句并执行
     type Mysql struct {
         connection  *sql.DB    // 数据库连接对象
         sql         string     // 最终要执行的语句
         whereSql    string     // where语句
         limitSql    string     // limit语句
         columnSql   string     // select 中的列语句
         tableSql    string     // 表名语句
         orderBySql  string     // order by 语句
         groupBySql  string     // group by语句
         havingSql   string     // having语句
         tableName   string     // 表名
         TableSchema string     // 所在数据库,在缓存表结构的时候有用
         Errors      []error    // 保存在连贯操作中可能发生的错误
         Result      sql.Result // 保存执行语句后的结果
         Rows        *sql.Rows  // 保存执行多行查询语句后的行
         Results     []sql.Result
         Row         *sql.Row // 保存单行查询语句后的行
         //Res         []map[string]interface{}  // 把查询结果生成一个map
     }
    
    

    这样,我们每次连续调用的时候,只需要返回当前结构体的指针就可以了.

  4. 连接数据库

    为了方便,先在公共函数中定义一个DB()函数,这个函数通过创建数据库驱动,来返回一个数据库对象(指针)

         func DB() interface{} {
             // 返回一个驱动的操作类
             // 传入db配置  env的driver,实例化不同的类,单例模式,获取唯一驱动
             if Driver == nil {
                 DriverInit()
             }
             con := Driver.GetConnection()  // 获取数据库连接
             return con
         }
    

    可以看到,为了节省资源,我们的数据库连接和驱动连接都是单例模式,只允许存在一次

    下面是初始化数据库驱动的方法DriverInit()

    
       
     	// 数据库驱动
         type driver struct {
             name     string // 驱动名
             dbConfig string // 配置
         }
    	    
         // 驱动初始化
         func DriverInit() {
             Driver = &driver{}
             Driver.name = strings.ToUpper(Env.Get("DB_DRIVER"))
             switch Driver.name {
             case "MYSQL":
                 // 初始化了驱动之后,开始初始化数据库连接
                 Driver.dbConfig = Env.Get("DB_USERNAME") + ":" + Env.Get("DB_PASSWORD") + "@tcp(" + Env.Get("DB_HOST") + ":" + Env.Get("DB_PORT") + ")" + "/" + Env.Get("DB_NAME") + "?" + "charset=" + Env.Get("DB_CHARSET")
                 break
             default:
                 break
             }
    	    
         }
    

    我们在初始化驱动的时候,会判断配置文件中用的是哪种数据库,然后根据数据库去拼接连接数据库的字符串

    这样为我们的ORM可以支持多个数据库提供了可能

    配置的字符串做好了,接下来就要开始获取数据库的单例连接了

        // 根据数据库驱动,获取数据库连接
        func (d *driver) GetConnection() interface{} {
        	switch d.name {
        	case "MYSQL":
        		m := mysql.Mysql{}                // 实例化结构体
        		m.TableSchema = Env.Get("DB_NAME") // 数据库名
        		m.Init(Driver.dbConfig) // 设置表名和数据库连接
        		return &m                         // 返回实例
        		break
        	default:
        		break
        	}
        	return nil
        }
    

    获取连接时首先创建了一个数据库的对象,然后把一些基本信息赋给了这个对象的对应属性,Init方法

    获取了这个人数据库的唯一连接:

        func (m *Mysql) Init(config string) {
         // 获取单例连接
         m.connection = GetInstanceConnection(config) // 获取数据库连接
        }
    
         var once sync.Once
         // 设置单例的数据库连接
         func GetInstanceConnection(config string) *sql.DB {
             once.Do(func() { // 只做一次
                 db, err := sql.Open("mysql", config)
                 if err != nil {
                     panic(err)
                 }
                 conn = db
             })
             return conn
         }
    

    Once.Dosync包中提供的只允许代码执行一次的方法

    这样我们就获取到了数据库的连接,然后把这个连接放到Mysql对象的 connection中,返回即可

  5. 执行连贯操作

    要注意由于我们使用的数据库不同,DB()返回的是一个interface{},所以需要我们重新转成*mysql.Mysql:

      bingo.DB().(*mysql.Mysql)
    

    接下来,只需要定义各种Mysql结构体的方法,然后返回这个结构体的指针就可以了,

    篇幅问题,只写一个最简单的在test表中查询id>1的数据,代码效果是这样:

        res:= bingo.DB().(*mysql.Mysql).Table("test").Where("id",">",1).Get()
    

    首先,写Table方法,只是给Mysql对象赋个值而已:

         // 初始化一些sql的值
        func (m *Mysql) Table(tableName string) *Mysql {
         m.tableSql = " " + tableName + " "
         m.whereSql = ""
         m.columnSql = " * "
         m.tableName = tableName
         return m
        }
    

    然后写Where方法,这个稍微复杂一点,如果传入两个参数,那么他们之间是等于的关系,如果是三个参数

    那么第二个参数就是他们直接的关系

    
      func (m *Mysql) Where(args ... interface{}) *Mysql {
      	// 要根据传入字段的名字,获得字段类型,然后判断第三个参数是否要加引号
      	// 最多传入三个参数  a=b
      	// 先判断where sql 里有没有 where a=b
      	cif := m.GetTableInfo().Info
         
      	// 先判断这个字段在表中是否存在,如果存在,获取类型获取值等
      	cType := ""
      	if _, ok := cif[convertToString(args[0])]; ok {
      		cType = cif[convertToString(args[0])].Type
      	} else {
      		m.Errors = append(m.Errors, errors.New("cannot find a column named "+convertToString(args[0])+" in "+m.tableName+" table"))
      		return m // 终止这个函数
      	}
      	var ifWhere bool
      	whereArr := strings.Fields(m.whereSql)
      	if len(whereArr) == 0 { // 空的
      		ifWhere = false
      	} else {
      		if strings.ToUpper(whereArr[0]) == "WHERE" {
      			ifWhere = true // 存在where
      		} else {
      			ifWhere = false // 不存在where
      		}
      	}
         
      	switch len(args) {
      	case 2:
      		// 有where 只需要写 a=b
      		if ifWhere {
      			// 如果是字符串类型,加引号
      			if isString(cType) {
      				m.whereSql = m.whereSql + ` AND ` + convertToString(args[0]) + `='` + convertToString(args[1]) + `'`
      			} else {
      				m.whereSql = m.whereSql + ` AND ` + convertToString(args[0]) + `=` + convertToString(args[1])
      			}
      		} else {
      			// 没有where 要写 where a=b
      			if isString(cType) {
      				m.whereSql = ` WHERE ` + convertToString(args[0]) + `='` + convertToString(args[1]) + `'`
      			} else {
      				m.whereSql = ` WHERE ` + convertToString(args[0]) + `=` + convertToString(args[1])
      			}
      		}
      		break
      	case 3:
      		// 有where 只需要写 a=b
      		if ifWhere {
      			// 如果是字符串类型,加引号
      			if isString(cType) {
      				m.whereSql = m.whereSql + ` AND ` + convertToString(args[0]) + convertToString(args[1]) + `'` + convertToString(args[2]) + `'`
      			} else {
      				m.whereSql = m.whereSql + ` AND ` + convertToString(args[0]) + convertToString(args[1]) + convertToString(args[2])
      			}
      		} else {
      			// 没有where 要写 where a=b
      			if isString(cType) {
      				m.whereSql = ` WHERE ` + convertToString(args[0]) + convertToString(args[1]) + `'` + convertToString(args[2]) + `'`
      			} else {
      				m.whereSql = ` WHERE ` + convertToString(args[0]) + convertToString(args[1]) + convertToString(args[2])
      			}
      		}
      		break
      	default:
      		m.Errors = append(m.Errors, errors.New("missing args length in where function"))
      	}
      	return m
      }
    
    

    上面的代码中我用到了GetTableInfo() 方法,这个方法是获取当前表的结构,为了快速的对表执行操作,或者过滤掉表不存在的字段

    需要缓存表的结构,把列名和列类型缓存起来,具体代码不贴了,在这里 缓存表结构

    然后会根据Mysql结构体的whereSql字段,拼接新的whereSql,并重新赋值

    接下来,写Get()方法,执行最后的SQL语句即可

       
     // 查询数据
     func (m *Mysql) Get() *Mysql {
     	m.sql = "select" + m.columnSql + "from " + m.tableSql + m.whereSql + m.limitSql + m.orderBySql
     	rows, err := m.connection.Query(m.sql)
     	m.checkAppendError(err)   
     	m.Rows = rows
     	return m
     }
    

    首先拼接语句,然后执行原生的Query方法,把执行结果放置在结构体中即可

    这样我们就拿到了结构体,可以把它打出来看看

     // 获取test表中id大于1的所有数据
     res:= bingo.DB().(*mysql.Mysql).Table("test").Where("id",">",1).Get()
     // 判断执行是否出错
     if len(res.Errors)!=0{
         	fmt.Fprintln(w,"执行出错!")
     	}
     	 // 执行成功,开始遍历
         for res.Rows.Next() {
             var id,age int
             var name string
             res.Rows.Scan(&id,&name,&age)
             fmt.Fprintln(w,"id:"+strconv.Itoa(id)+" name:"+name+" age:"+strconv.Itoa(age))
         }
    
  6. 小结

    由于篇幅问题,我只写了最简单的条件查询,其他的可以去GitHub上看

    目前实现的有:

     1. 创建数据库
     2. 创建数据表
     3. 插入数据(4种方法:插入一条数据,插入一条数据并过滤多余字段,批量插入数据,批量插入数据并过滤多余字段)
     4. 更新数据(3种方法:更新一条数据,更新一条数据并过滤多余字段,批量更新数据并过滤多余字段)
     5. 查询数据(条件查询、limit、order by等)
     6. 删除数据
     7. 删除数据表(2种方法:delete删除,truncate清空)
     8. 清空数据库中的所有表信息而不删除表
     9. 清空数据库,删除所有表
     10. 删除数据表
    

    将要实现的有:

     1. 关联查询
     2. join查询
     3. 分组查询
     4. 快速分页
     5. 数据库迁移 
    
  7. 具体用法请去看看README,时间问题,写的有点乱,等这个玩意儿开发完了会重新整理一份文档的

    最后一句,求star,求翻牌子~~~ヾ(´▽‘)ノ