[ Database/Sql及Gorm | 青训营笔记 ]

307 阅读5分钟

[ Database/Sql及Gorm的实现 | 青训营笔记 ]

标题:Database/Sql及Gorm的实现 - 掘金

网址:juejin.cn/course/byte…

标题 : Gorm设计原理 juejin.cn/course/byte…

一、理解 database/sql

1.1 设计原理

连接池配置

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

1、实现一个实际SQL的执行过程

下面这段操作代码就是一个操作过程的伪实现。

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)
    // validataConnection 有无错误
    // max life time, max idle conns 检查

    // 连接实现 driver.Queryer, driver.Execer等 interface
    if err == nil {
        err = dc.ci.Query(sql, args...)
    }
    isBadConn = errors.IS(err, driver.ErrBadConn)
    if !isBadConn {
        break
    }
}

调度的理论: cacm.acm.org/magazines/2…

2、连接接口

  1. database/sql包定义的一个driver接口
// Driver 接口
tyep Driver interface{
    // Open returns a new connection to the database.
    Open(name string) (Conn, error)
}
  1. 注册全局 driver
func Register(name string, driver driver.Driver) {
    driversMu.Lock()
    defer driversMu.Unlock()
    if driver == nil {
        panic("sql: Reigster driver is nil")
    }
    if _, dup := drivers[name]; dup {
        panic("sql: Register called twice for driver" + name)
    }
    drivers[name] = driver
}
  1. 实现driver
// 业务代码 
import _ "github.com/go-sql-driver/mysql"

func main() {
    db, err := sql.Open("mysql", "gorm:gorm@tcp(localhost:9910)/gorm?charset=utf8&parseTIme=True&loc=Local")
}
// github.com/go-sql-driver/mysql/driver.go
// 注册 Driver
func init() {
    sql.Register("mysql", &MySQLDriver{})
}

不过现在Golang出现了一个新的使用方法,如下代码:

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)
}

类比Java的使用方法

使用MySQL的是 com.mysql.jdbc.Driver包

import com.mysql.jdbc.Driver;
 
import java.sql.Connection;
import java.util.Properties;
 
public class Main {
    private static final String username = "root";
    private static final String password = "123456";
    private static final String url="jdbc:mysql://bar-mysql:3306/bar_baruser?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanxi";
    public static void main(String[] args) throws Exception {
        final Driver driver = new Driver();
        final Properties properties = new Properties();
        properties.put("user",username);
        properties.put("password",password);
        final Connection connect1 = driver.connect(url, properties);
        System.out.println(driver);
 
    }
}

3、操作接口

DB连接的几种类型:
TCP连接(直接连接/Conn)
PreparedStatement(预编译/Stmt):执行同样的SQL的时候,不需要传入原来的SQL,只需要发一下reference id,这样的话可以减少网络传输的时间。
事务/Tx

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

二、GORM 使用简介

GORM:设计简洁、功能强大、自有扩展的全功能ORM

ORM:对象关系映射,用于实现面向对象编程语言里不同类型系统的数据之间的转换

设计原则: API简洁、测试优先、最小惊讶、灵活扩展、无依赖 可信赖

功能完善:
关联: 一对一、一对多、单表自关联、多态;Preload、Joins预加载、级联删除;关联模式;自定义关联表
事务:事务代码块、嵌套事务、Save Point
多数据库、读写分离、命名参数、Map、子查询、分组条件、代码共享、SQL表达式(查询、创建、更新)、自动选字段、查询优化器
字段权限、软删除、批量数据处理、Prepared Stmt、自定义类型、命名策略、虚拟字段、自动track时间、SQL Builder、Logger
代码生成、复合主键、Constraint、Prometheus、Auto Migration、真·跨数据库兼容...
多模式灵活自由扩展
Developer Friendly

2.1 基本用法

使用gorm连接mysql

import (
	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/mysql"
)

func main() {
	db, err := gorm.Open("mysql", "root:123456@(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local")
	if err != nil {
		panic(err)
	}
	defer db.Close()
}

通过对比1.1里面使用driver来实现SQL语句,代码无疑是简单、简洁许多。

CRUD

CRUD(Create、Read、Update、Delete),操作数据库的增删改查操作

Create操作

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

// 创建
user := User{Name: "LiuXin", Age: 18, Birthday: time.Now()}
result := db.Create(&user) //pass pointer of data to Create

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

// 批量操作
var users = []User{{Name: "LiuXin1"},{Name: "LiuXin2"}, {Name: "LiuXin3"}}
db.Create(&users)
db.CreateInBatches(users, 100)

for _, user := range users{
    user.ID 
}

Read、Update、Delete操作

type UserInfo struct {
	Id     uint
	Name   string
	Gender string
	Hobby  string
}
	//创建表 自动迁移 (把结构体和数据库表进行对应)
	db.AutoMigrate(&UserInfo{})

	//创建数据行
	u1 := UserInfo{Id: 1, Name: "zyj", Gender: "男", Hobby: "唱"}
	db.Create(&u1)
	//查询
	var u UserInfo
	db.First(&u)
	fmt.Println(u)
	//更新
	db.Model(&u).Update("hobby", "唱跳rap篮球")
	//删除
	db.Delete(&u) //将查询出来的第一条数据删除

