极客兔兔7天教程(从0到1)-orm学习

day0-orm框架

  • 原文 https://geektutu.com/post/gee.html
对象关系映射
  • 对象关系映射(Object Relational Mapping,简称ORM)是通过使用描述对象和数据库之间映射的元数据,将面向对象语言程序中的对象自动持久化到关系数据库中。
数据库面向对象编程语言
表(Table)类(class/struct)
行(record,row)对象(object)
列(field,column)对象属性(attribute)
  • 表的定义,struct, 需通过 & 取指针模式使用
type User struct {
    Name string
    Age  int
}

orm.CreateTable(&User{}) // &
orm.Save(&User{"Tom", 18}) // &
var users []User
orm.Find(&users) // &
  • 通过反射获取 struct 名称,成员变量,方法等信息
type Account struct {

}

func main (){
    typ := reflect.Indirect(reflect.ValueOf(&Account{})).Type()
    fmt.Println(typ.Name())
    for i:=0;i<typ.NumField();i++{
        field := typ.Field(i)
        fmt.Println(field.Name)
    }
}
  • 反射方法的使用说明
    • reflect.ValueOf() 获取指针对应的反射值
    • reflect.Indirect() 获取指针指向的对象的反射值
    • (reflect.Type).Name() 返回类名, 等价于 php className::class
    • (reflect.Type).Field(i) 获取第i个成员变量

day01 database 基础

初始化项目
  • 项目初始化
go mod init geektutu-orm
  • 安装 sqlite3
apt-get install sqlite3
  • 相关操作
    • sqlite3 gee.db 初始数据库
    • CREATE TABLE User(Name text, Age integer); 建表
    • INSERT INTO User(Name, Age) VALUES (“Tom”, 18), (“Jack”, 25); 插入数据
  • sqlite3 操作需要引入额外的包
go get github.com/mattn/go-sqlite3

import (
    "database/sql"
    _ "github.com/mattn/go-sqlite3"
)

  • windows wsl 编译运行错误,设置 goos 为linux 改回
 go env -w GOOS='linux'
  • database/sql, sqlite3 标准库的使用
    • 使用 sql.Open() 连接数据库,第一个参数是驱动名称,import 语句 _ “github.com/mattn/go-sqlite3” 包导入时会注册 sqlite3 的驱动,第二个参数是数据库的名称,对于 SQLite 来说,也就是文件名,不存在会新建。返回一个 sql.DB 实例的指针。
    • Exec() 用于执行 SQL 语句,如果是查询语句,不会返回相关的记录。所以查询语句通常使用 Query() 和 QueryRow(),前者可以返回多条记录,后者只返回一条记录。
    • Exec()、Query()、QueryRow() 接受1或多个入参,第一个入参是 SQL 语句,后面的入参是 SQL 语句中的占位符 ? 对应的值,占位符一般用来防 SQL 注入。
    • QueryRow() 的返回值类型是 *sql.Row,row.Scan() 接受1或多个指针作为参数,可以获取对应列(column)的值,在这个示例中,只有 Name 一列,因此传入字符串指针 &name 即可获取到查询的结果。
db, err := sql.Open("sqlite3", "gee.db")
defer func() { _ = db.Close() }()
result, err := db.Exec("Insert into User(`name`) Values(?),(?)", "Tom", "Sam")
if err != nil {
    log.Println("insert err: ",err)
}
if err == nil {
    affected, _ := result.RowsAffected()
    log.Println("affected: ",affected)
}
row := db.QueryRow("SELECT name from User Limit 1")
var name string
// 赋值
if err := row.Scan(&name); err == nil {
    log.Println("name = ",name)
}else {
    log.Println("scan err:", err)
}
日志类的基础封装
  • 实例化日志类到&设置输出到标准输出
// 实例化日志类
// info 为蓝色,err 为红色
errorLog = log.New(os.Stdout, "\033[31m[error]\033[0m", log.LstdFlags|log.Lshortfile)

