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
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)
}
日志类的基础封装
errorLog = log. New ( os. Stdout, "\033[31m[error]\033[0m" , log. LstdFlags| log. Lshortfile)
loggers. SetOutput ( os. Stdout)
errorLog. SetOutput ( ioutil. Discard)
errorLog. SetOutput ( 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()
逻辑分析
定义Session 结构,指定成员变量, *sql.DB, sql , sqlVars
type Session struct {
db * sql. DB
sql strings. Builder
sqlVars [ ] interface { }
}
Session 结构体目前只包含三个成员变量,第一个是 db *sql.DB,即使用 sql.Open() 方法连接数据库成功之后返回的指针。
第二个和第三个成员变量用来拼接 SQL 语句和 SQL 语句中占位符的对应值。用户调用 Raw() 方法即可改变这两个变量的值。 定义(s *Session).Clear() 方法,用来处理执行完sql 后,把 通过 *sql.Reset() 把设置的sql. 置为空,及 s.sqlVars = nil 把占位符参数置空 定义(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
封装Exec(),QueryRow(), QueryRows(),方法,执行拼接后的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
注意事项,
占位符使用 []interface{} 切片类型的接口接收 执行完后 清空 执行的sql,及占位符参数 可变占位符参数的使用,切片的输出 s.sqlVars..., 通过… 传递
day1 包一级目录下的(同go.mod同级) 文件包声明&定义
通过go mod init 声明一个包 go.mod 同级目录下的非main包 package 声明 应该和包一致(错误结论,自定义即可,使用别名,不和main.go 同一目录下) 示例
go mod init geektutu-orm
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()
var x int = 22
typ := reflect. TypeOf ( x)
typ. Kind ( )
typ. Kind ( ) . Name ( )
interface 是否完全实现了验证
type P interface {
Say func ( string ) string
}
type s struct { }
var _ P = ( * s) ( nil )
Schema 模式
定义数据库字段,标签说明相关,name, type, tag 定义model,model, 字段的关系映射 ast.IsExported 判断成员变量是否可导出
day3 新增和查询
基础sql 查询和写入
使用反射(reflect)将数据库的记录转换为对应的结构体实例,实现查询(select)功能 构造sql 语句是通过拼接处理的 参数通过?占位符,拼接 切片类型的断言转换
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 { } { }
}
Find 功能的难点和 Insert 恰好反了过来。Insert 需要将已经存在的对象的每一个字段的值平铺开来,而 Find 则是需要根据平铺开的字段的值构造出对象。同样,也需要用到反射(reflect)。 sqllit3 返回结果 用 next(),循环处理
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() 是 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) {
err := someFunctionThatReturnsAnError ( )
checkError ( t, err)
}
t.Run 使得你可以在一个测试函数中运行多个子测试,并能为每个子测试指定一个名称,帮助你
func TestAdd ( t * testing. T) {
t. Run ( "1+1" , func ( t * testing. T) {
result := 1 + 1
expected := 2
if result != expected {
t. Errorf ( "expected %d, got %d" , expected, result)
}
} )
t. Run ( "2+3" , func ( t * testing. T) {
result := 2 + 3
expected := 5
if result != expected {
t. Errorf ( "expected %d, got %d" , expected, result)
}
} )
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) )
if slice1 == slice2{
}
比较示例
package main
import (
"fmt"
"reflect"
)
func main ( ) {
a := 5
b := 5
fmt. Println ( reflect. DeepEqual ( a, b) )
arr1 := [ 3 ] int { 1 , 2 , 3 }
arr2 := [ 3 ] int { 1 , 2 , 3 }
fmt. Println ( reflect. DeepEqual ( arr1, arr2) )
slice1 := [ ] int { 1 , 2 , 3 }
slice2 := [ ] int { 1 , 2 , 3 }
fmt. Println ( reflect. DeepEqual ( slice1, slice2) )
type Person struct {
Name string
Age int
}
p1 := Person{ "John" , 30 }
p2 := Person{ "John" , 30 }
fmt. Println ( reflect. DeepEqual ( p1, p2) )
map1 := map [ string ] int { "a" : 1 , "b" : 2 }
map2 := map [ string ] int { "a" : 1 , "b" : 2 }
fmt. Println ( reflect. DeepEqual ( map1, map2) )
fmt. Println ( reflect. DeepEqual ( a, b) )
fmt. Println ( reflect. DeepEqual ( arr1, slice1) )
}