2.2 模型定义

Gorm的模型定义格式如下

type User struct{
    ID      uint
    Name    string
    Email   *string
    Age     uint
    Birthday *time.Time
    MemebrNumber sql.NullString
    ActivatedAt  sql.Nulltime
    CreatedAt    time.Time
    UpdatedAt    time.Time
    DeletedAt    gorm.DeletedAt `gorm:"index"`
}

约定优于配置

表名为 struct name 的 snake_cases 复数格式

字段名为 field name 的 snake_case 单数格式

ID/id 字段为主键,如果为数字,则为自增主键

CreatedAt 字段,创建时,保存当前时间

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

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

一切皆可配置: gorm.io/docs/conven…

2.3 R(Relation)关联介绍

Gorm可以好多好多关联的支持(One to One[一对一]、Belongs To[属于]、Has one[拥有]、Has Many[一对多]、Many To Many[多对多])

一对一(Belongs to)


// `User` 属于 `Company`,`CompanyID` 是外键
type User struct {
	gorm.Model
	Name      string
	CompanyID int // 默认情况下, CompanyID 被隐含地用来在 User 和 Company 之间创建一个外键关系
	Company   Company
}

type Company struct {
	ID   int
	Name string
}

func main() {
	//user 里面有 company表的结构 所以只需要自动迁移user表即可
	db.AutoMigrate(&User{})
}

三、GORM 设计原理

3.1 SQL 生成

1.png

扩展字句

扩展字句采取的是gorm.io包下的hints包

import "gorm.io/hints"

db.Clauses(hints.New("MRR(idx1)")).Find(&User{}) // 扩展 SELECT Clause 后

// 扩展 FROM Clause 后
db.Clauses(hints.UseIndex("idx_user_name")).Find(&User{}) 
db.Clauses(hints.ForceIndex("idx_user_name", "idx_user_id").ForJoin()).Find(&User{})

// 自由扩展 Clause 前中后
db.Clauses(hints.Comment("select", "master")).Find(&User{})
db.Clauses(hints.CommentBefore("insert", "node2")).Create(&user)

3.2 插件

注册Callback

db.Callback().Create().Register("myplugin", func(*gorm.DB) {})

删除Callback

db.Callback().Create().Remove("gorm:begin_transaction")

替换Callback

db.Callback().Create().Replace("gorm:before_create", func(*gorm.DB) {})

查询注册的Callback

db.Callback().Create().Get("gorm:create")
插件系统工作时的灵活定制以及自由扩展的三个例子
多租户;多数据库、读写分离;加解密、混沌工程...

1、多租户

// 根据TenantID 过滤

func setTenantScope(db *gorm.DB) {
    if tenantID, err := getTenantID(db.Statement.Context); err != nil {
        db.Where("tenant_id = ?", tenantID)
    } else {
        db.AddError(err)
    }
}
db.Callback.Query().Before("gorm:query").Register("ser_tenant_scope", setTenantScope)

db.Callback.Delete().Before("gorm:delete").Register("ser_tenant_scope", setTenantScope)

db.Callback.Update().Before("gorm:update").Register("ser_tenant_scope", setTenantScope)

// 设置 TenantID
func setTenantID(db *gorm.DB) {
    tenantID,err := getTenantID(db.Statement.Context)
    db.Statement.SetColumn("tenant_id", tenantID)
}
db.Callback.Create().Before("gorm:create").Register("set_tenant_scope",setTenantID)

2、多数据库、读写分离

DB.Use(dbresolver.Register(dbresolver.Config{
    // db2 作为主数据库 db3 db4 作为从数据库
    Sources: []gorm.Dialector{ mysql.Open("db2_dsn")},
    Replicas: []gorm.Dialector{ mysql.Open("db3_dsn"),
    mysql.Open("db4_dsn")},
    // sources/replicas 负载均衡策略
    Policy: dbresolver.RandomPolicy{},
}).Register(dbresolver.Config{
    // db1 作为 主数据库,对于 User、Address使用 db5 作为从数据库
    Replicas: []gorm.Dialector{ mysql.Open("db5_dsn")},
},&User{}, &Address{}).Register(dbresolver.Config{
    // db6, db7作为主数据库,对于 orders、Product 使用 db8 作为从数据库
    Sources: []gorm.Dialector{ mysql.Open("db6_dsn"),mysql.Open("db7_dsn")},
    Replicas: []gorm.Dialector{ mysql.Open("db8_dsn")},
},"orders",&Product{}, "secondary"))

// 使用 Write 模式: 从Sources db `db1` 读取user
DB.Clauses(dbresolver.Write).First(&user)

3.3 ConnPool

数据库连接池ConnectPool

2.png

Prepare Stmt 在实现 ConnPool的接口时候,会进行如下三种操作

1.查找缓存的预编译SQL

2.未找到,将收到的SQL和Vars预编译

3.使用缓存的预编译SQL执行

3.4 Dialector 是什么

3.png