设计模式之 Database/SQL 与 GORM 实践 | 青训营笔记

236 阅读8分钟

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

1 理解database/sql

关系型数据库:做一些持久存储数据库时的首要选择。关系型数据库其实有很多种类,go的标准库database/sql包,为了访问这种关系型数据库提供了通用的协议接口,这样的话不同数据库只要实现了标准库的接口,就可以使用统一的方法去管理操作数据库。

1.1 基本用法

怎么实现连接,怎么去查询数据,怎么把数据scan到对象中去,怎么去处理其中一些错误. database/sql包只是去实现一个统一的接口,具体怎么去连接我们的数据库、怎么去解析数据库的部分它都不管。这里是不同的数据库自己去实现自己的一些driver。

第4行import了一个mysql的driver,这个driver实现了怎么去连接数据库、怎么去执行查询、怎么去解析数据库返回的一些内容,然后基于driver的实现DSN(数据库的连接)也就是第8行。关于DSN的知识可以看图上下面两个链接。driver会把这个sql发送给数据库。释放连接,否则会造成泄漏。 处理返回的数据,相关数据scan到user的字段中去,把结果append到user当中,Next()会不断获取下一条数据,其实一般来说在最后它会自动去Close掉rows,加defer close是担心在scan过程中有异常或者遇到错误提前return或者panic,造成row泄漏,导致服务被卡死。

如果后续遇到这种,服务卡死、数据库请求被停住、没有响应的时候都可以看一下是不是rows忘记关闭了、事务忘记commit或者忘记去rollback了,都可能会导致这样的问题。

当然通过Next去Close掉rows还会有一个问题,在rows.Close过程中其实是可能会发生错误的,但是通过Next去close掉rows,错误信息会丢失。理论上需要去进行一个错误检查. 容易漏掉的错误rows.Err() 一定要处理 只要不是数据相关的错误都会通过这一个Err信息去返回,常规的文档不太会去说这些地方。 注意这里是MySQL的driver,可以替换成其他driver去实现,可以支持不一样的数据库。

1.2 设计原理

采用极简接口设计原则,对下层暴露一些简单的驱动接口,在database/sql包内部实现连接池的管理。这意味着支持不一样的数据库,只需要去实现相同的连接接口。

连接池涉及到池化技术,把昂贵、费时的资源维护到一个特定的池子里,去做一些配置。 最重要的是连接池,占了大部分代码.maxOpen, maxLifetime等常见参数 默认BadRetries是两次,也就是默认重试两次,获取到一个连接时也会有两种策略,一种是尽量呼应,另一种是新建一个新的连接,在重试的前提下都会去复用连接池原来的一些连接,如果说前面的连接都出错了,就会在最后一次去强制去建一个新的连接。

defer操作,整个操作之后把连接放回连接池,有校验操作,看连接池状态有没有满足当前需求,如果满足需求就会把这个连接放回连接池,如果没有可能就直接把这个连接丢弃掉。

然后我们就会使用这个连接操作数据操作一些接口,具体去执行这段sql。

策略优化:(谷歌的调度优化实现,降低延时) 前面说了Database/sql包的整体流程以及连接池,接下来看一下它是怎么预留的接口.实现了一个register方法,可以把你的driver去注册到一个全局变量中去,这里是往drivers[name]里去写了一个值,这就是go对注册driver的一个实现。 注册一个driver以后怎么去用:首先在业务代码里需要去import一下这个driver,然后再通过main方法这个方式去建立一个连接。

这个设计有什么问题?首先DFN是特别特别长的一个字符串,很难去了解到它具体是什么意思,然后大家可能对参数都不是特别的懂,有些参数可能都不能通过字符串的方式去传进来,做字符串转义也是特别难。还会有一个问题就是经常性地会去忘记import driver,在import过程中其实是没有编译检查的,如果忘记Import只会有一个运行时的异常。 新的接口:传入一个interface,经过这个interface可以返回一个DB 通过这种方式创建连接,避免后续一些问题。也不会忘记去import因为有编译检查。

看完连接接口再来看操作接口:直连连接(TCP连接);预编译,执行同样的sql的时候会先去prepare一下,生成一个Prepare statement,根据其reference ID再去进行后面一些执行过程。比如说在后面执行同样的一个sql的时候就不需要把原来的sql传过去,只需要去查一下reference ID,减少网络传输时间,也可以减少数据库sql解析的时间,提升性能。 exec 数据处理结果,rows数据库请求后的结果,row只会读取一行数据,读取完自动close掉,不用手动close。 driver通过实现上面这个interface,通过mysql的协议连接去做解析实现。返回值通过next方法,把数据库返回值放到目标值里去。目标值也会根据上面的操作的一些接口返回到我们的应用程序的结果当中 最终就可以在应用程序里获取相关的一些值操作。

2 Gorm基础使用

2.1 背景知识

什么是gorm? orm(Object Relational Mapping)对象关系映射

2.2 基本用法

gorm的简单使用方法和前面代码的差异。通过grom可以避免去import driver,也会避免去close掉rows,err检查上也会简单很多。

2.3 模型定义

这个包还提供了value和scanner的一些接口,通过这些接口可以去处理稍微复杂一些的逻辑。 这里通过struct嵌套gorm.Model,嵌套的话会把定义平铺到上面去。

模型和数据库表示怎么对应?字段和column对应关系是什么?虽然说约定优于配置,但其实配置都可以去修改的.

2.4 关联操作

对于ORM,O代表object,R代表relation,也就是下面要讲的内容。

展示一下gorm关联支持的类型.User拥有一个Account,这是一个has one的关系,还有多个pets,这是一个has many的关系,还有好多toys,这是一个多态的has many,属于某个公司是belongs to 的关系,属于某个manager是单表belongs to Save保存关联。关联模式也支持批量操作 查询数据时查关联也是特别常见的操作,gorm也提供一些预加载的支持。这样可以避免查询一些用户的时候,每个用户都需要去查一下它相关的关联,这样可能会产生(N+1)SQL的操作。提供两种方式。

Preload触发另外一条sql,查询用户时会预加载一下它的包以及profile,然后发了三条sql把所有用户的profile取出来;join通过一条sql查出相关的关联。

这里有一个误区,一条sql不一定比三条sql性能更好,上面可能会有一些缓存操作对加速有好处。实际根据场景看。 级联删除: 保证数据没有孤儿数据。

数据库约束:使用数据库的能力,如果删除用户,自动把用户相关的关联去全部删掉。但有些公司可能不允许使用这种约束形式。

select方式,不依赖数据库约束

3 Gorm设计原理

sql怎么生成,插件怎么工作

怎么通过一行配置迅速提升服务性能? gorm其实就相当于在我们得database/sql包上面再加上一层,这一层负责和应用程序进行交互。 chain method:给gorm statement添加子句的方法,这些子句都会用来生成最终的一个sql,这里可以看到where()用于添加where相关的子句、Limit()用来添加limit相关的子句。 以前两个例子说明插件是怎么工作的。

这里是给query delete以及update去注册一个新的callback,从当前gorm statement的context里取出当前的用户ID,然后把当前用户id通过一个where条件去更新到gorm statement条件里去,也就是说后续所有的查询、更新、delete都会加上当前的过滤条件,这样就不可能会发生忘记去复制条件导致取出更新其他用户的事故。

create时也要做类似操作,把当前租户ID给当前创建的值去赋一下值,这样保证插入每条数据都有租户ID的绑定关系,避免越权操作