Godatabasesql⽂档
Go database/sql⽂档
No.1 ⽂档概要
为什么需要这个?包⽂档告诉你每件事情都做了什么,但它并没有告诉你如何使⽤这个包。我们很多⼈都希望⾃⼰能快速参考和⼊门的⽅法,⽽不是讲故事。欢迎捐款;请在这⾥发送请求。
⾸先你应该知道⼀个sql.DB不是⼀个数据库的连接。它也没有映射到任何特点数据库软件的“数据库”或“模式”的概念。它是数据库的接⼝和数据库的抽象,它可能与本地⽂件不同,可以通过⽹络连接访问,也可以在内存和进程中访问。
sql.DB为你在幕后执⾏⼀些重要的任务:
· 通过驱动程序打开和关闭实际的底层数据库的连接。
· 它根据需要管理⼀个连接池,这可能是如上所述的各种各样的事情。
sql.DB抽象旨在让你不必担⼼如何管理对基础数据存储的并发访问。⼀个连接在使⽤它执⾏任务时被标
记为可⽤,然后当它不在使⽤时返回到可⽤的池中。这样的后果之⼀是,如果你⽆法将连接释放到池中,则可能导致db.SQL打开⼤量连接,可能会耗尽资源(连接太多,打开的⽂件句柄太多,缺少可⽤⽹络端⼝等)。稍后我们将进⼀步讨论这个问题。
在创建sql.DB之后,你可以⽤它来查询它所代表的数据库,以及创建语句和事务。
No.2 导⼊数据库驱动
在本⽂档中,我们将使⽤@julienschmidt 和 @arnehormann中优秀的MySql驱动。
将以下内容添加到Go源⽂件的顶部(也就是package name下⾯):
import (
"database/sql"
_ "github/go-sql-driver/mysql"
)
现在你已经准备好访问数据库了。
No.3 访问数据库
现在你已经加载了驱动包,就可以创建⼀个数据库对象sql.DB。创建⼀个sql.DB你可以使⽤sql.Open()。Open返回⼀个*sql.DB。
func main() {
db, err := sql.Open("mysql",
"user:password@tcp(127.0.0.1:3306)/hello")
if err != nil {
log.Fatal(err)
}
defer db.Close()
}
在⽰例中,我们演⽰了⼏件事:
1.
2. 第⼆个参数是⼀个驱动特定的语法,它告诉驱动如何访问底层数据存储。在本例中,我们将连接本地的MySql服务器实例中
的“hello”数据库。
3. 你应该(⼏乎)总是检查并处理从所有database/sql操作返回的错误。有⼀些特殊情况,我们稍后将讨论这样做事没有意义的。
4. 如果sql.DB不应该超出该函数的作⽤范围,则延迟函数defer db.Close()是惯⽤的。
也许是反直觉的,sql.Open()不建⽴与数据库的任何连接,也不会验证驱动连接参数。相反,它只是准备数据库抽象以供以后使⽤。⾸次真正的连接底层数据存储区将在第⼀次需要时懒惰地建⽴。如果你想⽴即检查数据库是否可⽤(例如,检查是否可以建⽴⽹络连接并登陆),请使⽤db.Ping()来执⾏此操作,记得检查错误:
err = db.Ping()
if err != nil {
// do something here
}
虽然在完成数据库之后Close()数据库是惯⽤的,但是sql.DB对象被设计为长连接。不要经常Open()和Close()数据库。相反,为你需要访问的每个不同的数据存储创建⼀个sql.DB对象,并保留它,直到程序访问数据存储完毕。在需要时传递它,或在全局范围内使其可⽤,但要保持开放。并且不要从短暂的函数中Open()和Close()。相反,通过sql.DB作为参数传递给该短暂的函数。
如果你不把sql.DB视为长期存在的对象,则可能会遇到诸如重复使⽤和连接共享不⾜,耗尽可⽤的⽹络资源以及由于TIME_WAIT中剩余⼤量TCP连接⽽导致的零星故障的状态。这些问题表明你没有像设计的那样使⽤database/sql的迹象。
现在是时候使⽤你的sql.DB对象了。
No.4 检索结果集
有⼏个惯⽤的操作来从数据存储中检索结果。
1. 执⾏返回⾏的查询。
2. 准备重复使⽤的语句,多次执⾏并销毁它。
3. 以⼀次关闭的⽅式执⾏语句,不准备重复使⽤。
4. 执⾏⼀个返回单⾏的查询。这种特殊情况有⼀个捷径。
Golang的database/sql函数名⾮常重要。如果⼀个函数名包含查询Query(),它被设计为询问数据库的问题,并返回⼀组⾏,即使它是空的。不返回⾏的语句不应该使⽤Query()函数;他们应该使⽤Exec()。
从数据库获取数据
让我们来看⼀下如何查询数据库,使⽤Query的例⼦。我们将向⽤户表查询id为1的⽤户,并打印出⽤户的id和name。我们将使⽤
rows.Scan()将结果分配给变量,⼀次⼀⾏。
var (
id int
name string
)
rows, err := db.Query("select id, name from users where id = ?", 1)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
err := rows.Scan(&id, &name)
if err != nil {
log.Fatal(err)
}
log.Println(id, name)
}
err = rows.Err()
if err != nil {
log.Fatal(err)
}
下⾯是上⾯代码中正在发⽣的事情:
1. 我们使⽤db.Query()将查询发送到数据库。我们像往常⼀样检查错误。
2. 我们⽤defer内置函数推迟了rows.Close()的执⾏。这个⾮常重要。
3. 我们⽤rows.Next()遍历了数据⾏。
4. 我们⽤rows.Scan()读取每⾏中的列变量。
5. 我们完成遍历⾏之后检查错误。
这⼏乎是Golang中唯⼀的办法。例如,你不能将⼀⾏作为映射来获取。这是因为所有东西都是强类型的。你需要创建正确类型的变量并将指针传递给它们,如图所⽰。
其中的⼏个部分很容易出错,可能会产⽣不良后果。
· 你应该总是检查rows.Next()循环结尾处的错误。如果循环中出现错误,则需要了解它。不要仅仅假设循环遍历,直到你已经处理了所有的⾏。
· 第⼆,只要有⼀个打开的结果集(由⾏代表),底层连接就很忙,不能⽤于任何其他查询。这意味着它在连接池中不可⽤。如果你使⽤rows.Next()遍历所有⾏,最终将读取最后⼀⾏,rows.Next()将遇到内部EOF错误,并为你调⽤rows.Close()。但是,如果由于某种原因退出该循环-提前返回,那么⾏不会关闭,并且连接保持打开状态。(如果rows.Next()由于错误⽽返回false,则会⾃动关闭)。这是⼀种简单耗尽资源的⽅法。
· rows.Close()是⼀种⽆害的操作,如果它已经关闭,所以你可以多次调⽤它。但是请注意,我们⾸先检查错误,如果没有错误,则调⽤rows.Close(),以避免运⾏时的panic。
· 你应该总是⽤延迟语句defer推迟rows.Close(),即使你也在循环结束时调⽤rows.Close(),这不是⼀个坏主意。
· 不要在循环中⽤defer推迟。延迟语句在函数退出之前不会执⾏,所以长时间运⾏的函数不应该使⽤它。如果你这样做,你会慢慢积累记忆。如果你在循环中反复查询和使⽤结果集,则在完成每个结果后应显⽰的调⽤rows.Close(),⽽不⽤延迟语句defer。
Scan()如何⼯作
当你遍历⾏并将其扫描到⽬标变量中时,Golang会在幕后为你执⾏数据类型转换。它基于⽬标变量的类型。意识到这⼀点可以⼲净你的代码,并帮助避免重复⼯作。
例如,假设你从表中选择了⼀些⾏,这是⽤字符串列定义的。如varchar(45)或类似的列。然⽽,你碰巧知道表格总是包含数字。如果传递指向字符串的指针,Golang会将字节复制到字符串中。现在可以使⽤strconv.ParseInt()或类似的⽅式将值转换为数字。你必须检查SQL 操作中的错误以及解析整数的错误。这⼜乱⼜糟糕。
或者,你可以通过Scan()指向⼀个整数即可。Golang会检测到并为你调⽤strconv.ParseInt()。如果有转换错误,则调⽤Scan()将返回它。你的代码现在更⼩更整洁。这是推荐使⽤database/sql的⽅法。
准备查询
⼀般来说,你应该总是准备多次使⽤查询。准备查询的结果是⼀个准备语句,可以为执⾏语句时提供的参数,提供占位符(a.k.a bind值)。这⽐连接字符串更好,出于所有通常的理由(例如避免SQL注⼊攻击)。
在MySql中,参数占位符为?,在PostgreSql中为$N,其中N为数字。SQLite接受这两者之⼀。在Oracle中占位符以冒号开始,并命名为:param1。本⽂档中我们使⽤?占位符,因为我们使⽤MySql作为⽰例。
stmt, err := db.Prepare("select id, name from users where id = ?")
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
rows, err := stmt.Query(1)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
// ...
}
if err = rows.Err(); err != nil {
log.Fatal(err)
}
单⾏查询
如果⼀个查询返回最多⼀⾏,可以使⽤⼀些快速的样板代码:
var name string
err = db.QueryRow("select name from users where id = ?", 1).Scan(&name)
if err != nil {
log.Fatal(err)
}
fmt.Println(name)
来⾃查询的错误将被推迟到Scan(),然后返回。你也可以在准备的语句中调⽤QueryRow():
stmt, err := db.Prepare("select name from users where id = ?")
if err != nil {
log.Fatal(err)
mysql帮助文档
}
var name string
err = stmt.QueryRow(1).Scan(&name)
if err != nil {
log.Fatal(err)
}
fmt.Println(name)
No.5 修改数据和使⽤事务
现在我们已经准备好了如何修改数据和处理事务。如果你习惯于使⽤“statement”对象来获取⾏并更新数据,那么这种区别可能视乎是认为的,但是在Golang中有⼀个重要的原因。
修改数据的statements
使⽤Exec(),最好⽤⼀个准备好的statement来完成INSERT,UPDATE,DELETE或者其他不返回⾏的语句。下⾯的⽰例演⽰如何插⼊⾏并检查有关操作的元数据:
stmt, err := db.Prepare("INSERT INTO users(name) VALUES(?)")
if err != nil {
log.Fatal(err)
}
res, err := stmt.Exec("Dolly")
if err != nil {
log.Fatal(err)
}
lastId, err := res.LastInsertId()
if err != nil {
log.Fatal(err)
}
rowCnt, err := res.RowsAffected()
if err != nil {
log.Fatal(err)
}
log.Printf("ID = %d, affected = %d\n", lastId, rowCnt)
执⾏该语句将⽣成⼀个sql.Result,该语句提供对statement元数据的访问:最后插⼊的ID和⾏数受到影响。
如果你不在乎结果怎么办?如果你只想执⾏⼀个语句并检查是否有错误,但忽略结果该怎么办?下⾯两个语句不会做同样的事情吗?
_, err := db.Exec("DELETE FROM users")  // OK
_, err := db.Query("DELETE FROM users") // BAD
答案是否定的。他们不做同样的事情,你不应该使⽤Query()。Query()将返回⼀个sql.Rows,它保留数据库连接,直到sql.Rows关闭。由于可能有未读数据(例如更多的数据⾏),所以不能使⽤连接。在上⾯的⽰例中,连接将永远不会被释放。垃圾回收器最终会关闭底层的net.Conn,但这可能需要很长时间。此外,database/sql包将继续跟踪池中的连接,希望在某个时候释放它,以便可以再次使⽤连接。因此,这种反模式是耗尽资源的好⽅法(例如连接数太多)。
事务处理
在Golang中,事务本质上是保留与数据存储的连接的对象。它允许你执⾏我们迄今为⽌所看到的所有操作,但保证它们将在同⼀连接上执⾏。
你可以通过调⽤db.Begin()开始⼀个事务,并在结果Tx变量上⽤Commit()或Rollback()⽅法关闭它。在封⾯下,Tx从池中获取连接,并保留它仅⽤于该事务。Tx上的⽅法⼀对⼀到可以调⽤数据本本⾝的⽅法,例如Query()等等。
你不应该在SQL代码中混合BEGIN和COMMIT相关的函数(如Begin()和Commit()的SQL语句),可能会导致悲剧:
·
Tx对象可以保持打开状态,从池中保留连接⽽不返回。
· 数据库的状态可能与代表它的Golang变量的状态不同步。
· 你可能会认为你是在事务内部的单个连接上执⾏查询,实际上Golang已经为你创建了⼏个连接,⽽且⼀些语句不是事务的⼀部分。
当你在事务中⼯作时,你应该注意不要对Db变量进⾏调⽤。应当使⽤db.Begin()创建的Tx变量进⾏所有调⽤。Db不在⼀个事务中,只有Tx 是。如果你进⼀步调⽤db.Exec()或类似的函数,那么这些调⽤将发⽣在事务范围之外,是在其他的连接上。
如果你需要处理修改连接状态的多个语句,即使你不希望事务本⾝,也需要⼀个Tx。例如:
· 创建仅在⼀个连接中可见的临时表。
· 设置变量,如MySql's SET @var := somevalue语法。
· 更改连接选项,如字符集或超时。
如果你需要执⾏任何这些操作,则需要把你的作业(也可以说Tx操作语句)绑定到单个连接,⽽在Golang中执⾏此操作的唯⼀⽅法是使⽤Tx。
No.6 使⽤预处理语句

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。