//  设置输出 到标准输出
loggers.SetOutput(os.Stdout)
// 日志等级过低的情况下,设置输出 到 ioutil.Discard,即不打印该日志
errorLog.SetOutput(ioutil.Discard) // ioutil.Discard 已启用,直接 nil
errorLog.SetOutput(nil) //,直接 nil
  • fmt.Println 和 log.Println 的区别
    • fmt.Println : 用于打印格式化输出, 输出内容直接到标准输出(通常是终端或控制台)。 不会自动添加时间戳或日志级别等信息。 适用于一般的输出,不带任何日志记录功能。
    • log.Println : 用于打印日志信息,通常用于记录程序执行的日志。输出内容到标准错误(stderr)而非标准输出(stdout),这使得它适合用于日志记录。 自动在输出前加上时间戳,例如:2024/12/03 15:04:05 Hello, World!。 适用于调试和错误处理,可以在生产环境中用于记录日志。
  • 互斥锁可以直接申明,并使用,记得释放互斥锁
    var mu  sync.Mutex
    func xxx(){
		um.Lock()
		defer um.Unlock()
    }
  • log.Println 打印任何结构输出
  • strings.Builder 直接转字符 声明后直接使用, 转字符s.String()
    var s strings.Builder
    // 直接使用, 写入字符
    s.sql.WriteString(" ")
    // 输出字符串
    s.String()	
day-1 会话封装,Exec(), QueryRow(), QueryRows()
  • 逻辑分析
    1. 定义Session 结构,指定成员变量, *sql.DB, sql , sqlVars
        type Session struct {
      	db      *sql.DB
      	sql     strings.Builder
      	sqlVars []interface{}
      }
    
    1. Session 结构体目前只包含三个成员变量,第一个是 db *sql.DB,即使用 sql.Open() 方法连接数据库成功之后返回的指针。
      第二个和第三个成员变量用来拼接 SQL 语句和 SQL 语句中占位符的对应值。用户调用 Raw() 方法即可改变这两个变量的值。
    2. 定义(s *Session).Clear() 方法,用来处理执行完sql 后,把 通过 *sql.Reset() 把设置的sql. 置为空,及 s.sqlVars = nil 把占位符参数置空
    3. 定义(s *Session).Row(sql string, values… interface{}), 通过 strings.Builder 拼接sql,并保存占位符参数
          s.sql.WriteString(sql)
          s.sql.WriteString(" ")
      	s.sqlVars = append(s.sqlVars, values...)
      	return s
    
    1. 封装Exec(),QueryRow(), QueryRows(),方法,执行拼接后的sql,返回结果
      // 执行完后记得 清空设置的sql
      defer s.Clear()
      log.Info(s.sql.String(), s.sqlVars)
      if result, err = s.DB().Exec(s.sql.String(), s.sqlVars...); err != nil {
      	log.Error(err)
      }
      return
    
    1. 注意事项,
      1. 占位符使用 []interface{} 切片类型的接口接收
      2. 执行完后 清空 执行的sql,及占位符参数
      3. 可变占位符参数的使用,切片的输出 s.sqlVars..., 通过… 传递
day1 包一级目录下的(同go.mod同级) 文件包声明&定义
  • 通过go mod init 声明一个包
  • go.mod 同级目录下的非main包 package 声明 应该和包一致(错误结论,自定义即可,使用别名,不和main.go 同一目录下)
  • 示例
# geeorm 目录下
go mod init geektutu-orm # 声明包为geeorm
  • 目录定义
geektutu-orm/
    |--geeorm.go    
    | cmd_test
      |--main.go
  • geeorm.go package 声明 可以自定义,不要和main.go 同目录,引入自己是不允许的
    package geeorm
  • main 函数下,引入自己是不允许的,需要额外的文件夹,建main.go
  • main 函数使用 geeorm geektutu-orm/cmd_test/main.go
package main

import (
  geeorm "geektutu-orm"
)

day02-对象表结构映射

支持多种类型,考虑通用性的点
  • 表名的获取,通过反射(reflect)获取任意 struct 对象的名称和字段,映射为数据中的表。
  • 数据库类型不一致,将go 类型统一处理数据类型
  • 支持不同数据库类型,数据库的注入&获取
反射的再次使用
  • reflect.TypeOf(), reflect.Kind(), reflect.Kind().Name()
    // reflect.Kind 是 reflect 包中定义的一个枚举类型,一种具体的类型类别
   var x int = 22
   typ := reflect.TypeOf(x)
   typ.Kind() // 返回具体的类型, int , float32, ...,基础类型或复合类型
   typ.Kind().Name() //
  • 反射类型,反射值,反射字段获取…
  • 标签读取!!!
