从零实现ORM框架GeoORM-database/sql基础-01
- ORM 框架需要干什么
- 关于 GeoORM
- 初识 SQLite
- database/sql 标准库
- 实现一个简单的 log 库
- 核心结构 Session
- 核心结构 Engine
- 测试
本系列参考: 7天用Go从零实现ORM框架GeeORM
本系列源码: https://gitee.com/DaHuYuXiXi/geo-orm
ORM 框架需要干什么
对象关系映射(Object Relational Mapping,简称ORM)是通过使用描述对象和数据库之间映射的元数据,将面向对象语言程序中的对象自动持久化到关系数据库中。
那对象和数据库是如何映射的呢?
数据库 | 面向对象的编程语言 |
---|---|
表(table) | 类(class/struct) |
记录(record, row) | 对象 (object) |
字段(field, column) | 对象属性(attribute) |
举一个具体的例子,来理解 ORM。
代码语言:javascript复制CREATE TABLE `User` (`Name` text, `Age` integer);
INSERT INTO `User` (`Name`, `Age`) VALUES ("Tom", 18);
SELECT * FROM `User`;
第一条 SQL 语句,在数据库中创建了表 User,并且定义了 2 个字段 Name 和 Age;第二条 SQL 语句往表中添加了一条记录;最后一条语句返回表中的所有记录。
假如我们使用了 ORM 框架,可以这么写:
代码语言:javascript复制type User struct {
Name string
Age int
}
orm.CreateTable(&User{})
orm.Save(&User{"Tom", 18})
var users []User
orm.Find(&users)
ORM 框架相当于对象和数据库中间的一个桥梁,借助 ORM 可以避免写繁琐的 SQL 语言,仅仅通过操作具体的对象,就能够完成对关系型数据库的操作。
那如何实现一个 ORM 框架呢?
CreateTable
方法需要从参数&User{}
得到对应的结构体的名称 User 作为表名,成员变量 Name, Age 作为列名,同时还需要知道成员变量对应的类型。Save
方法则需要知道每个成员变量的值。Find
方法仅从传入的空切片&[]User
,得到对应的结构体名也就是表名 User,并从数据库中取到所有的记录,将其转换成 User 对象,添加到切片中。
如果这些方法只接受 User 类型的参数,那是很容易实现的。但是 ORM 框架是通用的,也就是说可以将任意合法的对象转换成数据库中的表和记录。例如:
代码语言:javascript复制type Account struct {
Username string
Password string
}
orm.CreateTable(&Account{})
这就面临了一个很重要的问题:如何根据任意类型的指针,得到其对应的结构体的信息。这涉及到了 Go 语言的反射机制(reflect),通过反射,可以获取到对象对应的结构体名称,成员变量、方法等信息,例如:
代码语言:javascript复制typ := reflect.Indirect(reflect.ValueOf(&Account{})).Type()
fmt.Println(typ.Name()) // Account
for i := 0; i < typ.NumField(); i {
field := typ.Field(i)
fmt.Println(field.Name) // Username Password
}
- reflect.ValueOf() 获取指针对应的反射值。
- reflect.Indirect() 获取指针指向的对象的反射值。
- (reflect.Type).Name() 返回类名(字符串)。
- (reflect.Type).Field(i) 获取第 i 个成员变量。
除了对象和表结构/记录的映射以外,设计 ORM 框架还需要关注什么问题呢?
1)MySQL,PostgreSQL,SQLite 等数据库的 SQL 语句是有区别的,ORM 框架如何在开发者不感知的情况下适配多种数据库?
2)如果对象的字段发生改变,数据库表结构能够自动更新,即是否支持数据库自动迁移(migrate)?
3)数据库支持的功能很多,例如事务(transaction),ORM 框架能实现哪些?
4)…
关于 GeoORM
数据库的特性非常多,简单的增删查改使用 ORM 替代 SQL 语句是没有问题的,但是也有很多特性难以用 ORM 替代,比如复杂的多表关联查询,ORM 也可能支持,但是基于性能的考虑,开发者自己写 SQL 语句很可能更高效。
因此,设计实现一个 ORM 框架,就需要给功能特性排优先级了。
Go 语言中使用比较广泛 ORM 框架是 gorm 和 xorm。除了基础的功能,比如表的操作,记录的增删查改,gorm 还实现了关联关系(一对一、一对多等),回调插件等;xorm 实现了读写分离(支持配置多个数据库),数据同步,导入导出等。
gorm 正在彻底重构 v1 版本,短期内看不到发布 v2 的可能。相比于 gorm-v1,xorm 在设计上更清晰。GeoORM的设计主要参考了 xorm,一些细节上的实现参考了 gorm。GeoORM的目的主要是了解 ORM 框架设计的原理,具体实现上鲁棒性做得不够,一些复杂的特性,例如 gorm 的关联关系,xorm 的读写分离没有实现。目前支持的特性有:
gorm 正在彻底重构 v1 版本,短期内看不到发布 v2 的可能。相比于 gorm-v1,xorm 在设计上更清晰。GeeORM 的设计主要参考了 xorm,一些细节上的实现参考了 gorm。GeeORM 的目的主要是了解 ORM 框架设计的原理,具体实现上鲁棒性做得不够,一些复杂的特性,例如 gorm 的关联关系,xorm 的读写分离没有实现。目前支持的特性有:
- 表的创建、删除、迁移。
- 记录的增删查改,查询条件的链式操作。
- 单一主键的设置(primary key)。
- 钩子(在创建/更新/删除/查找之前或之后)
- 事务(transaction)。
- …
初识 SQLite
SQLite中文文档
SQLite基本语法和Mysql等关系型数据库大体一致,无需耗费太多时间即可掌握
SQLite 是一款轻量级的,遵守 ACID 事务原则的关系型数据库。SQLite 可以直接嵌入到代码中,不需要像 MySQL、PostgreSQL 需要启动独立的服务才能使用。SQLite 将数据存储在单一的磁盘文件中,使用起来非常方便。也非常适合初学者用来学习关系型数据的使用。GeoORM的所有的开发和测试均基于 SQLite。
目前,几乎所有版本的 Linux 操作系统都附带 SQLite。所以,只要使用下面的命令来检查您的机器上是否已经安装了 SQLite。
在 Ubuntu 上,安装 SQLite 只需要一行命令,无需配置即可使用。 apt-get install sqlite3
接下来,连接数据库(geo.db),如若 geo.db 不存在,则会新建。如果连接成功,就进入到了 SQLite 的命令行模式,执行 .help 可以看到所有的帮助命令。
代码语言:javascript复制# sqlite3 geo.db
SQLite version 3.7.17 2013-05-20 00:56:22
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
使用 SQL 语句新建一张表 User,包含两个字段,字符串 Name 和 整型 Age。
代码语言:javascript复制CREATE TABLE User(Name text, Age integer);
插入两条数据
代码语言:javascript复制INSERT INTO User(Name, Age) VALUES ("Tom", 18), ("Jack", 25);
执行简单的查询操作,在执行之前使用 .head on 打开显示列名的开关,.mode column让每一列左对齐显示,这样查询结果看上去更直观。
代码语言:javascript复制.head on
.mode column
代码语言:javascript复制# 查找 `Age > 20` 的记录
SELECT * FROM User WHERE Age > 20;
代码语言:javascript复制# 统计记录个数
SELECT COUNT(*) FROM User;
使用 .table 查看当前数据库中所有的表(table),执行 .schema < table > 查看建表的 SQL 语句。
SqlLite的基本CURD用法和Mysql等关系型数据库一致,这里不多介绍了,详细内容可以参考官方文档
database/sql 标准库
Go 语言提供了标准库 database/sql 用于和数据库的交互,接下来我们写一个 Demo,看一看这个库的用法。
代码语言:javascript复制package main
import (
"database/sql"
"fmt"
_ "github.com/mattn/go-sqlite3"
)
func main() {
db, _ := sql.Open("sqlite3", "geo.db")
defer func() { _ = db.Close() }()
_, _ = db.Exec("DROP 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()
fmt.Println(affected)
}
row := db.QueryRow("SELECT Name FROM User LIMIT 1")
var name string
if err := row.Scan(&name); err == nil {
fmt.Println(name)
}
}
go-sqlite3 依赖于 gcc,如果这份代码在 Windows 上运行的话,需要安装 mingw 或其他包含有 gcc 编译器的工具包。
执行 go run .,输出如下。
代码语言:javascript复制> go run .
2020/03/07 20:28:37 2
2020/03/07 20:28:37 Tom
- 使用 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 即可获取到查询的结果。
掌握了基础的 SQL 语句和 Go 标准库 database/sql 的使用,可以开始实现 ORM 框架的雏形了。
实现一个简单的 log 库
开发一个框架/库并不容易,详细的日志能够帮助我们快速地定位问题。因此,在写核心代码之前,我们先用几十行代码实现一个简单的 log 库。
为什么不直接使用原生的 log 库呢?log 标准库没有日志分级,不打印文件和行号,这就意味着我们很难快速知道是哪个地方发生了错误。
这个简易的 log 库具备以下特性:
- 支持日志分级(trace,info,debug,warn,error)。
- 不同层级日志显示时使用不同的颜色区分。
- 显示打印日志代码对应的文件名和行号。
- 初始化项目结构如下:
- log.go
编写自己的日志库设计到对Log标准库的相关操作,建议大家先熟悉一下标准库的操作:
GoLang的Log标准库介绍
print输出的字体颜色设置
代码语言:javascript复制package log
import (
"io/ioutil"
"log"
"os"
"sync"
)
var (
//参数: 日志输出到控制台, 日志输出的统一前缀设置(包括颜色设置) , 日志输出的额外选项: 输出日期,文件名和行号
//print( "