Database/SQL 与 Gorm设计与实践 | 青训营笔记

532 阅读5分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第8篇笔记, 本次课主要讲了database/sql和 Gorm的用法.

一、database/sql

基本用法

注册Driver

一点小问题:

  • 上面连接中,连接信息是很长的字符串,有的时候字符串转义等会出现问题,为了解决这个问题,后来Go语言官方提供了一个新的方式连接
  • 新的方式使用结构体的方式连接

设计原理

DB连接的类型

  • 直接连接 / Conn
  • 预编译 / Stmt
  • 事务

处理返回数据的几种形式

  • Exec/ExecContext -> Result: 执行sql,可以获取是否成功,lastInsertId等信息
  • Query / QueryContext -> Rows (Columns): 将请求结果返回
  • QueryRow / QueryRowContext -> Row (Rows 简化)

具体实现

二、Gorm基础使用

基本用法

模型定义

软件设计范式:约定优于配置

  • 表名为struct name的snake_cases复数格式
  • 字段名为field name 的snake_case单数格式
  • ID/Id字段为主键,如果为数字,则为自增主键
  • CreatedAt 字段,创建时,保存当前时间
  • UpdatedAt字段,创建、更新时,保存当前时间
  • gorm.DeletedAt 字段,默认开启 soft delete模式

关联介绍

  • has one
  • has many
  • 多态 has many
  • ...

关联操作

Preload/Joins预加载

  • 一条sql不一定比三条sql性能好
  • 所以要测试preload和join的性能比较

级联删除

保障数据没有孤儿数据

  • 使用数据库约束
  • 使用 select 实现级联删除,不依赖数据库约数和软删除功能

三、Gorm设计原理

Gorm是在应用程序和database/sql之间的一层,操作sql的接口

SQL是怎么生成的

  • “找出35岁以上不是管理者的打工人”

  • Chain Method:可以添加字句的
  • Finisher Method: 决定类型 & 执行的方法

Chain Method实现

Finish Method 实现

为什么这么设计

  • 自定义Clause Builder
  • 方便扩展Clause
  • 自由选择 Clauses

不同数据库甚至不同版本支持的sql不同

  • 根据不同版本来实现

扩展子句

选择子句

\

插件是怎么工作的

Create的callback设计

修改callback

为什么要这么设计

完成灵活定制,自由扩展

  • 怎么支持多租户
  • 支持多数据库,读写分离
  • 加解密,混沌工程...

多租户

查询的时候往往加上id等过滤条件,希望查询的时候自动加上这些条件

多数据库,读写分离

ConnPool是什么

  • 重新定义了一个接口interface ConnPool 在Gorm和数据库之间

PrepareStmt模式:

  • 全局模式: 缓存不含参数部分,尽量不要拼接sql,一定要作为参数执行

    • 这里缓存会有容量限制,达到容量就无法缓存了
  • 会话模式:后续的会话操作都会预编译并缓存

  • 查找缓存的预编译 SQL
  • 未找到,将收到的SQL和Vars预编译
  • 使用缓存的预编译SQL执行

通过ConnPool接口更改下游数据库

利用ConnPool 开发缓存插件

通过一行配置提升性能

  • interpolateParams=false
  • 这里预编译用完就关掉了,导致利用率低
  • 这个为了解决多编码环境下的注入问题,一般情况下不会有这个情况
  • 可以试着关闭这个参数

字节内部封装了一些默认配置 bytedgorm

Dialector是什么

  • 不同语言的实现

功能

四、Gorm最佳实践

数据序列化与 SQL表达式

SQL表达式更新创建

  • gorm.Expr 使用sql表达式
  • 使用GormValue
  • 使用子查询SubQuery

查询

  • gorm.Expr
  • GormValue
  • 自定义查询SQL实现接口
  • SubQuery

数据序列化

  • 可以对密码加解密等

批量数据操作

创建

  • CreateInBatches(users, 100): 每一百条数据创建一次

查询

  • 避免把数据都加载到内存,避免oom
  • FindInBatches: 每100条查询数据,分批处理

更新

批量数据加速操作

  • 关闭默认事务
  • 默认批量导入会调用hooks方法,使用Skiphooks跳过
  • 使用Prepares Statement预编译sql
  • 混合使用, CreateBatchSize:默认使用批量插入,1000个一次

代码复用、分库分表、Sharding

代码复用

  • 把分页逻辑抽象出来,利用Scopes方法复用抽象的逻辑

分库分表

  • 根据数据选择分库分表,然后后面复用这个逻辑

Sharding

  • 创建订单时,userId是2,就会把数据插入到orders_2的表里

混沌工程/压测

插入数据时篡改一些数据,检查系统状态能不能发现

Logger/Trace

  • 全局模式
  • 会话模式

Migrator 数据库迁移管理

  • 自动迁移数据库
  • 版本管理数据库

  • 数据库迁移接口

  • 获取字段信息的接口

Raw SQL

  • Raw sql 不容易管理,改的时候容易忘

代码生成 Raw SQL - Gen

安全问题

  • 要以参数的形式输入,如果直接拼接会发生sql注入的事故

Q&A

  1. Gorm支持动态sql吗

    • 可以使用Scopes等实现
  2. 外键实现的场景

    • Gorm的外键和数据库的外键不一样,根据这个字段作为一个referenceId 辅助查询
  3. 修改数据库结构,可以更改Struct,然后AutoMigrate

  4. 取消点赞

    • 一般倾向于保留,添加一个状态实现,并且添加更改时间
  5. save和create方法区别

    • save使用前会判断主键是不是存在,如果存在,就调用update,不存在,就调用create
  6. gorm v1过渡gorm v2

    • 不能平滑过渡
    • 在做不同代码实现的时候,要考虑用户迁移成本
  7. 业务代码一般是通过model(和表对应的结构体)查询出来值,再赋值到返回给前端的响应体结构(vo)中吗

    • gorm有 smart select