第五次课-database/sql与GORM的一些相关概念及操作-课程笔记 | 青训营笔记

214 阅读6分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的3篇笔记。介绍了database/sql与GORM的一些原理和操作,以及在本地尝试的一些实现,并附上了一些网上的文档,希望能帮助同学们。

这节课内容属实挺难的,前半部分还好,磕磕绊绊还能听懂,后面实在顶不住了,就没有记下去。建议大家参考其他巨佬的笔记。咱实事求是,确实是超出能力范围了,这些真的是我这种基础班菜鸡能听的吗。 image.png 我之前的话只简单上课学过一点数据库的知识,几乎没实践过,又花了一两个小时速成了一下mysql。没了解过这方面的同学建议简单学一下MYSQL教程,不然理解起来是相当吃力的

01. 理解database/sql

1.1 Quick start

以前没有接触过这方面的内容,代码加了些自己的理解,并自己简单地在本地测试了以下

import (
	"database/sql"
	"fmt"
	_ "github.com/go-sql-driver/mysql"
)

func main() {
	//使用driver+DSN初始化DB连接
	//这里DSN(第二个参数)的格式是
	//用户名:密码@tcp(127.0.0.1:3306)/数据库名,其中3306是默认端口
	db, err := sql.Open("mysql", "root:******@tcp(127.0.0.1:3306)/hello")

	// 执行一条sql,意思是选择users中id=1的记录的id和名字
	//driver会将这句sql发送给数据库,通过rows取回返回的数据

	rows, err := db.Query("select id,name from users where id= ?", 1)

	if err != nil {
		//...
	}
	//处理完毕后记得释放连接
	defer func() {
		err = rows.Close()
	}()

	//数据、错误处理
	var users []User
	//rows是一个游标,Next()不断获取下一条数据
	for rows.Next() {
		//User应该是一个结构体
		var user User
		//将rows中的数据扫描到user中
		err := rows.Scan(&user.ID, &user.Name)

		if err != nil {
			//....
		}
		//将user添加到users列表中
		users = append(users, user)
	}
	//测试一下输出
	fmt.Println(users)

	if rows.Err() != nil {
		//....
	}
}

type User struct {
	ID   int64
	Name string
}

为了对这段代码进行测试,我在本地的mysql中制作了一个table。登录mysql后可以参考这段sql。

create database if not exists hello;
use hello;
create table if not exists users(
    id bigint(20) not null auto_increment,
    name varchar(100) not null,
    PRIMARY KEY (id)
);
insert into users(id,name) values (1,'John');
insert into users(id,name) values (2,'David');
select * from users;

image.png

测试结果:

image.png

1.2 设计原理

这一节主要是对database/sql的原理和应用的一些介绍,我听的也是云里雾里。网上找了一个不错的文章,大家可以借鉴 Go database/sql教程,我觉得吧,更多的是一个工具性的东西,用到的时候搜吧,像我这样的小白指望一下子学明白也不现实

  • database/sql 为应用程序提供标准API操作接口
  • 对下层驱动暴露一些驱动接口(连接接口和操作接口)
  • 内部实现连接池的管理(池化技术)

池化技术:把一些能够复用的东西(比如说数据库连接、线程)放到池中,避免重复创建、销毁的开销,从而极大提高性能。 在开发过程中我们会用到很多的连接池,像是数据库连接池、HTTP 连接池、 Redis 连接池等等。

连接池

