使用Go封装一个便捷的ORM

silsuer · 2018-04-18 10:28:36 · 2081 次点击 · 预计阅读时间 10 分钟 · 大约8小时之前 开始浏览    
这是一个创建于 2018-04-18 10:28:36 的文章,其中的信息可能已经有所发展或是发生改变。

使用Go封装一个便捷的ORM

最近在用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()

    ```go

    // 数据库驱动
    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可以支持多个数据库提供了可能

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

   ```go
       // 根据数据库驱动,获取数据库连接
       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中,返回即可

  1. 执行连贯操作

    要注意由于我们使用的数据库不同,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))
         }
    
  2. 小结

    由于篇幅问题,我只写了最简单的条件查询,其他的可以去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. 数据库迁移 
    
  3. 具体用法请去看看README,时间问题,写的有点乱,等这个玩意儿开发完了会重新整理一份文档的

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


有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2081 次点击  ∙  1 赞  
加入收藏 微博
2 回复  |  直到 2018-04-18 11:16:21
tk103331
tk103331 · #1 · 7年之前

已start,接受pr吗

silsuer
silsuer · #2 · 7年之前

@tk103331 欢迎PR

添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传