interface 是否完全实现了验证
  • 通过强转(nil)断言, 在编译过程中暴露出来
type P interface{
	Say func(string)string
}

type s struct{}
// 判断是否实现了接口的全部方法
// 声明一个忽略的变量,类型为 需要实现的 P interface, 
// 将我们自定定义的类型 s,通过强转 nil, 验证是否完全实现了P, 断言 s 可以作为 p 类型的赋值
var _ P = (*s)(nil)

Schema 模式
  • 定义数据库字段,标签说明相关,name, type, tag
  • 定义model,model, 字段的关系映射
  • ast.IsExported 判断成员变量是否可导出

day3 新增和查询

基础sql 查询和写入
  • 使用反射(reflect)将数据库的记录转换为对应的结构体实例,实现查询(select)功能
  • 构造sql 语句是通过拼接处理的
  • 参数通过?占位符,拼接
  • 切片类型的断言转换
func _insert(values ...interface{}) (string, []interface{}) {
    // INSERT INTO $tableName ($field)
    tableName := values[0]
	// 类型转换, vaues[1], 本身就是个切片,第一个参数指定 表名,后面指定参数
    fields := strings.Join(values[1].([]string), ",")

    return fmt.Sprintf("INSERT INTO %s (%v)", tableName, fields), []interface{}{}
}
  • Find 功能的难点和 Insert 恰好反了过来。Insert 需要将已经存在的对象的每一个字段的值平铺开来,而 Find 则是需要根据平铺开的字段的值构造出对象。同样,也需要用到反射(reflect)。
  • sqllit3 返回结果 用 next(),循环处理
// rows = 
rows,err := s.Raw(sql,vars).QueryRows()
// 返回结果集,循环处理
for rows.Next(){

}

day04- 链式操作&更新删除

  • 反射的多层级应用
func (s *Session) First(value interface{}) error {
    est := reflect.Indirect(reflect.ValueOf(value))
    destSlice := reflect.New(reflect.SliceOf(dest.Type())).Elem()
    if err := s.Limit(1).Find(destSlice.Addr().Interface()); err != nil {
        return err
    }
    if destSlice.Len() == 0 {
		return errors.New("NOT FOUND")
    }
    dest.Set(destSlice.Index(0))
    return nil
}
  • 错误的返回
err := errro.New("这是一个错误")
return err 

day5 钩子支持

钩子的定义
Hook,翻译为钩子,其主要思想是提前在可能增加功能的地方埋好(预设)一个钩子,当我们需要重新修改或者增加这个地方的逻辑的时候,把扩展的类或者方法挂载到这个点即可。钩子的应用非常广泛,例如 Github 支持的 travis 持续集成服务,当有 git push 事件发生时,会触发 travis 拉取新的代码进行构建。IDE 中钩子也非常常见,比如,当按下 Ctrl + s 后,自动格式化代码。再比如前端常用的 hot reload 机制,前端代码发生变更时,自动编译打包,通知浏览器自动刷新页面,实现所写即所得。

钩子机制设计的好坏,取决于扩展点选择的是否合适。例如对于持续集成来说,代码如果不发生变更,反复构建是没有意义的,因此钩子应设计在代码可能发生变更的地方,比如 MR、PR 合并前后。

那对于 ORM 框架来说,合适的扩展点在哪里呢?很显然,记录的增删查改前后都是非常合适的。

比如,我们设计一个 Account 类,Account 包含有一个隐私字段 Password,那么每次查询后都需要做脱敏处理,才能继续使用。如果提供了 AfterQuery 的钩子,查询后,自动地将 Password 字段的值脱敏,是不是能省去很多冗余的代码呢?
钩子的实现
  • 钩子与结构体绑定
  • 定义钩子常量
  • 定义钩子常量 BeforeQuery = "BeforeQuery", AfterQuery = "AfterQuery" , BeforeUpdate = "BeforeUpdate," AfterUpdate = "AfterUpdate", BeforeDelete = "BeforeDelete," AfterDelete = "AfterDelete", BeforeInsert = "BeforeInsert," AfterInsert = "AfterInsert",
  • 通过反射注入钩子
    • MethodByName返回一个函数值,对应于给定名称的v的方法。返回函数的Call参数不应该包含接收者;返回的函数将始终使用v作为接收者。如果没有找到方法,则返回零值。
    • fm := reflect.ValueOf(s.RefTable().Model).MethodByName(method)
    • fm.Call()
    func (s *Session) CallMethod(method string, value interface{}) {
        fm := reflect.ValueOf(s.RefTable().Model).MethodByName(method)
        if value != nil {
          fm = reflect.ValueOf(value).MethodByName(method)
        }
        params := []reflect.Value{reflect.ValueOf(s)}
        if fm.IsValid() {
          if v := fm.Call(params); len(v) > 0 {
              if err, ok := v[0].Interface().(error); ok {
              log.Error(err)
              }
          }
        }
        return
    }
    

