从零实现ORM框架GeoORM-记录新增和查询-03
Posted 大忽悠爱忽悠
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从零实现ORM框架GeoORM-记录新增和查询-03相关的知识,希望对你有一定的参考价值。
从零实现ORM框架GeoORM-记录新增和查询-03
本系列参考: 7天用Go从零实现ORM框架GeeORM
本系列源码: https://gitee.com/DaHuYuXiXi/geo-orm
Clause 构造 SQL 语句
从本节开始,GeoORM 需要涉及一些较为复杂的操作,例如查询操作。查询语句一般由很多个子句(clause) 构成。SELECT 语句的构成通常是这样的:
SELECT col1, col2, ...
FROM table_name
WHERE [ conditions ]
GROUP BY col1
HAVING [ conditions ]
也就是说,如果想一次构造出完整的 SQL 语句是比较困难的,因此我们将构造 SQL 语句这一部分独立出来,放在子package clause 中实现。
首先在 clause/generator.go 中实现各个子句的生成规则。
- clause/generator.go
//clause 该类的重点在于为相关子句提供构建方法
package clause
import (
"fmt"
"strings"
)
//generator 传入参数,构建出相关的sql语句后返回,如果有参数,会额外返回一个参数数组
type generator func(values ...interface) (string, []interface)
//generators 存放生成各个子句的函数映射表
var generators map[Type]generator
func init()
generators = make(map[Type]generator)
generators[INSERT] = _insert
generators[VALUES] = _values
generators[SELECT] = _select
generators[LIMIT] = _limit
generators[WHERE] = _where
generators[ORDERBY] = _orderBy
//genBindVars 给构建values子句提供支持
//传入的num表示存在多少个占位符
//最终构建出来的形式如下: ?,?,?
func genBindVars(num int) string
var vars []string
for i := 0; i < num; i++
vars = append(vars, "?")
return strings.Join(vars, ", ")
//_insert 构造insert子句
//例如: insert into user (name,age)
func _insert(values ...interface) (string, []interface)
tableName := values[0]
fields := strings.Join(values[1].([]string), ",")
return fmt.Sprintf("INSERT INTO %s (%v)", tableName, fields), []interface
//_values 构造values子句
//例如: values ("dhy",18),("xpy",20)....
func _values(values ...interface) (string, []interface)
// VALUES ($v1), ($v2), ...
var bindStr string
var sql strings.Builder
var vars []interface
sql.WriteString("VALUES ")
//每一个value就是一个切片数组==> [dhy,18],[xpy,20]
for i, value := range values
//[dhy,18]
v := value.([]interface)
//bindStr= ?,?
if bindStr == ""
bindStr = genBindVars(len(v))
//Values (?,?) ....
sql.WriteString(fmt.Sprintf("(%v)", bindStr))
//说明还有(?,?)需要拼接,因此这里在当前已经拼接好的values子句后面加上一个,
//Values (?,?),
if i+1 != len(values)
sql.WriteString(", ")
//vars=[dhy,18]
vars = append(vars, v...)
//返回拼接好的values子句和对应的实际参数值
//values (?,?),(?,?) , [dhy,18,xpy,20]
return sql.String(), vars
//_select 构造select子句
//例如: select name,age from stu
func _select(values ...interface) (string, []interface)
// SELECT $fields FROM $tableName
tableName := values[0]
fields := strings.Join(values[1].([]string), ",")
return fmt.Sprintf("SELECT %v FROM %s", fields, tableName), []interface
//_limit 构造limit子句
func _limit(values ...interface) (string, []interface)
// LIMIT $num
return "LIMIT ?", values
//_where 构造where子句
func _where(values ...interface) (string, []interface)
// WHERE $desc
desc, vars := values[0], values[1:]
return fmt.Sprintf("WHERE %s = ?", desc), vars
//_orderBy 构造orderBy子句
func _orderBy(values ...interface) (string, []interface)
return fmt.Sprintf("ORDER BY %s", values[0]), []interface
然后在 clause/clause.go 中实现结构体 Clause 拼接各个独立的子句。
- clause/clause.go
package clause
import "strings"
//Clause 保存构造一条sql需要的相关子句集合信息
//例如:
//select子句: select * from stu 参数:[]
//where子句: where name =? 参数:[dhy]
//...
type Clause struct
sql map[Type]string
sqlVars map[Type][]interface
//Type 当前sql子句的类型
type Type int
//目前只支持如下几种sql子句类型拼接
const (
INSERT Type = iota
VALUES
SELECT
LIMIT
WHERE
ORDERBY
)
//Set 设置一条子句信息到当前Clause内部
func (c *Clause) Set(name Type, vars ...interface)
if c.sql == nil
c.sql = make(map[Type]string)
c.sqlVars = make(map[Type][]interface)
else if c.sql[name] != ""
//避免多次进行构建造成额外开销
return;
sql, vars := generators[name](vars...)
c.sql[name] = sql
c.sqlVars[name] = vars
//Build 通过Clause内部的子句集合信息,和传入构建子句的顺序,最终构建出完整的sql子句和所需要的实际参数列表
func (c *Clause) Build(orders ...Type) (string, []interface)
var sqls []string
var vars []interface
for _, order := range orders
if sql, ok := c.sql[order]; ok
sqls = append(sqls, sql)
vars = append(vars, c.sqlVars[order]...)
return strings.Join(sqls, " "), vars
- Set 方法根据 Type 调用对应的 generator,生成该子句对应的 SQL 语句。
- Build 方法根据传入的 Type 的顺序,构造出最终的 SQL 语句。
在 clause_test.go 实现对应的测试用例:
package clause
import (
"reflect"
"testing"
)
func testSelect(t *testing.T)
var clause Clause
clause.Set(LIMIT, 3)
clause.Set(SELECT, "User", []string"*")
clause.Set(WHERE, "Name", "Tom")
clause.Set(ORDERBY, "Age ASC")
sql, vars := clause.Build(SELECT, WHERE, ORDERBY, LIMIT)
t.Log(sql, vars)
if sql != "SELECT * FROM User WHERE Name = ? ORDER BY Age ASC LIMIT ?"
t.Fatal("failed to build SQL")
//通过反射比较两个切片中是否相同
if !reflect.DeepEqual(vars, []interface"Tom", 3)
t.Fatal("failed to build SQLVars")
func TestClause_Build(t *testing.T)
t.Run("select", func(t *testing.T)
testSelect(t)
)
实现 Insert 功能
首先为 Session 添加成员变量 clause
type Session struct
db *sql.DB
dialect dialect.Dialect
refTable *schema.Schema
clause clause.Clause
sql strings.Builder
sqlVars []interface
func (s *Session) Clear()
s.sql.Reset()
s.sqlVars = nil
s.clause = clause.Clause
clause 已经支持生成简单的插入(INSERT) 和 查询(SELECT) 的 SQL 语句,那么紧接着我们就可以在 session 中实现对应的功能了。
INSERT 对应的 SQL 语句一般是这样的:
INSERT INTO table_name(col1, col2, col3, ...) VALUES
(A1, A2, A3, ...),
(B1, B2, B3, ...),
...
在 ORM 框架中期望 Insert 的调用方式如下:
s := geoOrm.NewEngine("sqlite3", "gee.db").NewSession()
u1 := &UserName: "Tom", Age: 18
u2 := &UserName: "Sam", Age: 25
s.Insert(u1, u2, ...)
也就是说,我们还需要一个步骤,根据数据库中列的顺序,从对象中找到对应的值,按顺序平铺。即 u1、u2 转换为 (“Tom”, 18), (“Same”, 25) 这样的格式。
因此在实现 Insert 功能之前,还需要给 Schema 新增一个函数 RecordValues 完成上述的转换。
- schema/schema.go
//RecordValues 解析实体对象中的值,变为数据库对应列的值
// Stuname: dhy ,age: 18 转换为 insert into stu(name,age) values("dhy",18)中values中实参值
func (schema *Schema) RecordValues(dest interface) []interface
destValue := reflect.Indirect(reflect.ValueOf(dest))
var fieldValues []interface
for _, field := range schema.Fields
fieldValues = append(fieldValues,
//通过反射去stu对象中获取到name字段对应的value,然后通过Interface()拿到该字段实际的值dhy
destValue.FieldByName(field.Name).Interface())
//返回的就是[dhy,18]实际参数数组
return fieldValues
在 session 文件夹下新建 record.go,用于实现记录增删查改相关的代码。
- session/record.go
package session
import (
"GeoORM/clause"
)
//Insert 插入实体对象到表中,需要做实体对象属性到表列值的转换
func (s *Session) Insert(values ...interface) (int64, error)
recordValues := make([]interface, 0)
//遍历每一个传入的对象
for _, value := range values
//拿到当前实体对应的tableName
table := s.Model(value).RefTable()
//进行Insert子句的构建,多次调用不会产生影响,因此Set内部做了判断
s.clause.Set(clause.INSERT, table.Name, table.FieldNames)
//拼接实参值
recordValues = append(recordValues, table.RecordValues(value))
//构建Values子句
s.clause.Set(clause.VALUES, recordValues...)
//构造完整的sql语句和对应的参数列表
sql, vars := s.clause.Build(clause.INSERT, clause.VALUES)
//执行sql语句
result, err := s.Raw(sql, vars...).Exec()
if err != nil
return 0, err
return result.RowsAffected()
后续所有构造 SQL 语句的方式都将与 Insert 中构造 SQL 语句的方式一致。分两步:
- 多次调用 clause.Set() 构造好每一个子句。
- 调用一次 clause.Build() 按照传入的顺序构造出最终的 SQL 语句。
- 构造完成后,调用 Raw().Exec() 方法执行。
实现 Find 功能
期望的调用方式是这样的:传入一个切片指针,查询的结果保存在切片中。
s := geoOrm.NewEngine("sqlite3", "gee.db").NewSession()
var users []User
s.Find(&users);
Find 功能的难点和 Insert 恰好反了过来。Insert 需要将已经存在的对象的每一个字段的值平铺开来,而 Find 则是需要根据平铺开的字段的值构造出对象。同样,也需要用到反射(reflect)。
//Find 传入实体对象的切片数组,然后查表将表记录转换为实体对象列表
func (s *Session) Find(values interface) error
//拿到指向values本身的value,而不是指向values指针的value
destSlice := reflect.Indirect(reflect.ValueOf(values))
//因为destSlice类型为切片,所有这里elem返回的是切片元素类型
destType := destSlice.Type().Elem()
//创建一个新的实体对象,对象属性由零值填充
//解析该实体对象后,返回对应的Schema类型
table := s.Model(reflect.New(destType).Elem().Interface()).RefTable()
//构建select子句
s.clause.Set(clause.SELECT, table.Name, table.FieldNames)
//构建完整的sql语句
sql, vars := s.clause.Build(clause.SELECT, clause.WHERE, clause.ORDERBY, clause.LIMIT)
//执行sql语句
rows, err := s.Raw(sql, vars...).QueryRows()
if err != nil
return err
//挨个记录遍历
for rows.Next()
//创建一个新对象实例
dest := reflect.New(destType).Elem()
//values数组存放所需要的字段
var values []interface
//遍历表字段
for _, name := range table.FieldNames
//通过字段名去实体中寻找对应的字段
//Addr是为了拿到变量的地址,而不是变量本身
values = append(values, dest.FieldByName(name).Addr().Interface())
//scan传入的是变量的地址,变量顺序和数量需要与数据库列顺序和数量一致
if err := rows.Scan(values...); err != nil
return err
//反射赋值
destSlice.Set(reflect.Append(destSlice, dest))
return rows.Close()
Find 的代码实现比较复杂,主要分为以下几步:
- destSlice.Type().Elem() 获取切片的单个元素的类型 destType,使用 reflect.New() 方法创建一个 destType 的实例,作为 Model() 的入参,映射出表结构 RefTable()。
- 根据表结构,使用 clause 构造出 SELECT 语句,查询到所有符合条件的记录 rows。
- 遍历每一行记录,利用反射创建 destType 的实例 dest,将 dest 的所有字段平铺开,构造切片 values。
- 调用 rows.Scan() 将该行记录每一列的值依次赋值给 values 中的每一个字段。
- 将 dest 添加到切片 destSlice 中。循环直到所有的记录都添加到切片 destSlice 中。
测试
在 session 文件夹下新建 record_test.go,创建测试用例。
- session/record_test.go
package cmd
import (
"GeoORM"
myLog "GeoORM/mylog"
"testing"
)
type Stu struct
Name string `geoOrm:"PRIMARY KEY"`
Age int
func TestSession_CreateTable(t *testing.T)
engine, _ := GeoORM.NewEngine("sqlite3", "geo.db")
defer engine.Close()
s := engine.NewSession()
s.Model(&Stu)
_ = s.DropTable()
_ = s.CreateTable()
if !s.HasTable()
t.Fatal("Failed to create table Stu")
func TestSession_InsertIntoTable(t *testing.T)
engine, _ := GeoORM.NewEngine("sqlite3以上是关于从零实现ORM框架GeoORM-记录新增和查询-03的主要内容,如果未能解决你的问题,请参考以下文章
从零实现ORM框架GeoORM-database/sql基础-01
1+X web中级 Laravel学习笔记——Eloquent ORM查询更新删除新增