简易实现 Go 的 ORM 框架(上) | 青训营
一、数据库基础
1.啥是ORM啊
对象关系映射(Object Relational Mapping,简称ORM)是通过使用描述对象和数据库之间映射的元数据,将面向对象语言程序中的对象自动持久化到关系数据库中。通过ORM框架,应用程序中的类和对象可以直接映射到数据库的表和列,ORM框架负责管理实体对象和关系数据库之间的映射。开发人员无需编写复杂的SQL语句,只需要使用面向对象编程语言中的类和对象来进行数据库操作,因此减少了程序开发的工作量和开发时间。
如果你学过Java的话,可以认为我们在写一个go语言的Mybatis
,具体包括以下功能:
- 表的创建、删除、迁移。
- 记录的增删查改,查询条件的链式操作。
- 单一主键的设置(primary key)。
- 钩子(在创建/更新/删除/查找之前或之后)
- 事务(transaction)
2.玩玩Sqlite
首先准备一台Linux的虚拟机或是服务器,不出意外的话,都内置了Sqlite
,在命令行中输入sqlite3
即可看到版本信息:
SQLite version 3.7.17 2013-05-20 00:56:22
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
如果没有安装sqlite
,一行命令即可解决:
apt-get install sqlite3
接下来,让我们输入几行命令玩一玩:
[root@VM-12-14-centos ~]# sqlite3 gee.db //创建名为gee的数据库
SQLite version 3.7.17 2013-05-20 00:56:22
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> CREATE TABLE User(Name text, Age integer); //创建名为User的表,拥有Name、Age字段
sqlite> INSERT INTO User(Name, Age) VALUES ("Tom", 18), ("Jack", 25); //向表中插入信息
sqlite> .head on //显示表头信息
sqlite> select * from User; //查询User表中的所有内容
Name|Age
Tom|18
Jack|25
sqlite>
sqlite> .table //查看库中拥有的表格
User
以上命令熟悉以后,就用go语言来实现吧!
import (
"database/sql"
"log"
_ "github.com/mattn/go-sqlite3"
)
func main() {
db, _ := sql.Open("sqlite3", "gee.db")
defer func() { _ = db.Close() }()
_, _ = db.Exec("DEOP TABLE IF EXISTS User;")
_, _ = db.Exec("CREATE TABLE User(Name text);")
result, err := db.Exec("INSERT INTO User(`Name`) values (?),(?)", "Tom", "Sam")
if err == nil {
affected, _ := result.RowsAffected()
log.Println(affected)
}
row := db.QueryRow("SELECT Name FROM User LIMIT 1")
var name string
if err := row.Scan(&name); err == nil {
log.Println(name)
}
}
尽管没有任何注释,但也比较容易看懂。Open
创建了sqlite的连接,Exec
执行相应的sql语句,?
用作占位符防止sql注入,运行结果如下所示:
到此,算是熟悉了一下database/sql
这个包。
3.实现一个log库
Go原生的log
库没有实现日志分级、打印文件行号等操作,对于后面的调试和定位出错不太友好,因此封装一个具有如下功能的log
库:
- 支持日志分级(Info、Error、Disabled 三级)。
- 不同层级日志显示时使用不同的颜色区分。
- 显示打印日志代码对应的文件名和行号。
开始编写log/log.go
:
var (
errorLog = log.New(os.Stdout, "\033[31m[error]\033[0m ", log.LstdFlags|log.Lshortfile)
infoLog = log.New(os.Stdout, "\033[34m[error]\033[0m ", log.LstdFlags|log.Lshortfile)
loggers = []*log.Logger{errorLog, infoLog}
mu sync.Mutex
)
var (
Error = errorLog.Println
Errorf = errorLog.Printf
Info = infoLog.Println
Infof = infoLog.Printf
)
const (
InfoLevel = iota
ErrorLevel
Disabled
)
func SetLevel(level int) {
mu.Lock()
defer mu.Unlock()
for _, logger := range loggers {
logger.SetOutput(os.Stdout)
}
if ErrorLevel < level {
errorLog.SetOutput(ioutil.Discard)
}
if InfoLevel < level {
infoLog.SetOutput(ioutil.Discard)
}
}
代码中设置了[info]
为蓝色,[error]
为红色,并暴露Error
、Errorf
、Info
、Infof
4个方法;将Info、Error、Disabled声明为三个常量,通过Output
来控制日志是否打印。至此,我们完成了log
库的编写。
4.核心结构Session
下面就在session/raw.go
写一下核心的数据结构Session
,用于对数据库进行操作:
type Session struct {
db *sql.DB
sql strings.Builder
sqlVars []interface{}
}
func New(db *sql.DB) *Session {
return &Session{db: db}
}
func (s *Session) Clear() {
s.sql.Reset()
s.sqlVars = nil
}
func (s *Session) DB() *sql.DB {
return s.db
}
func (s *Session) Raw(sql string, values ...interface{}) *Session {
s.sql.WriteString(sql)
s.sql.WriteString(" ")
s.sqlVars = append(s.sqlVars, values...)
return s
}
func (s *Session) Exec() (result sql.Result, err error) {
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
}
func (s *Session) QueryRow() *sql.Row {
defer s.Clear()
log.Info(s.sql.String(), s.sqlVars)
return s.DB().QueryRow(s.sql.String(), s.sqlVars)
}
func (s *Session) QueryRows() (rows *sql.Rows, err error) {
defer s.Clear()
log.Info(s.sql.String(), s.sqlVars)
if rows, err = s.DB().Query(s.sql.String(), s.sqlVars...); err != nil {
log.Error(err)
}
return
}
db *sql.DB
,即使用sql.Open()
方法连接数据库成功之后返回的指针。- 第二个和第三个成员变量用来拼接 SQL 语句和 SQL 语句中占位符的对应值。用户调用
Raw()
方法即可改变这两个变量的值。 Exec()
、QueryRow()
、QueryRows()
三个方法无非是对原有方法的封装,加上了日志打印部分,且每次执行完后都清空s.sql
和s.sqlVars
,实现了开启一次会话,执行多次SQL的功能。
个人觉得这部分很好理解,就是把比较底层的东西封装了一层,封装完以后继续封装它们对应的方法。
5.核心结构Engine
Session
的功能是和数据库交互,但是在交互之前,需要验证数据库是否连接成功,在交互以后,还要处理数据库连接的关闭操作,因此在geeorm.go
中封装Engine
解决这个问题:
type Engine struct {
db *sql.DB
}
func NewEngine(driver, source string) (e *Engine, err error) {
db, err := sql.Open(driver, source)
if err != nil {
log.Error(err)
return
}
if err = db.Ping(); err != nil {
log.Error(err)
return
}
e = &Engine{db: db}
log.Info("Connect database success")
return
}
func (e *Engine) Close() {
if err := e.db.Close(); err != nil {
log.Error("Failed to close database")
}
log.Info("Close database success")
}
func (e *Engine) NewSession() *session.Session {
return session.New(e.db)
}
Engine
的代码也不难,NewEngine()
方法返回了*sql.DB
,还用db.Ping()
检验了连接是否成功。NewSession()
则可以通过Engine创建Session,进而与数据库进行交互。
二、对象表结构映射
这部分要解决两个问题:
(1)类型转换:Go语言中的类型和数据库中的类型是有差异的,例如Go中的int
、int8
、int16
对应Sqlite中的Integer
类型;
(2)对象和表的转换:把Go语言中的结构体转换为数据库中的表;
1.类型转换
为了兼容不同数据库数据类型的差异,我们将其提取出来实现最大程度的复用和解耦,下面编写dialect/dialect.go
:
var dialectsMap = map[string]Dialect{}
type Dialect interface {
DataTypeOf(typ reflect.Value) string
TableExistSQL(tableName string) (string, []interface{})
}
func RegisterDialect(name string, dialect Dialect) {
dialectsMap[name] = dialect
}
func GetDialect(name string) (dialect Dialect, ok bool) {
dialect, ok = dialectsMap[name]
return
}
DataTypeOf()
:输入一个 reflect.Value 类型的数据,返回它在数据库中对应的数据类型的字符串。
TableExistSQL()
:输入表名,返回查找该表是否存在的 SQL 语句和查询参数。
为了支持不同的数据库,可以根据需要实现具体的Dialect
接口,并通过RegisterDialect
方法将其注册到全局的方言映射表dialectsMap
中。其中,name
是用于标识这个具体方言实例的字符串,dialect
是实现了Dialect接口的具体方言实例。通过 GetDialect
方法,可以根据name获取对应的具体方言实例。
紧接着,我们就在dialect/sqlite3.go
中添加框架对于Sqlite
的支持:
type sqlite3 struct {
}
var _ Dialect = (*sqlite3)(nil)
func init() {
RegisterDialect("sqlite3", &sqlite3{})
}
func (s *sqlite3) DataTypeOf(typ reflect.Value) string {
switch typ.Kind() {
case reflect.Bool:
return "bool"
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr:
return "integer"
case reflect.Int64, reflect.Uint64:
return "bigint"
case reflect.Float32, reflect.Float64:
return "real"
case reflect.String:
return "text"
case reflect.Array, reflect.Slice:
return "blob"
case reflect.Struct:
if _, ok := typ.Interface().(time.Time); ok {
return "datatime"
}
}
panic(fmt.Sprintf("invalid sql type %s (%s)", typ.Type().Name(), typ.Kind()))
}
func (s *sqlite3) TableExistSQL(tableName string) (string, []interface{}) {
args := []interface{}{tableName}
return "SELECT name FROM sqlite_master WHERE type='table' and name = ?", args
}
在定义了sqlite3
类型并指定其实现Dialect
接口后,代码通过init()
函数将sqlite3
实例注册到全局的方言映射表dialectsMap
中,方言名称为sqlite3
。DataTypeOf()
通过一系列的switch-case
语句,把Go语言中的类型转换为sqlite中的类型,TableExistSQL()
方法输入表名,返回一个SQL语句字符串和一个参数列表,用于后续查询数据库中是否存在该表。
下面讲一下我比较疑惑的一段代码:
var _ Dialect = (*sqlite3)(nil)
将sqlite3
类型转换为Dialect
接口类型,并使用空指针对其进行初始化。这样一来,如果sqlite3
类型没有实现Dialect
接口中的某个方法,编译器会在代码编译期间直接提示编译错误,告诉我们应该在sqlite3
类型中实现哪些方法。
这个技巧被称为 "duck typing"(鸭子类型)。即“如果一个对象走起路来像鸭子,叫起来也像鸭子,那么它就可以被当作鸭子”。在这里借助了反射机制,将sqlite3
类型当做Dialect
类型看待,并在编译期检查了其是否实现了Dialect
接口中的所有方法,实现了编译时类型检查的效果。
2.对象和表的转换
在进行对象和表的转换时,要注意以下问题:
- 表名(table name) —— 结构体名(struct name)
- 字段名和字段类型 —— 成员变量和类型。
- 额外的约束条件(例如非空、主键等) —— 成员变量的Tag(Go 语言通过 Tag 实现,Java、Python 等语言通过注解实现)
接下来编写schema/schema.go
实现上述功能:
type Field struct {
Name string
Type string
Tag string
}
type Schema struct {
Model interface{}
Name string
Fields []*Field
FieldNames []string
fieldMap map[string]*Field
}
func (s *Schema) GetField(name string) *Field {
return s.fieldMap[name]
}
func Parse(dest interface{}, d dialect.Dialect) *Schema {
modelType := reflect.Indirect(reflect.ValueOf(dest)).Type()
schema := &Schema{
Model: dest,
Name: modelType.Name(),
fieldMap: make(map[string]*Field),
}
for i := 0; i < modelType.NumField(); i++ {
p := modelType.Field(i)
if !p.Anonymous && ast.IsExported(p.Name) {
field := &Field{
Name: p.Name,
Type: d.DataTypeOf(reflect.Indirect(reflect.New(p.Type))),
}
if v, ok := p.Tag.Lookup("geeorm"); ok {
field.Tag = v
}
schema.Fields = append(schema.Fields, field)
schema.FieldNames = append(schema.FieldNames, p.Name)
schema.fieldMap[p.Name] = field
}
}
return schema
}
数据库中的表是由多个列组成的。每个列都有自己的字段名、数据类型和其他属性(如是否允许为空、是否唯一等),所以用Field
结构体表示一个字段的元信息,可以理解为对应数据库表中的列。
结构体是 Schema
代表的就是数据库中的表,在它的定义中,我们使用 Fields
存储每个字段(即每个列)对应的 Field
结构体,Name
则表示了表的名字。同时,Schema
还提供了一个 GetField
方法,用于根据字段名字获取对应的 Field
结构体,便于进行 ORM 操作。
Paese()
主要的作用是将传入的Go语言结构体类型dest
中的每个字段作为数据库表的一列进行解析,并生成对应的Field
结构体对象,将其保存在Schema
结构体对象中,最终返回该Schema
结构体对象。
具体实现上,首先使用reflect.ValueOf
获取dest
这个结构体的值,但因为设计的入参是一个对象的指针,所以需要 reflect.Indirect()
获取指针指向的实例,紧接着Type()
获取该指针指向的实例对应的值的类型赋给modelType
,并通过 modelType.NumField()
获取dest
中字段的数量。然后通过循环遍历每个字段,将字段的Name
、Type
、Tag
等元信息解析出来,并创建对应的Field
结构体对象,将其添加到Schema
结构体中的Fields
数组中,并将Name
保存在Schema
结构体的FieldNames
数组中,同时将Name
和Field
结构体对象的指针插入到fieldMap
中。
在for循环中,p.Anonymous
表示p
是否为匿名字段,如果是匿名字段则返回true
,否则返回false
。这里使用取反操作来判断p
是否为非匿名字段。ast.IsExported(p.Name)
判断p.Name
是否为导出字段的名称,如果是导出字段则返回true
,否则返回false
。导出字段指的是首字母大写的字段,可以在包外被访问。
3.Session
由于新增了Dialect
和Schema
,Session
的字段也要对应进行调整:
type Session struct {
db *sql.DB
dialect dialect.Dialect
refTable *schema.Schema
sql strings.Builder
sqlVars []interface{}
}
func New(db *sql.DB,dialect dialect.Dialect) *Session {
return &Session{
db: db,
dialect: dialect,
}
}
前面在定义Session
的时候设计它是对数据库进行操作的部分,因此在文件夹session
下新建table.go
用于放置操作数据库表相关的代码session/table.go
:
func (s *Session) Model(value interface{}) *Session {
if s.refTable == nil || reflect.TypeOf(value) != reflect.TypeOf(s.refTable.Model) {
s.refTable = schema.Parse(value, s.dialect)
}
return s
}
func (s *Session) RefTable() *schema.Schema {
if s.refTable == nil {
log.Error("Model is not set")
}
return s.refTable
}
func (s *Session) CreateTable() error {
table := s.RefTable()
var colums []string
for _, field := range table.Fields {
colums = append(colums, fmt.Sprintf("%s %s %s", field.Name, field.Type, field.Tag))
}
desc := strings.Join(colums, ",")
_, err := s.Raw(fmt.Sprintf("CREATE TABLE %s (%s);", table.Name, desc)).Exec()
return err
}
func (s *Session) DropTable() error {
_, err := s.Raw(fmt.Sprintf("DROP TABLE IF EXISTS %s", s.RefTable().Name)).Exec()
return err
}
func (s *Session) HasTable() bool {
sql, values := s.dialect.TableExistSQL(s.RefTable().Name)
row := s.Raw(sql, values...).QueryRow()
var tmp string
_ = row.Scan(&tmp)
return tmp == s.refTable.Name
}
Model
(方法用于指定「模型」(即数据库表的映射结构),该方法接收一个 value
参数,它可以是任意一个 Go 结构体对象。在方法内部,会通过反射获取 value
的类型,然后判断该类型是否与现有的 refTable
中的模型类型相同(也就是上一次执行 Model 方法时传入的结构体类型是否相同),如果不同,则调用 schema.Parse
方法对 value
进行解析,并返回其对应的模型结构体,同时将这个模型结构体保存到 Session
结构体的 refTable
字段中,以便 Session 对象执行后续的数据库操作。
RefTable()
方法 用于获取会话结构体 Session
中保存的当前操作的该表的模型结构体。如果该模型结构体还未被设置(即 refTable
字段为空),则会输出错误日志并返回空值。如果该模型结构体已经被设置,则返回保存在 refTable
字段中的值。
后面三个方法的逻辑都比较相似,分别为创建表、删除表、判断是否具有表,都是利用RefTable()
返回的数据库表和字段的信息,拼接出 SQL 语句,调用原生SQL接口执行。
4.Engine
同理,添加了新字段,Engine
也要对应更新:
type Engine struct {
db *sql.DB
dialect dialect.Dialect
}
func NewEngine(driver, source string) (e *Engine, err error) {
db, err := sql.Open(driver, source)
if err != nil {
log.Error(err)
return
}
if err = db.Ping(); err != nil {
log.Error(err)
return
}
dial, ok := dialect.GetDialect(driver)
if !ok {
log.Errorf("dialect %s Not Found", driver)
return
}
e = &Engine{db: db, dialect: dial}
log.Info("Connect database success")
return
}
func (e *Engine) NewSession() *session.Session {
return session.New(e.db, e.dialect)
}
具体修改为:创建Engine
实例时,获取driver
对应的dialect
;创建Session
实例时,传递dialect
给构造函数New
。
三、记录新增和查询
这部分要实现两个功能:
- 实现新增(insert)记录的功能。
- 使用反射(reflect)将数据库的记录转换为对应的结构体实例,实现查询(select)功能。
1.Clause构造SQL语句
如果要写一条查询语句,那么它大概长这样:
SELECT col1, col2, ...
FROM table_name
WHERE [ conditions ]
GROUP BY col1
HAVING [ conditions ]
所以要一次性实现SQL语句的拼接会比较困难,因此将构造语句这一部分独立出来,写在clause/generator.go
中:
type generator func(values ...interface{}) (string, []interface{})
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
}
func genBindVars(num int) string {
var vars []string
for i := 0; i < num; i++ {
vars = append(vars, "?")
}
return strings.Join(vars, ", ")
}
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{}{}
}
func _values(values ...interface{}) (string, []interface{}) {
var bindStr string
var sql strings.Builder
var vars []interface{}
sql.WriteString("VALUES ")
for i, value := range values {
v := value.([]interface{})
if bindStr == "" {
bindStr = genBindVars(len(v))
}
sql.WriteString(fmt.Sprintf("(%v)", bindStr))
if i+1 != len(values) {
sql.WriteString(", ")
}
vars = append(vars, v...)
}
return sql.String(), vars
}
func _select(values ...interface{}) (string, []interface{}) {
tableName := values[0]
fields := strings.Join(values[1].([]string), ",")
return fmt.Sprintf("SELECT %v FROM %s", fields, tableName), []interface{}{}
}
func _limit(values ...interface{}) (string, []interface{}) {
return "LIMIT ?", values
}
func _where(values ...interface{}) (string, []interface{}) {
desc, vars := values[0], values[1:]
return fmt.Sprintf("WHERE %s", desc), vars
}
func _orderBy(values ...interface{}) (string, []interface{}) {
return fmt.Sprintf("ORDER BY %s", values[0]), []interface{}{}
}
首先要说明的是,Type
以及generators
中的key部分会爆红,先别着急,后面会解决,下面讲解这部分代码:
变量generators
是一个map
,它的key
表示SQL语句的一部分,如INSERT
、VALUES
、SELECT
等等。而value
是一个函数,用于根据输入的参数动态地生成对应的SQL语句和绑定变量列表。在init
函数中,我们使用make
方法初始化了generators
,并将每个SQL语句的构建函数注册到generators
中去。
genBindVars()
方法用于生成指定数量的SQL绑定变量(即问号)。它接收一个num
参数,表示需要生成多少个绑定变量,然后使用 strings.Join
方法将多个问号连成一个逗号分隔的字符串返回。
_insert()
方法接收一个变长参数列表values
,其中包含要插入的表名和字段列表。函数首先通过 fmt.Sprintf
将表名和字段列表拼接成一个 INSERT INTO 语句,然后返回该语句和一个空的绑定变量列表。其中,values[1]
表示函数_insert
中传入的第二个参数,是一个字符串切片,存储了所有要插入的字段名。values[1].([]string)
表示将values[1]
转换为字符串切片类型。因为 values[1]
是一个空接口类型,需要通过类型断言将其转换为指定的类型[]string
,以便访问其中的元素。strings.Join(values[1].([]string), ",")
将这个字符串切片中的所有元素用逗号 ,
连接起来,并返回一个字符串类型的结果。这个结果将作为插入语句中的字段名。例如,如果values[1]
是[]string{"id", "name", "age"}
,那么这个函数返回的结果就是"id,name,age"
。
_values()
方法接收一个变长参数列表values
,其中每个元素都表示要插入的记录。函数遍历所有的记录并将它们拼接成 VALUES 语句,最终返回该语句和一个包含所有绑定变量的列表。
_select()
方法接收两个参数,表名和字段列表。函数通过fmt.Sprintf
将表名和字段列表拼接成一个 SELECT 语句,最终返回该语句和一个空的绑定变量列表。
_limit()
方法接收一个表示数量的参数values
,它将这个参数包装成一个LIMIT语句,并返回该语句和一个只包含LIMIT值的绑定变量列表。
_where()
方法接收一个字符串格式的查询条件和一组绑定变量。函数使用fmt.Sprintf
将查询条件包装成WHERE语句,最终返回该语句和对应的绑定变量列表。
_orderBy()
方法接收一个描述排序规则的字符串,将其拼接成一个ORDER BY语句。注意,该函数不需要绑定变量,因此返回一个空的绑定变量列表。
通过这些函数的组合,可以构建出包含多个 SQL 语句组成的完整的查询语句,让用户可以方便地将其应用到实际的数据库操作当中。
接着,在clause/clause.go
中实现结构体Clause
拼接各个独立的子句。
type Clause struct {
sql map[Type]string
sqlVars map[Type][]interface{}
}
type Type int
const (
INSERT Type = iota
VALUES
SELECT
LIMIT
WHERE
ORDERBY
)
func (c *Clause) Set(name Type, vars ...interface{}) {
if c.sql == nil {
c.sql = make(map[Type]string)
c.sqlVars = make(map[Type][]interface{})
}
sql, vars := generators[name](vars...)
c.sql[name] = sql
c.sqlVars[name] = vars
}
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[orders]...)
}
}
return strings.Join(sqls, " "), vars
}
在这个文件下声明的Type
和const里面的关键字,解决了generator.go
中的爆红问题。
Clause
为SQL语句生成器的结构体类型。该类型包含两个字段:sql
和sqlVars
。sql
用于存储生成的SQL语句,sqlVars
用于存储语句中的绑定变量。
Set()
:用于设置各种SQL语句的函数。该方法接受一个名为name
的Type值,表示要设置的SQL语句类型,以及可选的变长参数列表vars
,表示该SQL语句中的参数值。该方法首先根据参数获取相应的SQL语句和绑定变量,并存储在结构体中的sql
和sqlVars
字段中。
Build()
:用于组装各种SQL语句的函数。该方法接受一个名为orders
的可变长参数列表,表示要查询的SQL语句类型。该方法根据传入的orders
列表,从结构体中的sql
和sqlVars
字段中取出各种SQL语句和绑定变量,并拼接成一个完整的SQL语句和绑定变量数组,最终返回这两个值。
2.实现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{}
}
接着让我们看一下INSERT对应的Sql语句:
INSERT INTO table_name(col1, col2, col3, ...) VALUES
(A1, A2, A3, ...),
(B1, B2, B3, ...),
...
那么在调用ORM框架时,应该要这么书写代码:
s := geeorm.NewEngine("sqlite3", "gee.db").NewSession()
u1 := &User{Name: "Tom", Age: 18}
u2 := &User{Name: "Sam", Age: 25}
s.Insert(u1, u2, ...)
因此还需要一个步骤:根据数据库中列的顺序,从对象中找到对应的值,按顺序平铺。即u1
、u2
转换为("Tom", 18), ("Same", 25)
这样的格式,所以要先给Schema
新增一个函数RecordValues
完成上述的转换,以下为schema/schema.go
:
func (s *Schema) RecordValues(dest interface{}) []interface{} {
destValue := reflect.Indirect(reflect.ValueOf(dest))
var fieldValues []interface{}
for _, field := range s.Fields {
fieldValues = append(fieldValues, destValue.FieldByName(field.Name).Interface())
}
return fieldValues
}
此方法先通过反射获取dest
的的值,然后遍历Fields
中所有字段,获取每个字段在结构体实例中的值,并将其转换成Interface()
类型,添加到fieldValues
数组中,最终返回。
下面,在session
文件夹下新建record.go
,用于实现记录增删查改相关的代码:
func (s *Session) Insert(values ...interface{}) (int64, error) {
recordValues := make([]interface{}, 0)
for _, value := range values {
table := s.Model(value).RefTable()
s.clause.Set(clause.INSERT, table.Name, table.FieldNames)
recordValues = append(recordValues, table.RecordValues(value))
}
s.clause.Set(clause.INSERT, recordValues...)
sql, vars := s.clause.Build(clause.INSERT, clause.VALUES)
result, err := s.Raw(sql, vars...).Exec()
if err != nil {
return 0, err
}
return result.RowsAffected()
}
该方法遍历每个传入的values
参数,获取该结构体对象对应的表信息,并使用RefTable
函数获取其映射到数据库的元数据信息,然后通过RecordValues
函数处理它在数据库中对应的值集合,将处理后的结果追加到recordValues
中;接着使用clause.Set
函数设置INSERT
操作,并将表名和字段列表传递给clause.Set
函数;再将recordValues
中的所有记录传递给clause.Set
函数,用于设置当前的INSERT
指令;然后使用clause.Build
函数构建与当前INSERT
操作对应的SQL语句,并获取语句中对应待绑定的变量vars
;最后使用s.Raw
函数执行构造后的SQL语句,并将绑定变量作为可变参数列表一同传入。
后续所有构造SQL语句的方式都将与Insert
中构造 SQL 语句的方式一致。分两步:
- 1)多次调用
clause.Set()
构造好每一个子句。 - 2)调用一次
clause.Build()
按照传入的顺序构造出最终的 SQL 语句。
构造完成后,调用Raw().Exec()
方法执行。
3.实现Find功能
在调用ORM的Find功能时,代码一般会这么写:
s := geeorm.NewEngine("sqlite3", "gee.db").NewSession()
var users []User
s.Find(&users);
Find功能和Insert恰好反了过来。Insert需要将已经存在的对象的每一个字段的值平铺开来,而Find则是需要根据平铺开的字段的值构造出对象。同样,也需要用到反射(reflect)。
func (s *Session) Find(values interface{}) error {
destSlice := reflect.Indirect(reflect.ValueOf(values))
destType := destSlice.Type().Elem()
table := s.Model(reflect.New(destType).Elem().Interface()).RefTable()
s.clause.Set(clause.SELECT, table.Name, table.FieldNames)
sql, vars := s.clause.Build(clause.SELECT, clause.WHERE, clause.ORDERBY, clause.LIMIT)
rows, err := s.Raw(sql, vars...).QueryRows()
if err != nil {
return err
}
for rows.Next() {
dest := reflect.New(destType).Elem()
var values []interface{}
for _, name := range table.FieldNames {
values = append(values, dest.FieldByName(name).Addr().Interface())
}
if err := rows.Scan(values...); err != nil {
return err
}
destSlice.Set(reflect.Append(destSlice, dest))
}
return rows.Close()
}
该方法获取传入参数变量values
的反射值,并通过该值获取要查询的结构体类型;接着获取该结构体类型对象对应的表信息,并使用 clause.Set
函数设置SELECT
操作,传递表名和字段列表给clause.Set
函数;构建 SQL 语句,并通过调用Raw
函数执行SQL查询操作,并获取查询结果集;再遍历查询结果,并使用reflect.New
函数创建一个新的变量,然后通过该变量的反射值dest
来获取每个字段的指针;将值的指针添加到与其对应的值的values
数组中,然后使用rows.Scan
函数将查询结果绑定到指定值的指针上。 reflect.Append
函数将匹配的结果添加到切片对象 destSlice
中。最后返回结果集,并关闭结果集的游标。