连接池配置:
func (db *DB) SetConnMaxIdleTime(d time.Duration)
func (db *DB) SetConnMaxlifeTime(d time.Duration)
func (db *DB) SetMaxIdleConns(n int)
func (db *DB) SetMaxOpenConns(n int)
连接池状态
func (db *DB) Stats() DBStats
操作过程(伪实现)
//maxBadConnRetries默认为2
	for i:=0;i<maxBadConnRetries;i++{
	//从连接池获取连接或通过driver新建连接	
		dc,err:=db.Conn(ctx,strategy)
		//获取连接的两种策略
		//有空闲连接 -> reuse -> max life time
		//若无空闲连接,新建连接 -> max open...

		//将连接放回连接池
		defer dc.db.putConn(dc,err,true)
		//校验
		//validateConnection 有无错误
		//max life time,max idle conns 检查
		
		//连接实现一些操作的接口 driver.Queryer,driver.Execer等interface
		if err==nil{
			err=dc.ci.Query(sql,args...)
		}
		//如果返回BadConn的错误,就回重新for循环两次,如果有一次没有错误,就会break出来
		isBadConn=errors.Is(err,driver.ErrBadConn)
		if !isBadConn{
			break
		}
	}

连接接口

Driver 连接接口
//Driver接口
type Driver interface{
	//Open 返回一个数据库的新的连接
	Open(name string)(Conn,error)
}

//注册全局driver
func Register(name string,driver driver.Driver){
	driversMu.Lock()
	defer driversMu.Unlock()
	if driver == nil{
		panic("sql:Register driver is nil")
	}
	//如果这个名字的driver已经有了的话,报错
	if _,dup := drivers[name];dup{
		panic("sql : Register called twice for driver "+name)
	}
	//drivers应该是个全局的map,我们把driver放到全局变量中
	drivers[name]=driver
}


//业务代码
import _ "github.com/go-sql-driver/mysql"

func main(){
	db,err := sql.Open("mysql","grom:grom@tcp(localhost:9910)/grom?charset=utf&&parseTime=True&loc=Local")
}

//注册driver
func init(){
	sql.Register("mysql",&MySQLDriver{})
}
Driver连接接口2

将DSN转为一个结构体

type Connector interface{
	Connect(context.Context)(Conn,error)
	Driver() Driver
}

func OpenDB(c driver.Connector) *DB{
	// ...
}
import "github.com/go-sql-driver/mysql"
func main(){
	connector,err := mysql.NewConnector(&mysql.Config{
		User:		"gorm",
		Passwd:		"gorm",
		Net:		"tcp",
		Addr:		"127.0.0.1:3306",
		DBName:		"gorm",
		ParseTime:	"true",
	})
	
	db:=sql.OpenDB(connector)
}

操作接口

DB连接的几种类型:
  • 直接连接/Conn:简单的tcp连接
  • 预编译/Stmt:先生成一个prepare statement以及其reference ID,后面执行同样的sql时,就不需要传递原来的sql,只需要找一下reference ID,减少执行时间
  • 事务/Tx
处理返回数据的几种方式:
  • Exec/ExecContext -> result:执行sql只关心结果是否成功
  • Query/QueryContext -> Rows(Columns)查询以行(列)的形式放回
  • QueryRow/QueryRowContext -> Row(Rows的简化)

接口定义:

type driver.Rows interface{
	//返回columns名字
	Columns() []string
	
	//实现数据库协议
	//解析数据库到database/sql.Rows.lastcols中
	Next(dest []Value) error
	
	//多批数据解析
	HasNextResultSet() bool
	NextResultSet() error
}

type Rows struct{
	dc		*driverConn
	lastcols	[]driver.Value
	//...
}

func (rs /Rows) Scan(dest...any) error {
	for i,sv:=range rs.lastcols {
		err := convertAssignRows(dest[i],sv,rs)
		if err!=nil{
			return fmt.Errorf(`sql:Scan error on column index %d , name %q:%w`,i,rs.rowsi.Columns()[i],err)
		}
	}
	return nil
}

func convertAssignRows(dest,src any,rows *Rows) error{
	//...常见的几种数据类型的赋值
}

02. GORM基础使用

GROM:设计简洁,功能强大,自由扩展的全功能ORM 那么问题来了,ORM又是啥嘞? 什么是ORM

ORM全称是:Object Relational Mapping(对象关系映射),其主要作用是在编程中,把面向对象的概念跟数据库中表的概念对应起来。举例来说就是,我定义一个对象,那就对应着一张表,这个对象的实例,就对应着表中的一条记录。

