设计模式之database与GORM实战| 青训营笔记

441 阅读6分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第5篇笔记

01. 理解database/sql

基本用法

_ "github.com/go-sql-driver/mysql",这个我们必须加,没有编译检查,但是运行就会报错了 为什么要加这个呢,因为我们可以用不上里面的方法,但是我们可能需要用到里面的初始化方法,所以这个是不可或缺的。

 package main
 ​
 import (
     "database/sql"
     "fmt"
     _ "github.com/go-sql-driver/mysql"
 )
 ​
 type User struct {
     Id int64
     Name string
 }
 ​
 func main()  {
     db, err := sql.Open("mysql","username:password@tcp(127.0.0.1:3306)/数据库名")
     if err != nil {
         //....
     }
     rows, err := db.Query("select * from `user`")
     if err != nil {
         //....
     }
     defer func() {
         err := rows.Close()
         if err != nil {
             //....
         }
     }()
     var users []User
     for rows.Next() {
         var user User
         err := rows.Scan(&user.Id,&user.Name)
         if err != nil {
             //....
         }
         users = append(users,user)
     }
     if rows.Err() !=  nil {
         //....
     }
     for _, user := range users {
         fmt.Println(user)
     }
 }
 defer func() {
     err := rows.Close()
     if err != nil {
         //。。。。
     }
 }()

这个关闭可以会出现错误,要处理

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

这个异常也要记得处理

设计原理

应用程序 操作接口 database/sql (连接池) 连接接口、操作接口 数据库

连接池

 type DB struct {
     // Atomic access only. At top of struct to prevent mis-alignment
     // on 32-bit platforms. Of type time.Duration.
     waitDuration int64 // Total time waited for new connections.
 ​
     connector driver.Connector
     // numClosed is an atomic counter which represents a total number of
     // closed connections. Stmt.openStmt checks it before cleaning closed
     // connections in Stmt.css.
     numClosed uint64
 ​
     mu           sync.Mutex    // protects following fields
     freeConn     []*driverConn // free connections ordered by returnedAt oldest to newest
     connRequests map[uint64]chan connRequest
     nextRequest  uint64 // Next key to use in connRequests.
     numOpen      int    // number of opened and pending open connections
     // Used to signal the need for new connections
     // a goroutine running connectionOpener() reads on this chan and
     // maybeOpenNewConnections sends on the chan (one send per needed connection)
     // It is closed during db.Close(). The close tells the connectionOpener
     // goroutine to exit.
     openerCh          chan struct{}
     closed            bool
     dep               map[finalCloser]depSet
     lastPut           map[*driverConn]string // stacktrace of last conn's put; debug only
     maxIdleCount      int                    // zero means defaultMaxIdleConns; negative means 0
     maxOpen           int                    // <= 0 means unlimited
     maxLifetime       time.Duration          // maximum amount of time a connection may be reused
     maxIdleTime       time.Duration          // maximum amount of time a connection may be idle before being closed
     cleanerCh         chan struct{}
     waitCount         int64 // Total number of connections waited for.
     maxIdleClosed     int64 // Total number of connections closed due to idle count.
     maxIdleTimeClosed int64 // Total number of connections closed due to idle time.
     maxLifetimeClosed int64 // Total number of connections closed due to max connection lifetime limit.
 ​
     stop func() // stop cancels the connection opener.
 }

重点了解一下下面的就行了

 // 设置连接可以被重用的最大时间量。 
 func (db *DB) SetConnMaxLifetime(d time.Duration) 
 //设置连接空闲的最大时间
 func (db *DB) SetConnMaxIdleTime(d time.Duration)
 //设置空闲连接池中的最大连接数。
 func (db *DB) SetMaxIdleConns(n int)
 //设置打开到数据库的最大连接数。
 func (db *DB) SetMaxOpenConns(n int)
 func (db *DB) Stats() DBStats

sql执行的一个过程

  • 这个重点说了

下面: maxBadConnRetries 等于2的,driver.ErrBadConn失败会再尝试操作一次

 for i := 0; i < maxBadConnRetries; i++ {
     //从连接池获取连接或通过driver新建连接
     dc, err := db.conn(ctx,strategy)
     
     //将连接放回连接池
     defer dc.db.putConn(dc,err,true)
     
     //连接实现
     if err == nil {
         err = dc.ci.Query(sql,args...)
     }
     isBadConn = errors.Is(err,driver.ErrBadConn)
     if !isBadConn {
         break
     }
 }

这里会让我们数据库出现“重复插入的情况”。我们出现了一些网络波动,导致连接失败,我们再次重试就再插入一次了,就出问题了。

driver

  • 提供了一个接口,这样就可以适配不同的数据库了
  • 通过Register全局注册:drivers[name] = driver

设计缺点:

  • import没有编译检查

  • DSN通过字符串传会受限

    • 提到了sqlserver的账号密码
  • 然后就是这个名字,别的导入起名啥的不能和它重名


后来也解决了上面的问题,只是出来的太晚了,很多时候大家也没介绍这个

DB连接的几种类型

DB连接的几种类型:直接连接/Conn 预编译/Stmt 事务/Tx

这个预编译生成PrepareStatement(PrepareStatementId获取),当出现相同的查询语句的时候我们不需要重新生成PrepareStatement,传一个id就行了,这样可以减少网络传输的时间,减少数据库解析同样sql的时间。

返回数据的几种方式