day6-支持事务

  • 原生支持事务,打开事务句柄,用事务句柄操作
    db, _ := sql.Open("sqlite3", "gee.db")
	defer func() { _ = db.Close() }()
	_, _ = db.Exec("CREATE TABLE IF NOT EXISTS User(`Name` text);")
	// 开启获取事务句柄
	tx, _ := db.Begin()
	// 用事务句柄操作
	_, err1 := tx.Exec("INSERT INTO User(`Name`) VALUES (?)", "Tom")
  • 事务支持的逻辑
GeeORM 之前的操作均是执行完即自动提交的,每个操作是相互独立的。之前直接使用 sql.DB 对象执行 SQL 语句,
如果要支持事务,需要更改为 sql.Tx 执行。在 Session 结构体中新增成员变量 tx *sql.Tx,当 tx 不为空时,
则使用 tx 执行 SQL 语句,否则使用 db 执行 SQL 语句。
这样既兼容了原有的执行方式,又提供了对事务的支持。
  • 封装的另一个目的是统一打印日志,方便定位问题
  • 测试文件用 t *testing.T 作为参数,用来输出日志方便点
func OpenDB(t *testing.T)*Engine{
    t.Helper()
	engine, err :=NewEngine("sqlite3","gee.db")
    if err != nil {
		// 参数只是用来输出了日志
        t.Fatal("failed to open db", err)
    }
    return engine
}
  • 测试包 t.Helper() 的作用
   t.Helper() 是 testing 包中的一个方法,主要用于标记一个函数为测试辅助函数。当你在编写测试时,常常会调用一些辅助函数来简化测试
代码的编写,或者为测试提供一些额外的功能。 如果你在辅助函数中调用 t.Helper(),它会告诉测试框架,这个函数本身并不是直接进行测试的代码,
而是辅助代码。
  t.Helper() 用于帮助测试框架更清晰地显示错误信息,让你在查看错误日志时能直接看到出错的测试函数,而不是辅助函数。

