您的位置:首页 >聚焦 >

探究 Go database/sql 包的设计模式

2022-11-01 10:02:21    来源:程序员客栈

使用 Go 中的 SQL database 是容易的,只需下列这三步:

//步骤 1:导入主要的 SQL 包import"database/sql"//步骤 2:导入一个驱动包来明确要使用的 SQL 数据库import_"github.com/mattn/go-sqlite3"//步骤 3:用一个注册好的驱动名称来打开一个数据库funcmain(){//...db,err:=sql.Open("sqlite3","database.db")//...}

从这时候开始,对象db可以用相同的代码来查询和修改所有支持的 SQL 数据库。如果我们想从 SQLite 转到 PostgreSQL,类似的做法只需要导入另个一数据库的驱动包,以及在调用sql.Open[1]时传入另一个驱动名称。


【资料图】

在这篇博客里,我想简述一些database/sql背后的设计模式及架构。

主要的设计模式

database\sql的架构受一个整体的设计模式制约。我尝试分析它可能是哪个经典的设计模式,然后策略模式看起来比较接近,尽管它不是那么的一致。如果你认为哪个设计模式更符合,请告诉我 [2]。

它看起来像这样:我们有一个想要呈现给用户的通用接口,并有一个针对每个数据库后端的实现。很显然,它听起来很像经典的接口+实现,Go在这方面特别擅长,它对接口的支持很强大。

所以第一个想法会是:创建一些用户会交互的DB接口,并且每个数据库后端都实现这些接口。听起来是不是很简单?

当然,但使用这个方法会有一些问题。记住的是 Go 建议接口尽量小,也就是实现较少的方法。这里我们需要较大一点的DB接口,而这导致了一些问题:

添加面向用户的能力是很困难的,因为他们可能需要对接口添加额外的方法。这个破坏了所有接口的实现,并且需要许多独立的项目去维护它们的代码。

让所有数据库后端封装相同的方法是困难的,因为如果用户想直接加方法到DB接口,是没有一个原生(不能直接修改接口的方法)的地方去添加的。它需要每个后端独立地实现,这是很浪费的,逻辑也是非常复杂的。

如果后端想增加可选的能力,对一个单一的接口来说,不为特定的后端采用类型转换,这是具有挑战性的。

因此,一个更好的想法应该像这样:从后端接口分离出面向用户的类型及方法。如图所示:

DB是一个面向用户类型。不是一个接口,而是一个在database/sql包内实现的一个*具体*类型(一个结构)。它是与后端是分隔开的,封装了许多后端通用的功能,就像连接池。

为了做后端特定的工作(比如向实际的数据库发出 SQL 查询),DB使用一个叫做database/sql/driver的接口。Driver(以及其他几个定义连接、事务等的接口)。这个接口是低级别的,它由每个数据库后端实现。在上图中,我们看到pq包的实现 (PostreSQL 的实现) 以及sqlit3包的实现。

这个方法优雅地帮助database/sql解决了在前面提到的问题:

现在增加面向用户的能力不一定需要改变接口,只要该能力可以在独立于后端(DB和它的姐妹类型)中实现即可。

所有数据库后端共有的功能现在有了一个可以修改的位置。虽然我在前面提到了连接池,但是database/sql中独立于后端的类型在后端特定实现的基础上增加了很多其他的东西。另一个例子:处理与数据库服务器的错误连接的重试。

如果后端增加了可选的能力,这些能力可以在独立于后端的层中被选择性地利用,而不用直接暴露给用户。

注册驱动

database/sql的设计另一个有趣的方面是数据库驱动如何自己注册到 main 包里面。这是一个在 Go 中实现*编译时插件*的好例子。

正如本博客顶部的代码示例所示,database/sql知道导入的驱动程序的名称,并且可以用sql.Open按名称打开它们。它是如何实现的呢?

诀窍就在空白引用:

import_"github.com/mattn/go-sqlite3"

虽然它实际上没有从包中导入任何名字,但它调用了它的init函数,对于sqlite3来说,就是:

funcinit(){sql.Register("sqlite3",&SQLiteDriver{})}

在sql.go中,Register添加了一个从字符串名称到driver.Driver接口实现的映射;该映射在一个全局映射中:

var(driversMusync.RWMutexdrivers=make(map[string]driver.Driver))//Registermakesadatabasedriveravailablebytheprovidedname.//IfRegisteriscalledtwicewiththesamenameorifdriverisnil,//itpanics.funcRegister(namestring,driverdriver.Driver){driversMu.Lock()deferdriversMu.Unlock()ifdriver==nil{panic("sql:Registerdriverisnil")}if_,dup:=drivers[name];dup{panic("sql:Registercalledtwicefordriver"+name)}drivers[name]=driver}