处理返回数据的几种方式: Exec/ExecContext => Result Query/Querytext -> Rows(Columns) QueryRow/QueryRowContext -> Row(Rows简化)

这个Rows简化我们不需要关闭。

02. GORM使用简介

背景知识

"设计简洁、功能强大、自由扩展的全功能ORM" 设计原则:API简洁、测试优先、最小惊讶、灵活扩展、无依赖、可信赖

功能完善:关联、事务、多数据源、读写分离。。。。

关联中的多态:www.cnblogs.com/mingxuanton…

基本使用

 package main
 ​
 import (
     "fmt"
     "gorm.io/driver/mysql"
     "gorm.io/gorm"
 )
 ​
 type User struct {
     Id int64
     Name string
 }
 ​
 // TableName 通过这个指定表名 *User和User绑定都行
 func (*User) TableName() string {
     return "user"
 }
 ​
 func main() {
     db, err := gorm.Open(
         mysql.Open("username:password@tcp(127.0.0.1:3306)/datasourceName"),
     )
     if err != nil {
         fmt.Println(err)
         return
     }
     var users []User
     err = db.Select("*").Find(&users,).Error
     if err != nil {
         fmt.Println(err)
         return
     }
     fmt.Println(users)
 }

CRUD

db.AutoMigrate(&User{})
  • 文章参考://davidchan0519.github.io/2019/05/06/…
  • 比如我们User新增了字段,我们使用这个进行刷新,但是我们删除了一个字段,刷新之后删除的字段还在
db.Migrator().CreateTable(&User{

})

创建表格,表格字段和user里面的字段一致

db.Create(&user)//新增,如果id是自增的,我们插入数据之后会返回id到我们的user里面
fmt.Println(user.Id)
fmt.Println(res.Error)//返回error
fmt.Println(res.RowsAffected)//返回影响的行数
db.Create(&users)//传切片的地址和直接传切片都行的
db.CreateInBatches(users,len(users))//第二个参数是插入的数量

批量创建 CreateInBatches:CreateInBatches会根据batchSize来分配进行create,但是他们是在同一个事务的,其rowsAffected是每个批次的rowsAffected的累加

var user User
db.First(&user,1)//查询id为1的user
fmt.Println(user)
user.Id = 0
db.First(&user,"name = ?","1111")
fmt.Println(user)
var users []User
res := db.Find(&users,[]int{1,2,3})
fmt.Println(res.RowsAffected)//返回找s的记录数
is := errors.Is(res.Error,gorm.ErrRecordNotFound) //First,Last,Take查找不到数据
fmt.Println(is)
fmt.Println(users)

更新

//更新某个字段
res := db.Model(&User{}).Where("name","1111").Update("name", "随风")
fmt.Println(res.RowsAffected)
//Where要在UpdateColumn前面
res := db.Model(&User{}).Where("name","222").UpdateColumn("name","哈哈哈")

还有批量更新等等

删除

模型定义

约定优于配置

gorm.io/zh_CN/docs/…

  • 表名复数

    • 例子:type User struct {},默认表名:users
    • 我们可以给User绑定func (User) TableName() string,返回自定义表名
  • 字段名单数

    • 例子:name string,对应数据库字段:name
  • ID/Id字段为主键,如果为数字则为自增主键

    • 我们也可以自定义一下:UUID   string gorm:"primaryKey"
  • CreatedAt字段,创建时保存当前时间

  • UpdatedAt字段,创建和更新时,保存当前时间

  • gorm.DeletedAt字段,默认开启soft delete模式

    • DeletedAt gorm.DeletedAt gorm:"index"

惯例约定

gorm.Model里面包含的东西: ID uint gorm:"primarykey" CreatedAt time.Time UpdatedAt time.Time DeletedAt DeletedAt gorm:"index"

关联操作

  • gorm.io/zh_CN/docs/…
  • 我们平时需要进行开发的时候,忘记了一些知识点可以直接到这个官方文档里面进行查看。

介绍

本质就是提供一组函数来帮助我们快速拼接 sql 语句,尽量减少 sql 的编写,关联操作也可以减少我们繁琐的sql拼接。 通过关联操作,我们可以快速进行表间的关联操作。

CRUD

通过文档的关联操作,我们可以进行表查询的关联,删除关联等。

Preload/Joins预加载

preload提升了资源加载的优先级,使得它提前开始加载(预加载),在需要用的时候能够更快的使用上。

需要注意的是:Joins只能用于一对一关联中,即HasOne和BelongTo关联

联级删除

方法1:使用数据库约束自动删除

放法2:使用Select实现级联删除,不依赖数据库约束和逻辑删除

03. GORM设计原理

应用程序 => GORM =操作接口=> database/sql =连接接口/操作接口=> 数据库

PS:上这课的时候完全没接触过GORM,所以这个设计原理是真的听不懂。

04. GORM最佳实践

1、数据序列化与SQL表达式

2、批量数据操作

3、代码复用、分库分表

4、混沌工程/压测

5、Logger/Trace

6、Migrator

7、Gen代码生成/Raw SQL

8、安全

安全要求我们防止SQL注入,要怎么做到这一点呢。我们使用GORM提供的函数的时候以参数形式传入(比如将参数绑定到实体对象里面再传入),不要直接使用类似fmt.Sprintf这种函数进行SQL拼接

随记,要是想详细学习GORM可以参照官方文档进行学习