func checkError(t *testing.T, err error) {
	t.Helper()  // 标记为辅助函数
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestSomething(t *testing.T) {
	// 这里调用了 checkError 函数,它会标记为辅助函数
	err := someFunctionThatReturnsAnError()
	checkError(t, err)  // 错误信息会显示到 TestSomething 中
}
  • t.Run() 用法说明
t.Run 使得你可以在一个测试函数中运行多个子测试,并能为每个子测试指定一个名称,帮助你
func TestAdd(t *testing.T) {
	// 子测试 1:测试 1 + 1
	t.Run("1+1", func(t *testing.T) {
		result := 1 + 1
		expected := 2
		if result != expected {
			t.Errorf("expected %d, got %d", expected, result)
		}
	})

	// 子测试 2:测试 2 + 3
	t.Run("2+3", func(t *testing.T) {
		result := 2 + 3
		expected := 5
		if result != expected {
			t.Errorf("expected %d, got %d", expected, result)
		}
	})

	// 子测试 3:测试 0 + 0
	t.Run("0+0", func(t *testing.T) {
		result := 0 + 0
		expected := 0
		if result != expected {
			t.Errorf("expected %d, got %d", expected, result)
		}
	})
}

day7 迁移

  • 支持新增,或删除字段,不支持字段类型修改
  • 原生实现逻辑
第一步:从 old_table 中挑选需要保留的字段到 new_table 中。
第二步:删除 old_table。
第三步:重命名 new_table 为 old_table。
CREATE TABLE new_table AS SELECT col1, col2, ... from old_table
DROP TABLE old_table
ALTER TABLE new_table RENAME TO old_table;
  • 两个字段的对比,[]string, 用来计算前后两个字段切片的差集。新表 - 旧表 = 新增字段,旧表 - 新表 = 删除字段。
1. 制造一个 map, 循环b, 标识B已经存在的 字段
2. 循环A, 判断 A 字段 是否在map 存在,不存在 则记录 改字段
func difference(a []string, b []string)(diff []string){
	mapB := make(map[string]bool)
	for _,v :=range b{
		mapB[v] = true
	}
	for _,v := range a {
		if _,ok := mapB[v];!ok{
			diff = append(diff,v)
		}
	}
	return
}
变结构变更逻辑 - 删除表的操作影响了已存在的数据
1. 基于当前表的 struct, 获取所有字段(最终字段),表字段的添加,是基于当前表对应的struct
2. 从数据库查询一条数据,解析出数据库字段
3. 两个字段对比,标识 删除的字段和新增的字段
4. 有新增字段,执行 拼接的新增字段sql
5. 无删除的字段,直接返回,当前表struct 是最终的要的字段
6. 有删除的字段,用当前结构创建一个零时表,删除当前表名,把当前用当前要的字段的临时表 重命名 要用的表名
  • *testing.T.Log() 和 Errorf() 的用法, log 需要加 -v
    • t.Log 用于打印调试信息,在 go test 执行时如果使用 -v 参数,会输出日志内容。
    • t.Errorf 用于报告错误,但不终止测试。这将标记测试失败,并继续执行其他测试
reflect.DeepEqual()的用法,用来处理复杂结构类型
reflect.DeepEqual 适用于比较复杂数据结构(如切片、数组、映射、结构体等)。
它通过递归方式对比嵌套的值,提供深度比较。
需要注意它的一些特殊处理(如 NaN、空切片与 nil 切片等)。

- 基本类型:对于基本类型的比较(如 int、float64、string 等),reflect.DeepEqual 的行为和 == 操作符一致。
- 数组和切片:DeepEqual 会比较数组或切片中的每个元素,确认它们是否相等。注意:对于切片,reflect.DeepEqual 会检查它们的内容和长度,而不是它们的内存地址。
- 结构体:如果两个结构体具有相同的字段,并且字段的值相同,DeepEqual 会返回 true。注意,结构体中的匿名字段也会被检查。
- 映射:DeepEqual 会检查映射的键值对是否完全相同,并且顺序无关紧要。
- 指针:对于指针,DeepEqual 比较的是指针指向的值是否相等,而不是指针的内存地址
  • 数组和切片 用 == 判断的支持与不支持
    • 支持
    // 数组比较
    arr1 := [3]int{1, 2, 3}
    arr2 := [3]int{1, 2, 3}
    if arr1 == arr2 {
    fmt.Println("arr1 = arr2 :", arr1 == arr2)
    }
    if arr1 == [3]int{1, 2, 3} {}
    
    
    • 不支持
    // 不支持 这种写法!!! 直接报错
    slice1 := []int{1, 2, 3}
    slice2 := []int{1, 2, 3}
    fmt.Println(reflect.DeepEqual(slice1, slice2)) // 输出 true
    if slice1 == slice2{
        
    }
    
  • 比较示例
package main

import (
	"fmt"
	"reflect"
)

func main() {
	// 基本类型比较
	a := 5
	b := 5
	fmt.Println(reflect.DeepEqual(a, b)) // 输出 true

	// 数组比较
	arr1 := [3]int{1, 2, 3}
	arr2 := [3]int{1, 2, 3}
	fmt.Println(reflect.DeepEqual(arr1, arr2)) // 输出 true

	// 切片比较
	slice1 := []int{1, 2, 3}
	slice2 := []int{1, 2, 3}
	fmt.Println(reflect.DeepEqual(slice1, slice2)) // 输出 true

	// 结构体比较
	type Person struct {
		Name string
		Age  int
	}
	p1 := Person{"John", 30}
	p2 := Person{"John", 30}
	fmt.Println(reflect.DeepEqual(p1, p2)) // 输出 true

	// 映射比较
	map1 := map[string]int{"a": 1, "b": 2}
	map2 := map[string]int{"a": 1, "b": 2}
	fmt.Println(reflect.DeepEqual(map1, map2)) // 输出 true

	// 不同的值
	fmt.Println(reflect.DeepEqual(a, b))   // 输出 true
	fmt.Println(reflect.DeepEqual(arr1, slice1)) // 输出 false
}

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值