当sql.Open被调用时,它在drivers映射中查找名称,然后会实例化一个DB对象,并附加适当的驱动实现。你也可以在任何时候调用sql.Drivers函数来获取所有注册的驱动的名称。

这个方法实现了一个*编译时*的插件,因为包含的后端的import是在 Go 代码编译时发生的。二进制文件中构建了一套固定的数据库驱动程序。Go 也支持运行时的插件,但是这是另一篇的主题。

实现Scanner接口的自定义类型

database/sql包的另一个有趣的结构特征是支持数据库中自定义类型的存储和检索。Rows.Scan方法通常用于从行中读取列。它把一连串的interface{}当作泛型,内部通过类型开关,根据一个参数的类型选择正确的 reader。

为了定制化,Rows.Scan支持实现了sql.Scanner接口的类型,然后调用它们Scan方法来执行实际的数据读取操作。

一个内建例子是sql.NullString。如果我们尝试Scan一个列到一个string参数:

varidintvarusernamestringerr=rows.Scan(&id,&username)

然后那个列有一个NULL的值,我们会得到一个错误:

sql:Scanerroroncolumnindex1,name"username":unsupportedScan,storingdriver.Valuetypeintotype*string

我们可以通过使用sql.NullString来避免:

varidintvarusernamesql.NullStringerr=rows.Scan(&id,&username)

在这里,username将把它的Valid字段设置为false,因为它是NULL列。这个能够实现的原因是NullString实现了Scanner接口。

一个更有趣的例子涉及到某些数据库后端特有的类型。举个例子,虽然 PostgrSQL 支持数组类型,但其他一些数据库(如 SQLite )并不支持。所以database/sql不能原生支持数组类型,但是像Scanner接口这样的功能使得用户代码可以相较容易地与这些数据进行交互。

为了扩展前面的例子,假设我们的行也有每个用户[3]的 activities(作为字符串)。那么Scan就会像这样:

varidintvarusernamesql.NullStringvaractivities[]stringerr=rows.Scan(&id,&username,pq.Array(&activities))

pq.Array函数是由pq PostgreSQL 绑定提供。它接收一个分片,并将其转换为一个匿名类型,实现sql.Scanner接口。

这是一个很好的方法,在必要时可以*摆脱抽象*。尽管有一个统一的接口来访问许多种数据库是很好的,但有时我们确实想使用一个特定的数据库,并具有其特定的功能。在这种情况下,放弃database/sql是很可惜的,不过我们也不必这样 - 因为这些功能让特定的数据库后端提供了自定义行为。


[1]当然,假设我们在查询中只使用两个数据库,且都支持的标准SQL语法。

[2]我第一次在最近加入的Go CDK 项目中遇到了关于这种模式的明确讨论。Go CDK 对其可移植类型使用了类似的方法,其设计文档称其为*可移植类型和驱动模式*。

[3]我意识到,多值字段并不是好的关系设计。这只是一个例子。

相关链接:

[1]:https://eli.thegreenplace.net/2019/design-patterns-in-gos-databasesql-package/#sql1

[2]:https://eli.thegreenplace.net/2019/design-patterns-in-gos-databasesql-package/#footnote-reference-2

[3]:https://eli.thegreenplace.net/2019/design-patterns-in-gos-databasesql-package/#footnote-reference-3000

策略模式:https://en.wikipedia.org/wiki/Strategy_pattern

连接池:https://en.wikipedia.org/wiki/Connection_pool数组类型:https://www.postgresql.org/docs/9.1/arrays.htmlpq PostgreSQL 绑定:https://godoc.org/github.com/lib/pqGo CDK 项目:https://github.com/google/go-cloud设计文档:https://github.com/google/go-cloud/blob/master/internal/docs/design.md#portable-types-and-drivers原文地址:https://eli.thegreenplace.net/2019/design-patterns-in-gos-databasesql-package/原文作者:Eli Bendersky本文永久链接:https://github.com/gocn/translator/blob/master/2022/w44_Design_patterns_in_Go_databasesql_package.md译者:zxmfke

校对:----


2022 GopherChina大会报名火热进行中!

扫描下方二维码即可报名参与哦~

大会合作、现场招聘及企业购票等事宜请联系微信:18516100522

戳这里 Go !

关键词: 设计模式 数组类型 面向用户的

相关阅读