这是我参与「第三届青训营 -后端场」笔记创作活动的的3篇笔记。介绍了database/sql与GORM的一些原理和操作,以及在本地尝试的一些实现,并附上了一些网上的文档,希望能帮助同学们。
这节课内容属实挺难的,前半部分还好,磕磕绊绊还能听懂,后面实在顶不住了,就没有记下去。建议大家参考其他巨佬的笔记。咱实事求是,确实是超出能力范围了,这些真的是我这种基础班菜鸡能听的吗。 我之前的话只简单上课学过一点数据库的知识,几乎没实践过,又花了一两个小时速成了一下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;
测试结果:
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的第一段代码意思相同,但简洁很多。测试如下:
基本用法-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设计原理
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