从零实现ORM框架GeoORM-database/sql基础-01

Posted 大忽悠爱忽悠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从零实现ORM框架GeoORM-database/sql基础-01相关的知识,希望对你有一定的参考价值。

从零实现ORM框架GeoORM-database/sql基础-01


本系列参考: 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。

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 框架,可以这么写:

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 框架是通用的,也就是说可以将任意合法的对象转换成数据库中的表和记录。例如:

type Account struct 
    Username string
    Password string


orm.CreateTable(&Account)

这就面临了一个很重要的问题:如何根据任意类型的指针,得到其对应的结构体的信息。这涉及到了 Go 语言的反射机制(reflect),通过反射,可以获取到对象对应的结构体名称,成员变量、方法等信息,例如:

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 框架是 gormxorm。除了基础的功能,比如表的操作,记录的增删查改,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 可以看到所有的帮助命令。

# 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。

CREATE TABLE User(Name text, Age integer);

插入两条数据

INSERT INTO User(Name, Age) VALUES ("Tom", 18), ("Jack", 25);

执行简单的查询操作,在执行之前使用 .head on 打开显示列名的开关,.mode column让每一列左对齐显示,这样查询结果看上去更直观。

.head on
.mode column
# 查找 `Age > 20` 的记录
SELECT * FROM User WHERE Age > 20;

# 统计记录个数
SELECT COUNT(*) FROM User;


使用 .table 查看当前数据库中所有的表(table),执行 .schema < table > 查看建表的 SQL 语句。

SqlLite的基本CURD用法和Mysql等关系型数据库一致,这里不多介绍了,详细内容可以参考官方文档


database/sql 标准库

Go 语言提供了标准库 database/sql 用于和数据库的交互,接下来我们写一个 Demo,看一看这个库的用法。

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 .,输出如下。

> 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输出的字体颜色设置

package log

import (
	"io/ioutil"
	"log"
	"os"
	"sync"
)

var (
	//参数: 日志输出到控制台, 日志输出的统一前缀设置(包括颜色设置) , 日志输出的额外选项: 输出日期,文件名和行号
	//print( "\\033[字背景颜色;字体颜色m字符串\\033[0m" )
	errorLog = log.New(os.Stdout, "\\033[31m[error]\\033[0m ", log.LstdFlags|log.Lshortfile)
	warnLog  = log.New(os.Stdout, "\\033[33m[warn ]\\033[0m ", log.LstdFlags|log.Lshortfile)
	infoLog  = log.New(os.Stdout, "\\033[34m[info ]\\033[0m ", log.LstdFlags|log.Lshortfile)
	debugLog = log.New(os.Stdout, "\\033[36m[debug]\\033[0m ", log.LstdFlags|log.Lshortfile)
	traceLog = log.New(os.Stdout, "\\033[30m[trace]\\033[0m ", log.LstdFlags|log.Lshortfile)

	//存放所有日志记录器的数组
	loggers = []*log.LoggererrorLog, warnLog, infoLog, debugLog, traceLog
	mu      sync.Mutex
)

//日志输出方法
var (
	Error  = errorLog.Println
	Errorf = errorLog.Printf
	Warn   = warnLog.Println
	Warnf  = warnLog.Printf
	Info   = infoLog.Println
	Infof  = infoLog.Printf
	Debug  = debugLog.Println
	Debugf = debugLog.Printf
	Trace  = traceLog.Println
	Tracef = traceLog.Printf
)

//日志级别
const (
	TraceLevel = iota
	DebugLevel
	InfoLevel
	WarnLevel
	ErrorLevel
	Disabled
)

//SetLevel 设置日志级别
func SetLevel(level int) 
	mu.Lock()
	defer mu.Unlock()
    //ioutil.Discard,即不打印该日志
	if ErrorLevel < level 
		errorLog.SetOutput(ioutil.Discard)
	

	if WarnLevel < level 
		warnLog.SetOutput(ioutil.Discard)
	

	if InfoLevel < level 
		infoLog.SetOutput(ioutil.Discard)
	

	if DebugLevel < level 
		debugLog.SetOutput(ioutil.Discard)
	

	if TraceLevel < level 
		traceLog.SetOutput(ioutil.Discard)
	


  • log_test.go
package log

import (
	"testing"
)

func TestPrintColor(t *testing.T) 
	SetLevel(InfoLevel)
	logAllLevel()


func logAllLevel() 
	errorLog.Println("error日志输出测试")
	warnLog.Println("warn日志输出测试")
	infoLog.Println("info日志输出测试")
	debugLog.Println("debug日志输出测试")
	traceLog.Println("trace日志输出测试")


上面是极客兔兔给出的日志库Demo,下面给出一个我自己写的日志库demo:

package myLog

import (
	"errors"
	"io"
	"log"
	"os"
	"sync"
)

