从零实现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 的代码实现比较复杂,主要分为以下几步:

  1. destSlice.Type().Elem() 获取切片的单个元素的类型 destType,使用 reflect.New() 方法创建一个 destType 的实例,作为 Model() 的入参,映射出表结构 RefTable()。
  2. 根据表结构,使用 clause 构造出 SELECT 语句,查询到所有符合条件的记录 rows。
  3. 遍历每一行记录,利用反射创建 destType 的实例 dest,将 dest 的所有字段平铺开,构造切片 values。
  4. 调用 rows.Scan() 将该行记录每一列的值依次赋值给 values 中的每一个字段。
  5. 将 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

ORM框架学习

ORM轻量级框架---ActiveAndroid

1+X web中级 Laravel学习笔记——Eloquent ORM查询更新删除新增

初学 go 入门-案例-教程-记录(13)orm 框架 Gorm 简单案例 - 连接sqlserver,并查询数据

C# 数据操作系列 - 4. 自己实现一个ORM