go数据库编程
Go SQL驱动接口解读
Go官方没有提供数据库驱动,而是为开发数据库驱动定义了一些标准接口(即database/sql ),开发者可以根据定义的接口来开发相应的数据库驱动。Go中支持MySQL的驱动比较多,如
-
github.com/go-sql-driver/mysql 支持 database/sql
-
github.com/ziutek/mymysql 支持 database/sql,也支持自定义的接口
-
github.com/Philio/GoMySQL 不支持 database/sql,自定义接口
Driver
type Driver interface {
Open(name string) (Conn, error)
}
var d = Driver{proto: "tcp", raddr: "127.0.0.1:3306"}
sql.Register("mysql", &d) //注册数据库驱动
Conn
type Conn interface {
Prepare(query string) (Stmt, error) //把一个查询query传给Prepare,返回Stmt(statement)
Close() error //关闭数据库连接
Begin() (Tx, error) //返回一个事务Tx(transaction)
}
Stmt
type Stmt interface {
Close() error //关闭当前的链接状态
NumInput() int //返回当前预留参数的个数
Exec(args []Value) (Result, error) //执行Prepare准备好的 sql,传入参数执行 update/insert 等操作,返回 Result 数据
Query(args []Value) (Rows, error) //执行Prepare准备好的 sql,传入需要的参数执行 select 操作,返回 Rows 结果集
}
Tx
type Tx interface {
Commit() error //提交事务
Rollback() error //回滚事务
}
Result
type Result interface {
LastInsertId() (int64, error) //返回由数据库执行插入操作得到的自增ID号。如果使用单个INSERT将多行插入到表中,则LastInsertId是第一条数据使用的id
RowsAffected() (int64, error) //返回操作影响的数据条目数
}
RowsAffected RowsAffected是int64的别名,它实现了Result接口。
type RowsAffected int64
func (RowsAffected) LastInsertId() (int64, error)
func (v RowsAffected) RowsAffected() (int64, error)
Rows
type Rows interface {
Columns() []string //查询所需要的表字段
Close() error //关闭迭代器
Next(dest []Value) error //返回下一条数据,把数据赋值给dest,dest里面的元素必须是 driver.Value的值。如果最后没数据了,Next 函数返回 io.EOF
}
Value
type Value interface{}
Value 要么是 nil,要么是下面的任意一种
-
int64
-
float64
-
bool
-
[]byte
-
string
-
time.Time ValueConverter
type ValueConverter interface {
//把数据库里的数据类型转换成Value允许的数据类型
ConvertValue(v interface{}) (Value, error)
}
数据库增删改查
下载第三方库
go get github.com/go-sql-driver/mysql
连接数据库
db, err := sql.Open("mysql", "root:@tcp(localhost:3306)/test?charset=utf8")
DSN(data source name)格式: ]/dbname[?param1=value1&...¶mN=valueN] 例如user:password@tcp(localhost:5555)/dbname?charset=utf8mb4&parseTime=True 如果是本地MySQl,且采用默认的3306端口,可简写为:user:password@/dbname 连接参数 要支持完整的UTF-8编码,您需要将charset=utf8更改为charset=utf8mb4 想要正确的处理time.Time ,您需要带上parseTime参数 增删改
func (*sql.DB).Exec(sql string) (sql.Result, error)
查
func (*sql.DB).Query(sql string) (*sql.Rows, error)
crud.go
import (
"database/sql"
"fmt"
"go-course/database"
"time"
_ "github.com/go-sql-driver/mysql"
)
const TIME_LAYOUT = "2006-01-02"
var (
loc *time.Location
)
func init() {
loc, _ = time.LoadLocation("Asia/Shanghai")
}
//insert 插入数据
func insert(db *sql.DB) {
//一条sql,插入2行记录
res, err := db.Exec("insert into student (name,province,city,enrollment) values ('小明', '深圳', '深圳', '2021-04-18'), ('小红', '上海', '上海', '2021-04-26')")
database.CheckError(err)
lastId, err := res.LastInsertId() //ID自增,用过的id(即使对应的行已delete)不会重复使用。如果使用单个INSERT语句将多行插入到表中,则LastInsertId是第一条数据使用的id
database.CheckError(err)
fmt.Printf("after insert last id %d\n", lastId)
rows, err := res.RowsAffected() //插入2行,所以影响了2行
database.CheckError(err)
fmt.Printf("insert affect %d row\n", rows)
}
//replace 插入(覆盖)数据
func replace(db *sql.DB) {
//由于name字段上有唯一索引,insert重复的name会报错。而使用replace会先删除,再插入
res, err := db.Exec("replace into student (name,province,city,enrollment) values ('小明', '深圳', '深圳', '2021-04-18'), ('小红', '上海', '上海', '2021-04-26')")
database.CheckError(err)
lastId, err := res.LastInsertId() //ID自增,用过的id(即使对应的行已delete)不会重复使用
database.CheckError(err)
fmt.Printf("after insert last id %d\n", lastId)
rows, err := res.RowsAffected() //先删除,后插入,影响了4行
database.CheckError(err)
fmt.Printf("insert affect %d row\n", rows)
}
//update 修改数据
func update(db *sql.DB) {
//不同的city加不同的分数
res, err := db.Exec("update student set score=score+10 where city='上海'") //上海加10分
database.CheckError(err)
lastId, err := res.LastInsertId() //0, 仅插入操作才会给LastInsertId赋值
database.CheckError(err)
fmt.Printf("after update last id %d\n", lastId)
rows, err := res.RowsAffected() //where city=?命中了几行,就会影响几行
database.CheckError(err)
fmt.Printf("update affect %d row\n", rows)
}
//query 查询数据
func query(db *sql.DB) {
rows, err := db.Query("select id,name,city,score from student where id>2") //查询得分大于2的记录
database.CheckError(err)
for rows.Next() { //没有数据或发生error时返回false
var id int
var score float32
var name, city string
err = rows.Scan(&id, &name, &city, &score) //通过scan把db里的数据赋给go变量
database.CheckError(err)
fmt.Printf("id=%d, score=%.2f, name=%s, city=%s \n", id, score, name, city)
}
}
//delete 删除数据
func delete(db *sql.DB) {
res, err := db.Exec("delete from student where id>13") //删除得分大于13的记录
database.CheckError(err)
rows, err := res.RowsAffected() //where id>13命中了几行,就会影响几行
database.CheckError(err)
fmt.Printf("delete affect %d row\n", rows)
}
stmt
首先看两个sql注入攻击的例子。
sql = "select username,password from user where username='" + username + "' and password='" + password + "'";
变量username和password从前端输入框获取,如果用户输入的username为lily, password为aaa' or '1'='1,则完整的sql为select username,password from user where username='lily' and password='aaa' or '1'='1'。会返回表里的所有记录,如果记录数大于0就允许登录,则lily的账号被盗。
sql="insert into student (name) values ('"+username+" ') ";
变量username从前端输入框获取,如果用户输入的username为lily'); drop table student;--。完整sql为insert into student (name) values ('lily'); drop table student;--')。通过注释符--屏蔽掉了末尾的'),删除了整个表。 防止sql注入的方法:
-
前端输入要加正则校验、长度限制。
-
对特殊符号(<>&*; '"等)进行转义或编码转换,Go的text/template 包里面的HTMLEscapeString函数可以对字符串进行转义处理。
-
不要将用户输入直接嵌入到sql语句中,而应该使用参数化查询接口,如Prepare、Query、Exec(query string, args ...interface{})。
-
使用专业的SQL注入检测工具进行检测,如sqlmap、SQLninja。
-
避免网站打印出SQL错误信息,以防止攻击者利用这些错误信息进行SQL注入。
参数化查询
db.Where("merchant_id = ?", merchantId)
拼接sql
db.Where(fmt.Sprintf("merchant_id = %s", merchantId))
定义一个sql模板
stmt, err := db.Prepare("update student set score=score+? where city=?")
多次使用模板
res, err := stmt.Exec(10, "上海")
res, err = stmt.Exec(9, "深圳")
SQL预编译
DB执行sql分为3步:
-
词法和语义解析。
-
优化SQL语句,制定执行计划。
-
执行并返回结果。
SQL预编译技术是指将用户输入用占位符?代替,先对这个模板化的sql进行预编译,实际运行时再将用户输入代入。除了可以防止SQL注入,还可以对预编译的SQL语句进行缓存,之后的运行就省去了解析优化SQL语句的过程。
stmt_demo.go
//update 通过stmt修改数据
func update(db *sql.DB) {
//不同的city加不同的分数
stmt, err := db.Prepare("update student set score=score+? where city=?")
database.CheckError(err)
//执行修改操作通过stmt.Exec,执行查询操作通过stmt.Query
res, err := stmt.Exec(10, "上海") //上海加10分
database.CheckError(err)
res, err = stmt.Exec(9, "深圳") //深圳加9分
database.CheckError(err)
lastId, err := res.LastInsertId() //0, 仅插入操作才会给LastInsertId赋值
database.CheckError(err)
fmt.Printf("after update last id %d\n", lastId)
rows, err := res.RowsAffected() //where city=?命中了几行,就会影响几行
database.CheckError(err)
fmt.Printf("update affect %d row\n", rows)
}
//query 通过stmt查询数据
func query(db *sql.DB) {
stmt, err := db.Prepare("select id,name,city,score from student where id>?")
database.CheckError(err)
//执行修改操作通过stmt.Exec,执行查询操作通过stmt.Query
rows, err := stmt.Query(2) //查询得分大于2的记录
database.CheckError(err)
for rows.Next() { //没有数据或发生error时返回false
var id int
var score float32
var name, city string
err = rows.Scan(&id, &name, &city, &score) //通过scan把db里的数据赋给go变量
database.CheckError(err)
fmt.Printf("id=%d, score=%.2f, name=%s, city=%s \n", id, score, name, city)
}
}
遍历一张表的正确姿势:
//traverse 借助于主健自增ID,通过where id>maxid遍历表
func traverse(db *sql.DB) {
var maxid int
begin := time.Now()
stmt, _ := db.Prepare("select id,name,province from student where id>? limit 100") //limit m,n limit 0,n
for i := 0; i < 100; i++ {
t0 := time.Now()
rows, _ := stmt.Query(maxid)
fmt.Println(i, time.Since(t0))
for rows.Next() {
var id int
var name string
var province string
rows.Scan(&id, &name, &province)
if id > maxid {
maxid = id
}
}
}
fmt.Println("total", time.Since(begin))
}
SQLBuilder
Go-SQLBuilder
Go-SQLBuilder是一个用于创建SQL语句的工具函数库,提供一系列灵活的、与原生SQL语法一致的链式函数。归属于艾润物联公司。安装方式
go get -u github.com/parkingwang/go-sqlbuilder
Go-SQLBuilder通过函数链来构造sql语句,比如select语句的构造
func query() {
sql := gsb.NewContext().Select("id", "name", "score", "city").
From("student").
OrderBy("score").DESC().
Column("name").ASC().
Limit(10).Offset(20).
ToSQL()
fmt.Println(sql)
}
为什么需要SQLBuilder?
-
写一句很长的sql容易出错,且出错后不好定位。
-
函数式编程可以直接定位到是哪个函数的问题。
-
函数式编程比一长串sql更容易编写和理解。
Gendry
Gendry是一个用于辅助操作数据库的Go包。基于go-sql-driver /mysql,它提供了一系列的方法来为你调用标准库database/sql中的方法准备参数。安装方式
go get –u github.com/didi/gendry
Gendry倾向于把复杂的筛选条件放在map中,并且跟stmt技术结合得比较紧密。
func query(db *sql.DB) {
where := map[string]interface{}{
"city": []string{"北京", "上海", "杭州"},
"score<": 30,
"addr": builder.IsNotNull,
"_orderby": "score desc",
}
table := "student"
fields := []string{"id", "name", "city", "score"}
//准备stmt模板
template, values, err := builder.BuildSelect(table, where, fields)
database.CheckError(err)
//执行stmt模板
rows, err := db.Query(template, values...)
database.CheckError(err)
for rows.Next() {
var id int
var name, city string
var score float32
err := rows.Scan(&id, &name, &city, &score)
database.CheckError(err)
fmt.Printf("%d %s %s %.2f\n", id, name, city, score)
}
}
自行实现SQLBuilder
作为练习,我们自行实现一个SQLBuilder,它最终应该支持如下函数链式的编程风格。
sql := NewSelectBuilder("student").Column("id,name,city").
Where("id>0").
And("city='郑州'").
Or("city='北京'").
OrderBy("score").Desc().
Limit(0, 10).ToString()`
Builder设计模式的精髓在于Builder对象的方法还是返回一个Builder。首先定义一个Builder接口。
type Builder interface {
toString() string
getPrev() Builder
}
select、where、limit、orderby这些都是Builder,这里详细讲解WhereBuilder的设计与实现。
type WhereBuilder struct {
sb strings.Builder //拼接where条件字符串
orderby *OrderByBuilder//where后面可能会接order by
limit *LimitBuilder // where后面可能会接limit
prev Builder //where前面是select
}
WhereBuilder中的sb负责当下,orderby和limit负责维护后面节点,prev负责维护前面的节点。 where表达式中可能包含and和or,把它们定义为WhereBuilder的方法,并且这两个方法依赖返回WhereBuilder自身。
func (self *WhereBuilder) And(condition string) *WhereBuilder {
self.sb.WriteString(" and ")
self.sb.WriteString(condition)
return self
}
func (self *WhereBuilder) Or(condition string) *WhereBuilder {
self.sb.WriteString(" or ")
self.sb.WriteString(condition)
return self
}
where表达式后面可能会跟order by表达式,把OrderBy定义为WhereBuilder的方法,该方法返回OrderByBuilder。
func (self *WhereBuilder) OrderBy(column string) *OrderByBuilder {
orderby := newOrderByBuilder(column)
self.orderby = orderby
orderby.prev = self
return orderby
}
函数链上的最后一个Builder调用ToString()方法生成写成的sql语句。
func (self *LimitBuilder) ToString() string {
var root Builder
root = self
for root.getPrev() != nil {
root = root.getPrev() //递归找到最前面的Builder
}
return root.toString() //在最前面的Builder(即SelectBuilder)上调用toString()
}
每个Builder都有toString()方法,以WhereBuilder为例,它在构造函数里把where表达式放入sb成员变量里,WhereBuilder在toString()方法里调用where后面的节点的toString()方法。
func newWhereBuilder(condition string) *WhereBuilder {
builder := &WhereBuilder{}
builder.sb.WriteString(" where ")
builder.sb.WriteString(condition)
return builder
}
func (self *WhereBuilder) toString() string {
//递归调用后续Builder的ToString()
if self.orderby != nil {
self.sb.WriteString(self.orderby.toString())
}
if self.limit != nil {
self.sb.WriteString(self.limit.toString())
}
return self.sb.String()
}
GORM
ORM即Object Relational Mapping,对象关系映射。Relational指各种sql类的关系型数据库。Object指面向对象编程(object-oriented programming)中的对象。ORM在数据库记录和程序对象之间做一层映射转换,使程序中不用再去编写原生SQL,而是面向对象的思想去编写类、对象、调用相应的方法来完成数据库操作。
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql
GORM是一个全能的、友好的、基于golang的ORM库。 GORM 倾向于约定,而不是配置。默认情况下,GORM 使用ID作为主键,使用结构体名的【蛇形复数】作为表名,字段名的【蛇形】作为列名,并使用CreatedAt、UpdatedAt字段追踪创建、更新时间。 GORM完全是在操作struct,看不到sql的影子。
type Student struct {
Id int `gorm:"column:id;primaryKey"`
Name string `gorm:"column:name"`
Province string
City string `gorm:"column:city"`
Address string `gorm:"column:addr"`
Score float32 `gorm:"column:score"`
Enrollment time.Time `gorm:"column:enrollment;type:date"`
}
student := Student{
Name: "光绪",
Province: "北京",
City: "北京",
Score: 38,
Enrollment: time.Now()
}
db.Create(&student)
GORM同时支持使用函数链的方式写sql语句。
func query(db *gorm.DB) {
//返回一条记录
var student Student
db.Where("city=?", "郑州").First(&student) //有First就有Last
fmt.Println(student.Name)
fmt.Println()
//返回多条记录
var students []Student
db.Where("city=?", "郑州").Find(&students)
for _, ele := range students {
fmt.Printf("id=%d, name=%s\n", ele.Id, ele.Name)
}
fmt.Println()
students = []Student{} //清空student,防止前后影响
db.Where("city in ?", []string{"郑州", "北京"}).Find(&students)
for _, ele := range students {
fmt.Printf("id=%d, name=%s\n", ele.Id, ele.Name)
}
fmt.Println("============where end============")
//根据主键查询
student = Student{} //清空student,防止前后影响
students = []Student{}
db.First(&student, 1)
fmt.Println(student.Name)
fmt.Println()
db.Find(&students, []int{1, 2, 3})
for _, ele := range students {
fmt.Printf("id=%d, name=%s\n", ele.Id, ele.Name)
}
fmt.Println("============primary key end============")
//根据map查询
student = Student{}
students = []Student{}
db.Where(map[string]interface{}{"city": "郑州", "score": 0}).Find(&students)
for _, ele := range students {
fmt.Printf("id=%d, name=%s\n", ele.Id, ele.Name)
}
fmt.Println("============map end============")
//OR查询
student = Student{}
students = []Student{}
db.Where("city=?", "郑州").Or("city=?", "北京").Find(&students)
for _, ele := range students {
fmt.Printf("id=%d, name=%s\n", ele.Id, ele.Name)
}
fmt.Println("============or end============")
//order by
student = Student{}
students = []Student{}
db.Where("city=?", "郑州").Order("score").Find(&students)
for _, ele := range students {
fmt.Printf("id=%d, name=%s\n", ele.Id, ele.Name)
}
fmt.Println("============order end============")
//limit
student = Student{}
students = []Student{}
db.Where("city=?", "郑州").Order("score").Limit(1).Offset(0).Find(&students)
for _, ele := range students {
fmt.Printf("id=%d, name=%s\n", ele.Id, ele.Name)
}
fmt.Println("============limit end============")
//选择特定的字段
student = Student{}
db.Select("name").Take(&student) //Take从结果中取一个,不保证是第一个或最后一个
fmt.Printf("name=%s, province=%s\n", student.Name, student.Province) //只select了name,所以province是空的
}
Go操作MongoDB
NoSQL泛指非关系型数据库,如mongo,redis,HBase。mongo使用高效的二进制数据存储,文件存储格式为 BSON ( 一种json的扩展,比json性能更好,功能更强大)。MySQL中表的概念在mongo里叫集合(collection), MySQL中行的概念在mongo中叫文档(document),一个文档看上去像一个json。 安装mongo前先配置yum源: vim /etc/yum.repos.d/mongodb-org-4.2.repo
[mongodb-org-4.2]
name=MongoDB Repository baseurl=https://repo.mongodb.org/yum/redhat/$releasever/mongodb-org/4.2/x86_64/ gpgcheck=1 enabled=1 gpgkey=https://www.mongodb.org/static/pgp/server-4.2.asc
一键安装mongo: sudo yum install -y mongodb-org 启动mongo: systemctl start mongod mongo常用命令
use test; 切换到test库,如果没有则(创建集合时)会自动创建
db.createCollection("student"); 创建collection
db.createUser({user: "tester",pwd: "123456", roles: [{role: "dbAdmin", db: "test"}]});创建用户
登录mongo --port 27017 -u "tester" -p "123456" --authenticationDatabase "test"
db.student.createIndex({"name":1});在name上创建索引,不是唯一索引
db.student.insertOne({name:"张三",city:"北京"});
db.student.find({name:"张三"});
db.student.update({name:"张三"},{name:"张三",city:"上海"})
db.student.deleteOne({name:"张三"});
安装go mongo-driver
go get go.mongodb.org/mongo-driver
go get go.mongodb.org/mongo-driver/x/bsonx/bsoncore@v1.7.1
go get go.mongodb.org/mongo-driver/x/mongo/driver@v1.7.1
go get go.mongodb.org/mongo-driver/mongo/options@v1.7.1
go get go.mongodb.org/mongo-driver/x/mongo/driver/topology@v1.7.1
go get go.mongodb.org/mongo-driver/mongo@v1.7.1
连接db
option := options.Client().ApplyURI("mongodb://127.0.0.1:27017").
SetConnectTimeout(time.Second).//连接超时时长
SetAuth(options.Credential{Username: "tester", Password: "123456", AuthSource: "test"}) //指定用户名和密码,AuthSource代表Database
client, err := mongo.Connect(context.Background(), option)
err = client.Ping(ctx, nil)
注意Ping成功才代表连接成功。 查询mongo
sort := bson.D{{"name", 1}} //1升序,-1降序
filter := bson.D{{"score", bson.D{{"$gt", 3}}}} //score>3
findOption := options.Find()
findOption.SetSort(sort)//按name排序
findOption.SetLimit(10) //最多返回10个
findOption.SetSkip(3) //跳过前3个
cursor, err := collection.Find(ctx, filter, findOption)
defer cursor.Close(ctx) //关闭迭代器
for cursor.Next(ctx) {
var doc Student
err := cursor.Decode(&doc)
database.CheckError(err)
fmt.Printf("%s %s %.2f\n", doc.Name, doc.City, doc.Score)
}
浙公网安备 33010602011771号