基本用法

package main

import (
	"fmt"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

type User struct {
	ID   int64
	Name string
}

func main() {
//省略了错误处理
	db, _ := gorm.Open(mysql.Open("root:******@tcp(127.0.0.1:3306)/hello"))
	var users []User
	_ = db.Select("id", "name").Find(&users, 1).Error
	fmt.Println(users)
}

这段代码和之前QuickStart的第一段代码意思相同,但简洁很多。测试如下:

image.png

基本用法-CRUD

gorm对数据库操作的一些基本用法,学过数据库的话应该看起来比较直观

//操作数据库
db.AutoMigrate(&Product{})
db.Migrator().CreateTable(&Product{})

//创建
user:=User{Name:"Jinzhu",Age:18,Birthday:time.Now()}
result:=db.Create(&user)

User,ID	//返回主键last insert id
result.Error//返回Error
result.RowsAffected//返回影响的行数

//批量创建
var users=[]User{{Name:"jinzhu1"},{Name:"jinzhu2"},{Name:"jinzhu3"}}
db.Create(&users)
db.CreateInBateches(users,100)

for _,user:=range users{
	user.ID//1,2,3
}

//读取
var product Product
db.First(&product,1)//查询id为1的product
db.First(&product,"code = ?","L1212")//查询code为L1212的prduct

result := db.Find(&users,[]int{1,2,3})
result.RowsAffected//返回找到的记录数

//更新某个字段
db.Model(&product).Update("Price",2000)
db.Model(&product).UpdataColumn("Price",2000)

//更新多个字段
db.Model(&product).Updates(Product{Price:2000,Code:"L1212"})
db.Model(&product).Updates(map[string]interface{}("Price":2000,"Code""L1212"))

//批量更新
db.Model(&Product{}).Where("price < ?",2000).Updates(map[string]interface{}{"Price":2000}
													 
//删除
db.Delete(&product)								

gorm.Model是gorm.io/gorm中自带的一个结构体,包含以下基本内容,定义结构体时可以很方便地将其嵌入其他结构体中

//gorm.io/gorm
type Model struct{
	ID		uint
	CreateAt	time.Time	`gorm:"primaryKey"`
	UpdateAt	time.Time
	DeleteAt	gorm.DeletedAt	`gorm:"index"`
}

type User struct{
	gorm.Model
	Id			uint
	Name		string
	Email		*string
	Age			uint8
	Birthday		*time.Time
	MemberNumber	sql.NullString
	ActivatedAt		sql.NullTime
}
惯例约定
  • 表名为struct name的snake_cases复数形式
    • 如struct User的表名就是users
  • 字段名为field name的snake_case单数格式
  • ID/Id字段为主键,如果为数字,则为自增主键
  • CreatedAt字段,创建时,保存当前时间
  • UpdatedAt字段,创建更新时,保存当前时间
  • gorm.DeletedAt字段,默认开启soft delete模式
关联

后面的部分有大量的代码,但没什么实践性的内容,大家可以访问GORM官方文档,包括我们课上的内容都有详细的介绍,这里就不把代码一一敲下来了 我觉得这些东西都是记是记不住的,只有多多实践才能掌握

03. GORM设计原理

image.png gorm处在应用程序和database/sql之间,为database/sql提供操作接口

SQL是怎么生成的

例句

db.Where("role<> ?","manager").Where("age>?",35).Limit(100).Order("age desc").Find(&user)

我们将where、order、Limit等等中间方法称为Chain Method,最后的Find方法称为Finisher Method,Chain Method给GORM添加一些自句,只有Finisher Method才能决定执行的类型

因为最后的方法是Find,所以我们要做的是一个select模式,同样,如果最后是一个create方法,那么我们对应的就是insert模式。我们了解Finisher Method之后,就可以生成SQL链 SELECT FROM _ WHERE _ ORDER BY _ LIMIT _

SELECT FROM User WHERE role <> "manager" AND age>35 ORDER BY age desc LIMIT 100