type any interface

//日志级别
const (
	TraceLevel = iota
	DebugLevel
	InfoLevel
	WarnLevel
	ErrorLevel
)

var mu sync.Mutex

type Log struct 
	//日志级别名称
	Log string
	//日志级别
	level int
	//日志输出前缀
	prefix string
	//是否开启了当前日志级别的输出
	logAble bool
	//标准库
	logger *log.Logger
	//日志输出到哪里
	out io.Writer


func (l *Log) log(v any) 
	if l.logAble 
		l.logger.Println(v)
	


func (l *Log) logf(str string, args interface) 
	if l.logAble 
		l.logger.Printf(str, args)
	


// 初始化相关log
var (
	traceLog = &LogLog: "trace", prefix: "\\033[30m[trace]\\033[0m ", out: os.Stdout, logAble: false, level: 0
	debugLog = &LogLog: "debug", prefix: "\\033[36m[debug]\\033[0m ", out: os.Stdout, logAble: false, level: 1
	infoLog  = &LogLog: "info", prefix: "\\033[34m[info ]\\033[0m ", out: os.Stdout, logAble: true, level: 2
	warnLog  = &LogLog: "warn", prefix: "\\033[33m[warn ]\\033[0m ", out: os.Stdout, logAble: true, level: 3
	errorLog = &LogLog: "error", prefix: "\\033[31m[error]\\033[0m ", out: os.Stdout, logAble: true, level: 4
)

func init() 
	traceLog.logger = log.New(traceLog.out, traceLog.prefix, log.LstdFlags|log.Lshortfile)
	debugLog.logger = log.New(debugLog.out, debugLog.prefix, log.LstdFlags|log.Lshortfile)
	infoLog.logger = log.New(infoLog.out, infoLog.prefix, log.LstdFlags|log.Lshortfile)
	warnLog.logger = log.New(warnLog.out, warnLog.prefix, log.LstdFlags|log.Lshortfile)
	errorLog.logger = log.New(errorLog.out, errorLog.prefix, log.LstdFlags|log.Lshortfile)


var logs = []*LogtraceLog, debugLog, infoLog, warnLog, errorLog

type HandleLog func(l *Log)

//logsOp 对日志数组中每个日志log进行处理
func logsOp(logHandle HandleLog) 
	for _, log := range logs 
		logHandle(log)
	


//SetLogLevel 设置日志级别
func SetLogLevel(level int) 
	mu.Lock()
	defer mu.Unlock()

	clearLevel()

	if level < 0 
		for _, log := range logs 
			log.logAble = false
		
	 else if level >= len(logs) 
		clearLevel()
	 else 
		for i := 0; i < level; i++ 
			logs[i].logAble = false
		
	


//clearLevel 让所有日志级别都可以输出日志
func clearLevel() 
	logsOp(func(l *Log) 
		l.logAble = true
	)


//SetGlobalLogOutPut 设置全局日志输出
func SetGlobalLogOutPut(out io.Writer) 
	logsOp(func(l *Log) 
		l.logger.SetOutput(out)
	)


func SetGlobalLogMulOutPut(out ...io.Writer) 
	logsOp(func(l *Log) 
		l.logger.SetOutput(io.MultiWriter(out...))
	)


//SetLogOutPut 设置某个级别的日志输出
func SetLogOutPut(logLevel int, out io.Writer) 
	if logLevel < 0 || logLevel >= len(logs) 
		panic(errors.New("logLevel is wrong"))
	
	logs[logLevel].logger.SetOutput(out)


func SetLogMulOutPut(logLevel int, out ...io.Writer) 
	if logLevel < 0 || logLevel >= len(logs) 
		panic(errors.New("logLevel is wrong"))
	
	logs[logLevel].logger.SetOutput(io.MultiWriter(out...))


func Info(v any) 
	infoLog.log(v)


func Infof(str string, args interface) 
	infoLog.logf(str, args)


func Trace(v any) 
	traceLog.log(v)


func Tracef(str string, args interface) 
	traceLog.logf(str, args)


func Debug(v any) 
	debugLog.log(v)


func Debugf(str string, args interface) 
	debugLog.logf(str, args)


func Warn(v any) 
	warnLog.log(v)


func Warnf(str string, args interface) 
	warnLog.logf(str, args)


func Error(v any) 
	errorLog.log(v)


func Errorf(str string, args interface) 
	errorLog.logf(str, args)


  • 测试
从零实现ORM框架GeoORM-记录新增和查询-03

Node.js-Koa2框架生态实战-从零模拟新浪微博

hibernate从零开始到各种映射

SSM框架专题-MyBatis框架从零入门老杜版笔记(上)

代码篇从零开始一步步搭建自己的golang框架

从零实现深度学习框架收藏