第一次接触数据库的操作,很多概念都是陌生的,如果你跟我一样,请先移步:MySql入门(1) ,初步了解数据库的操作。本文后面的介绍将以mysql为主。
Go没有内置操作数据库的驱动,仅仅定义了一套database/sql接口,用户开发时需要基于驱动接口开发相应的数据库驱动,Go定义的这些驱动接口参考源码:driver.go。我没有去研究这些接口的开发,而是实行拿来主义,因为github上已经有很多开源的驱动:SQLDrivers,比如Go Web编程推荐的MySql驱动:https://github.com/go-sql-driver/mysql/,本文的例子也是采用这个驱动。
准备工作
创建一个数据库表结构:数据库test,用户表test_table。
jindg@nc:~$ mysql -h localhost -u root -p //root用户连接mysql,创建test数据库,设置权限jindg用户能访问和操作数据库test
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 6
Server version: 5.6.31-0ubuntu0.15.10.1 (Ubuntu)
Copyright (c) 2000, 2016, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql>create database test;
Query OK, 1 row affected (0.00 sec)
mysql> grant all on test.* to jindg@localhost identified by '123';
Query OK, 0 rows affected (0.04 sec)
mysql> exit
Bye
jindg@nc:~$ mysql -h localhost -u jindg -p //jindg用户连接mysql,在test下创建一张表
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 7
Server version: 5.6.31-0ubuntu0.15.10.1 (Ubuntu)
Copyright (c) 2000, 2016, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| test |
| testdb |
+--------------------+
3 rows in set (0.00 sec)
mysql> use test
Database changed
mysql> create table test_table(name vchar(40), age int)
-> ;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'vchar(40), age int)' at line 1
mysql> create table test_table(id bigint, name varchar(40), age int)
-> ;
Query OK, 0 rows affected (0.22 sec)
mysql> alter table test_table add primary key (id)
-> ; //忘记设主键了
Query OK, 0 rows affected (0.45 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> alter table test_table modify id bigint auto_increment; //设自动增加
Query OK, 0 rows affected (0.45 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> describe test_table;
+-------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+----------------+
| id | bigint(20) | NO | PRI | NULL | auto_increment |
| name | varchar(40) | YES | | NULL | |
| age | int(11) | YES | | NULL | |
+-------+-------------+------+-----+---------+----------------+
3 rows in set (0.00 sec)
连接数据库
package main
import (
"database/sql"
"fmt"
_ "github.com/Go-SQL-Driver/MySQL"
//"time"
)
func main() {
db, err := sql.Open("mysql", "jindg:123@tcp(localhost:3306)/test")
checkErr(err)
err = db.Ping()
if err == nil {
fmt.Println("ping is ok")
}
checkErr(err)
defer db.Close()
}
func checkErr(err error) {
if err != nil {
panic(err)
}
}
输出:
ping is ok
通过上面的代码可以看出,连接数据库其实很简单,一个Open函数就可以了。其中几个关键要点【其实是我学习中碰到的各种问题】我解释下:
1. import _ “github.com/Go-SQL-Driver/MySQL”:
这里有一个下划线,它的功能是表示当前程序不引用该包,而是调用它的init函数而已。当你执行go build或者go get时,会从github.com/Go-SQL-Driver/MySQL把源码下下来编译成一个库文件放在pkg目录下,比如我的系统在如下目录:
jindg@nc:~/goWorkProjects/goPath/pkg/linux_amd64/github.com/Go-SQL-Driver$ ls
MySQL.a
更多的参考:go-import下划线的作用和import 下划线 什么作用
2. sql.Open
sql.Open的第一个参数是driver的名称;第二个参数是DSN (Data Source Name),用于连接数据的信息。这里重点是DSN的格式。在了解DSN格式前,先阅读DSN (Data Source Name)和PEAR DB ,他们会帮助你建立DSN的概念。
DSN的格式:
[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]
如何获取mysql的端口,有两种方法:一种是通过netstat -apn命令查看端口占用情况,如下:
jindg@nc:~/daly_wiki/linux$ sudo netstat -apn|grep mysql
tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN 29501/mysqld
unix 2 [ ACC ] 流 LISTENING 1602657 29501/mysqld /var/run/mysqld/mysqld.sock
unix 3 [ ] 流 已连接 1814150 29501/mysqld /var/run/mysqld/mysqld.sock
unix 3 [ ] 流 已连接 1814540 8870/mysql
另一种是查看mysql的配置文件:
vim /etc/mysql/mysql.conf.d/mysqld.cnf
查找port,就能找到mysql所用的端口号。
3. db.Ping”:
Ping平时我们是用来检测网络通不通,这里的功能也一样,用来检测数据库连接是否正常。那问题就来了,为什么Open成功了,还需要Ping检测呢?我们来看看Open的源码,如下图:
选中的部分翻译过来意思是:Open只是验证了传入的参数,而不创建一个连接到数据库。要验证数据源是否有效,调用Ping接口。
可以尝试将上面的代码中第二个参数的密码修改成错误的密码,会发现 Open接口不会返回任何错误,直到Ping才会报错。
操作数据库
1. Query”:
用于查询,如果要问一个数据库的问题,并将返回一组行,即使它是空的。可以用Query接口实现,但是如果不返回行不应使用查询功能;他们应该使用exec()。
先来看看查询的例子,在上面的main方法中加入如下查询语句:
rows, err := db.Query("describe test_table")
fmt.Println(rows)
defer rows.Close()
for rows.Next() {
var fileId1 string
var fileId2 string
var fileId3 string
var fileId4 string
var fileId5 interface{}
var fileId6 string
err := rows.Scan(&fileId1, &fileId2, &fileId3, &fileId4, &fileId5, &fileId6)
fmt.Println(fileId1, fileId2, fileId3, fileId4, fileId5, fileId6)
checkErr(err)
}
对应的代码输出:
id bigint(20) NO PRI <nil> auto_increment
name varchar(40) YES <nil>
age int(11) YES <nil>
上面的代码实现了检索表结构功能:describe test_table。Query的参数其实就是我们平时操作数据的命令字,并且它还可以使用带占位符?的参数,比如:
hql:="select * from tb_user where name = ?"
_,err:=db.Query(hql,"viney")
checkErr(err)
mysql中的占位符是?,PostgreSQL的占位符为$N,注意领会它的使用。
为什么要显示的调用rows.Close()?在结果集(rows)未关闭前,底层的连接处于繁忙状态。当遍历读到最后一条记录时,会发生一个内部EOF错误,自动调用rows.Close(),但是如果提前退出循环,rows不会关闭,连接不会回到连接池中,连接也不会关闭。所以手动关闭非常重要。rows.Close()可以多次调用,是无害操作。
Next()方法和Scan()方法如何关联?使用Scan()方法遍历返回行的内容。至于Next()方法和Scan()方法如何关联的,去看它的源码,不难发现,Next()会把当前行的内容放入Row结构体中成员lastcols,然后调用Scan()从Row结构体中成员lastcols取出来
2. Exec”:
Exec()方法一帮用于插入、删除等没有返回行的操作,比如插入:
_, err = db.Exec("insert into test_table(name, age) values('zhang shan', '20')")
上面是直接往数据库插入一条信息。
3. Prepare”:
我们在插入大量数据时,采用批量插入所花的时间肯定比一条一条的插入要省时间一些,原因是批量,是所有的数据都在内存,然后一起提交数据库写入硬盘,而一条一条的插入每一次都会与数据库、硬盘打交道等,每一次都会做一些重复的操作,自然会费时很多。那在Go中如何实现批量操作呢?
Exec、Prepare区别?
在使用Prepare前,先了解直接db.Exec()和先Prepre后Exec的区别,先来看直接调用db.Exec()的源码,如下:
可以看到每次调用Exec()方法,都会先调用Prepare()方法先创建一个预处理语句,然后调用resultFromStatement()方法执行sql操作。
再来看Prepare后的Exec的源码,如下:
很明显,这里是直接调用resultFromStatement()方法,并不再需要Prepare,说明签名的Prepare执行后,连接会一直在,即使连接不在了,也会自动re-prepare。因此在大数据操作的时候,采用此方法会省时不少。
一个简单的例子实现
stmt, err := db.Prepare("insert into test_table(name, age) values(?,?)")
checkErr(err)
_, err = stmt.Exec("li si", 21)
checkErr(err)
_, err = stmt.Exec("wang ma zi", 22)
checkErr(err)
注意占位符?的使用。
事务
事务的概念属于数据库,而不属于Go。数据库中事务的提出是为了解决决并发情况下保持数据一致性的问题,具体概念细节请阅读数据库事务 。
db.Begin()开始事务,Commit() 或 Rollback()关闭事务。Tx从连接池中取出一个连接,在关闭之前都是使用这个连接。Tx不能和DB层的BEGIN, COMMIT混合使用。
如果你需要通过多条语句修改连接状态,你必须使用Tx,例如:
创建仅对单个连接可见的临时表
设置变量,例如SET @var := somevalue
改变连接选项,例如字符集,超时
Exec, Query, QueryRow and Prepare 方法已经全部可以在tx上面使用。使用方法和在*sql.DB是一样的,事务必须以Commit()或者Rollback()结束,使用事务进行大批量数据操作的时候,你会发现它的处理速度最快,效率更高,比前面提到的方法还要更有效率。来看一个例子:
tx, err := db.Begin()
checkErr(err)
stmt, err := db.Prepare("insert into test_table(name, age) values(?,?)")
checkErr(err)
_, err = stmt.Exec("li si", 21)
checkErr(err)
_, err = stmt.Exec("wang ma zi", 22)
checkErr(err)
//err = tx.Commit()
//checkErr(err)
err = tx.Rollback()
checkErr(err)
参考:
关于Golang中database/sql包的学习笔记
Golang操作数据库
golang学习之旅:使用go语言操作mysql数据库
go-mysql
go操作数据库 Go-SQL-Driver/MySQL 使用详解