[ Database/Sql及Gorm的实现 | 青训营笔记 ]
标题:Database/Sql及Gorm的实现 - 掘金
标题 : 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、连接接口
- database/sql包定义的一个driver接口
// Driver 接口
tyep Driver interface{
// Open returns a new connection to the database.
Open(name string) (Conn, error)
}
- 注册全局 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
}
- 实现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 生成
扩展字句
扩展字句采取的是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
Prepare Stmt 在实现 ConnPool的接口时候,会进行如下三种操作
1.查找缓存的预编译SQL
2.未找到,将收到的SQL和Vars预编译
3.使用缓存的预编译SQL执行