From d923309ef75ce456620aaf8beb98bfa80a8cfabd Mon Sep 17 00:00:00 2001 From: Jwindqiu Date: Sat, 12 Jun 2021 21:50:47 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E7=BF=BB=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...er 29 Data storage files and databases.md | 481 ++++++++++++++++++ 1 file changed, 481 insertions(+) create mode 100644 Chapter 29 Data storage files and databases.md diff --git a/Chapter 29 Data storage files and databases.md b/Chapter 29 Data storage files and databases.md new file mode 100644 index 0000000..b05e653 --- /dev/null +++ b/Chapter 29 Data storage files and databases.md @@ -0,0 +1,481 @@ +第 29 章:数据存储:文件和数据库 + +### **1、你将在本章节学到什么?** + +* 如何新建一个文件。 +* 如何将数据写入文件。 +* 如何新建 CSV 格式的文件。 +* 如何查询以下数据库: + * MySQL + * PostgreSQL + * MongoDB + * ElasticSearch + +### **2、技术概念梳理** + +* 文件权限 +* 八进制 +* SQL注入 +* 预查询 +* 无模式 +* 关系数据库管理系统 + +### **3、介绍** + +此章节,会通过 Go 使用不同技术存储数据。 + +### **4、新建一个文件** + +使用 os.Create 方法可以新建一个文件: + +```go +// data-storage/file-save/simple/main.go +package main + +import ( + "fmt" + "os" +) + +func main() { + f, err := os.Create("test.csv") + if err != nil { + fmt.Println(err) + return + } + //... success +} +``` + +我们新建一个命名为 "test.csv" 的文件。如果有报错的话,程序会打印错误并返回,程序代码如下: + +```go +f, err := os.OpenFile("test.csv", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) +if err != nil { + fmt.Println(err) + return +} +``` + +os.Create 的内部通过以下参数调用 os.Openfile 方法,参数如下: + +* 文件名(比如:"test.csv") +* 文件模式(比如:os.O_RDWR|os.O_CREATE|os.O_TRUNC ) +* 文件权限模式(比如:0666 ) + +我们将在下面两个章节中知晓 **文件模式 和**文件权限模式**是什么。当然,如果你已经熟悉了这些章节内容,可以跳过。 + +### **5、文件模式标记** + +你将通过系统调用的方式,使用 os.Openfile 打开一个文件。通过调用,系统需要清楚**文件路径**以及需要达到目的其他额外信息。这一系列信息包含在一个模式标记列表中,他们有不同类型的模式标记: + +* **允许访问模式标记**:在新建一个文件时,必须指定其中一个标记作为入参参数。 +* **允许执行模式标记**:系统执行文件操作时会被限制。 +* **文件状态模式标记**:当文件打开时,会得到一个对文件操作的状态结果反馈。 + + + +| **标记类型** | 描述 | +| ------------ | ------------------------------------------------------------ | +| os.O_RDONLY | 以只读模式打开,只能读。 | +| os.O_WRONLY | 以只写模式打开,只能写不能读。 | +| os.O_RDWR | 以读写模式打开。 | +| os.O_CREATE | 新建文件若不存在。 | +| os.O_EXCL | 若已使用 os.O_CREATE 模式,且文件已存在,则返回一个错误。 | +| os.O_TRUNC | 打开文件时会被清空,若文件有内容则会清除。 | +| os.O_APPEND | 将内容写入追加到文件文本后面。 | +| os.O_SYNC | 以同步模式打开,程序会等待所有数据被完全写入为止。 | + +文件打开模式标记 + +每种模式标记的值都是整型。你必须使用按位计算使用这些模式标记(看章节 [Bitmasks])。当你使用 os.Create 时,文件将会通过以下模式标记打开: + +``` +os.O_RDWR|os.O_CREATE|os.O_TRUNC +``` + +这样意味着文件是通过**读写模式**打开的,若文件不存在则会新建。另外,系统会进行文件内容清空操作。 + +举例:若需要确定文件是否新建,可以通过添加模式标记:os.O_EXCL + +```go +f, err := os.OpenFile("test.csv", os.O_RDWR|os.O_CREATE|os.O_TRUNC|os.O_EXCL, 0666) +if err != nil { + fmt.Println(err) + return +} +fmt.Println(f) +``` + +上面程序执行后将输出: + +``` +open test.csv: file exists +``` + +哪种模式标记才是期望的呢? + +### **6、文件模式 [sec: file-mode]** + +在 UNIX 系统中,每一个文件都有权限设置: + +* 文件权限属性的拥有者 +* 文件权限所属用户组别 +* 文件权限所属其他用户 + +若你想查看文件的权限信息,可以打开你的终端窗口,并输入 ls -l 命令。 + +```shell +$ ls -al +-rw-r--r-- 1 maximilienandile staff 242 25 nov 19:47 main.go +``` + +* **-rw-r--r--** :文件权限模式 +* **1** :链接数字 +* **maximilienandile** :文件权限属性的拥有者 +* **staff** :文件权限所属用户组别 +* **242** :以 byte 为单位的文件大小 +* **25 nov 19:47** :最后修改日期时间 + +#### 6.0.0.1 符号表示 + +文件权限模式由三个区块组成(权限拥有者 user,权限所属组别 group,以及其他人 others ) + +![File mode schema[fig:File-mode-schema]](https://www.practical-go-lessons.com/img/permissions.7a548570.png) + +当看到第一个区块时,可以通过 3 个字母去定义权限,它们总是以这样的方式排序: + +* **r** :读 +* **w** :写 +* **x** :执行 + +第一个字符是连接号 - 。当文件是一个目录类型时,相当于目录英文第一个字母 d。如果文件权限模式的第一个字符是- ,这表示权限不可用。 + +下面通过 **-rw-r--r--** 例子的方式,说明: + +* **First char**:这是一个文件 +* **User(rw-)**:文件拥有者可读、可写、不可执行。 +* **Group(r--)**:文件所属组别可读、不可写、不可执行。 +* **Others(r--)**:文件所属组别可读、不可写、不可执行。 + +下面用 Go 实现文件模式: + +```go +// data-storage/file-save/mode/main.go + +// 打开文件 +f, err := os.Open("myFile.test") +if err != nil { + fmt.Println(err) + return +} +// 读取文件信息 +info, err := f.Stat() +if err != nil { + fmt.Println(err) + return +} +fmt.Println(info.Mode()) +``` + +上述程序将输出:-rw-r--r-- 。 + +#### 6.0.0.2、数字表示 + +此模式还可以通过转换为 八进制 数字转换 (通过 %o 格式化)。 + +```go +fmt.Printf("Mode Numeric : %o\n", info.Mode()) +fmt.Printf("Mode Symbolic : %s\n", info.Mode()) +// 数字模式 : 644 +// 字符模式 : -rw-r--r-- +``` + +Go 在调用 os.Openfile 使用数字表示,由 3个数字组成: + +![File mode numeric notation[图2]](https://www.practical-go-lessons.com/img/octal_file_mode.65204000.png) + +数字(八进制)表示一组权限(见图2)。每个八进制数字(从0到7)都有特定的含义。 + +| 权限 | 符号 | 八进制 | +| :------------------: | ---------------- | ------ | +| 空 | --- | 0 | +| 只执行 | --x | 1 | +| 只写 | -w- | 2 | +| 可写 & 可执行 | -wx | 3 | +| 只读 | r-- | 4 | +| 可读 & 可执行 | r-x | 5 | +| 可读 & 可写 | rw- | 6 | +| 可读 & 可写 & 可执行 | rwx | 7 | + +让我们举个例子: 644,我们分解: + +* Owner(拥有者):6 表示可读、可写 +* Group(所属组别): 4 表示可读 +* Others(其他人): 4 表示可读 + +另一个例子 777: + +* Owner(拥有者):7 表示可读、可写、可执行 +* Group(所属组别): 7 表示可读、可写、可执行 +* Others(其他人): 7 表示可读、可写、可执行 + +#### 6.0.0.3 通过 Go 更改文件权限 + +在一个文件上,你可以使用 Chmod 方法来更改文件模式 + +```go +// data-storage/file-save/mode/main.go + +err = f.Chmod(0777) +if err != nil { + fmt.Println(err) +} +``` + +这里我们将文件的模式更改为0777。这意味着所有者、组和其他人的都更改为全部权限。 + +为什么 0777 要加0 ?它向 Go 表示该数不是十进制数而是八进制数。 + +### **7、写文件:以 CSV 为例** + +### 7.1 什么是 CSV + +CSV 表示逗号分隔值,它是一种用于存储数据的文件格式。 + +* 一个 CSV 文件由多行数据组成 +* 每一行代表数据记录 +* 每一个数据由 多个“区域” 组成 +* 每一个逗号分隔符,分割每个“区域” +* 第一行的位置,通常被用作表头标题信息进行描述 + +举例如下: + +``` +age,genre,name +23,M,Hendrick +65,F,Stephany +``` + +#### 7.2、Code + +在本节中,我们将看到如何将数据写入文件。为了立即应用我们的知识,我们将使用一个真实的用例:从一个切片创建一个CSV文件。 + +```go +// data-storage/file-save/writing/main.go +package main + +import ( + "bytes" + "fmt" + "os" +) + +func main() { + s := [][]string{ + {"age", "genre", "name"}, + {"23", "M", "Hendrick"}, + {"65", "F", "Stephany"}, + } + // 打开文件或新建 + f, err := os.Create("myFile.csv") + defer f.Close() + if err != nil { + fmt.Println(err) + return + } + var buffer bytes.Buffer + // 将切片 s 进行迭代 + for _, data := range s { + buffer.WriteString(fmt.Sprintf("%s,%s,%s\n", data[0], data[1], data[2])) + } + n, err := f.Write(buffer.Bytes()) + fmt.Printf("%d bytes written\n", n) + if err != nil { + fmt.Println(err) + return + } +} +``` + +首先,我们新建一个切片 s,这是我们想要写入 CSV 文件的数据。 + +我们新建一个类型为 bytes.Buffer 变量 buffer 。 + +然后我们迭代 s 将数据添加到 buffer中,每个值由逗号分隔,并且通过字符“\ n”添加新行。 +当 buffer 已就绪时,我们将其通过方法 write 写入文件中。 此方法将 **字节类型的切片变量** 作为参数,并返回两个值: + +* 写入文件的字节数 +* 错误结果 + +#### 7.3、 bytes.Buffer + +我们在前面的程序中使用了一个数据缓冲区,缓冲区是物理存储器的一个区域,当数据从一处移动到另一处时,用来临时存储数据。 + +若你需要高效的连接字符串或者操作 byte类型的切片时,bytes.Buffer将非常有用。 + +### **8、用例** + +我们将通过下一节内容说明以下用例: + +* 你被学校聘请了 +* 学校需要软件程序来管理学生和教师们: + - 查询、新增、更新、删除一名老师 + - 查询、新增、更新、删除一名学生 + +在接下来的章节内容中,我们将看到如何使用不同数据库进行操作: + +### **9、MySQL数据库** + +MySQL是一个开源的关系数据库管理系统。在本节中,我们将看到如何连接到 MySQL 数据库,以及如何用 Go 执行基本的 CRUD 操作。 + +Go 没有附带 MySQL 驱动程序,但是标准库定义了操作 SQL 数据库的接口。 + +在撰写本文时,在 Go wiki 中提出了两个驱动程序。 + +我选择了最受欢迎的 github https://github.com/go-sql-driver/mysql,它是Mozilla公共许可证 2.0 版本。 + +#### 9.1、连接 + +```go +// 目录:data-storage/mysql/create/main.go +package main + +import ( + "database/sql" + "fmt" + + _ "github.com/go-sql-driver/mysql" +) + +func main() { + db, err := sql.Open("mysql", "root:root@tcp(localhost:8889)/school") + if err != nil { + fmt.Println(err) + return + } + err = db.Ping() + if err != nil { + fmt.Println(err) + return + } +} +``` + +在此,我们导入标准包数据库/ SQL,它定义在 SQL 数据库上执行操作所需的所有功能。 + +然后我们使用空白处导入,进行注册驱动程序 github.com/go-sql-driver/mysql 。 + +如果不导入驱动程序,可能会遇到以下错误 + +```go +// 未知 mysql 驱动 +sql: unknown driver "mysql" (forgotten import?) +``` + +sql.Open 接收两个参数: + +1. 驱动名称 +2. 数据源名称(通常叫法:DSN) + +这个 DSN 参数是你定义的一个字符串: + +* 用户名:root +* 密码:root +* 数据库连接协议:TCP +* 主机:localhost +* 端口:8889 +* 数据库名称:school + +在数据库驱动文档中,DSN 等同于以下: + +``` +username:password@protocol(address)/dbname?param=value +``` + +sql.Open 函数会返回 *sql.DB 指针 + +通过 Ping 方法可以检查是否连接有效,如果无效,则新建一个。 + +#### 9.2、建表 + +第一件事,你应该想在数据库中创建一张数据表,开始编写 SQL 脚本: + +```sql +CREATE TABLE `teacher` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `create_time` TIMESTAMP DEFAULT NULL, + `update_time` TIMESTAMP DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, + `firstname` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, + `lastname` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB; +``` + +我们新建了一张 **教师表**,表中会存储学校内所有老师的以下数据: + +* 名字 +* 姓氏 + +每个老师将有一个id(我们的表的主键)。我们还添加了两个字段列,用于保存每行数据的创建日期(创建时间)和最后更新日期(更新时间)。 + +接下来,执行这个 SQL 脚本吧! + +首先,我们将这个 SQL 脚本保存到一个命名为 create_table.sql 的文件中。我们的脚本程序将首先加载此SQL 脚本: + +```go +f, err := os.Open("create_table.sql") +if err != nil { + fmt.Println(err) + return +} +b, err := ioutil.ReadAll(f) +if err != nil { + fmt.Println(err) + return +} +``` + +在变量 b,我们有一个字节类型的切片可以代替查询条件,执行如下: + +```go +res, err := db.Exec(string(b)) +if err != nil { + fmt.Println(err) + return +} +``` + +我们使用 string(b) 将字节切片转换为字符串,因为 Exec 方法以字符串作为参数。 + +注意,返回的两个值要么是结果(类型为 sql.Result ),要么是错误。 sql.Result是一个具有两个方法的类型接口: + +* LastInsertId() (int64, error) +* RowsAffected() (int64, error) + +我们将检查结果是否没有错误,创建表不会影响现有的行,也不会插入数据 + +#### 9.3 插入数据 + +要创建一个 teacher,我们将使用 INSERT 语句。我们将使用 Exec 方法 + +```go +// data-storage/mysql/insert/main.go + +func createTeacher(firstname string, lastname string, db *sql.DB) (int64, error) { + res, err := db.Exec("INSERT INTO `teacher` (`create_time`, `firstname`, `lastname`) VALUES (NOW(), ?, ?)", firstname, lastname) + if err != nil { + return 0, err + } + id, err := res.LastInsertId() + if err != nil { + return 0, err + } + return id, nil +} +``` + +* 有一个函数 createTeacher,它接受两个字符串参数 ( firstnamelastname) 和一个指向 sql.DB 的指针参数。 +* 我们传递给 Exec 方法三个参数。 + * SQL查询(带有?作为值的占位符)和我们想注入到数据库中的值。 + -- Gitee From df16ea2e26b93687705193e0cd0cbce1a897f00f Mon Sep 17 00:00:00 2001 From: Jwindqiu Date: Sun, 13 Jun 2021 20:39:38 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E7=BF=BB=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...er 29 Data storage files and databases.md | 1061 ++++++++++++++++- 1 file changed, 1043 insertions(+), 18 deletions(-) diff --git a/Chapter 29 Data storage files and databases.md b/Chapter 29 Data storage files and databases.md index b05e653..4c98cc8 100644 --- a/Chapter 29 Data storage files and databases.md +++ b/Chapter 29 Data storage files and databases.md @@ -1,6 +1,8 @@ 第 29 章:数据存储:文件和数据库 -### **1、你将在本章节学到什么?** +[TOC] + +### **1 你将在本章节学到什么?** * 如何新建一个文件。 * 如何将数据写入文件。 @@ -11,7 +13,7 @@ * MongoDB * ElasticSearch -### **2、技术概念梳理** +### **2 技术概念梳理** * 文件权限 * 八进制 @@ -20,11 +22,11 @@ * 无模式 * 关系数据库管理系统 -### **3、介绍** +### **3 介绍** 此章节,会通过 Go 使用不同技术存储数据。 -### **4、新建一个文件** +### **4 新建一个文件** 使用 os.Create 方法可以新建一个文件: @@ -65,7 +67,7 @@ if err != nil { 我们将在下面两个章节中知晓 **文件模式 和**文件权限模式**是什么。当然,如果你已经熟悉了这些章节内容,可以跳过。 -### **5、文件模式标记** +### **5 文件模式标记** 你将通过系统调用的方式,使用 os.Openfile 打开一个文件。通过调用,系统需要清楚**文件路径**以及需要达到目的其他额外信息。这一系列信息包含在一个模式标记列表中,他们有不同类型的模式标记: @@ -115,7 +117,7 @@ open test.csv: file exists 哪种模式标记才是期望的呢? -### **6、文件模式 [sec: file-mode]** +### **6 文件模式 [sec: file-mode]** 在 UNIX 系统中,每一个文件都有权限设置: @@ -137,7 +139,7 @@ $ ls -al * **242** :以 byte 为单位的文件大小 * **25 nov 19:47** :最后修改日期时间 -#### 6.0.0.1 符号表示 +###### 6.0.0.1 符号表示 文件权限模式由三个区块组成(权限拥有者 user,权限所属组别 group,以及其他人 others ) @@ -180,7 +182,7 @@ fmt.Println(info.Mode()) 上述程序将输出:-rw-r--r-- 。 -#### 6.0.0.2、数字表示 +###### 6.0.0.2 数字表示 此模式还可以通过转换为 八进制 数字转换 (通过 %o 格式化)。 @@ -220,7 +222,7 @@ Go 在调用 os.Openfile 使用数字表示,由 3个数字组成 * Group(所属组别): 7 表示可读、可写、可执行 * Others(其他人): 7 表示可读、可写、可执行 -#### 6.0.0.3 通过 Go 更改文件权限 +###### 6.0.0.3 通过 Go 更改文件权限 在一个文件上,你可以使用 Chmod 方法来更改文件模式 @@ -237,9 +239,9 @@ if err != nil { 为什么 0777 要加0 ?它向 Go 表示该数不是十进制数而是八进制数。 -### **7、写文件:以 CSV 为例** +### **7 写文件:以 CSV 为例** -### 7.1 什么是 CSV +#### 7.1 什么是 CSV CSV 表示逗号分隔值,它是一种用于存储数据的文件格式。 @@ -257,7 +259,7 @@ age,genre,name 65,F,Stephany ``` -#### 7.2、Code +#### 7.2 Code 在本节中,我们将看到如何将数据写入文件。为了立即应用我们的知识,我们将使用一个真实的用例:从一个切片创建一个CSV文件。 @@ -308,13 +310,13 @@ func main() { * 写入文件的字节数 * 错误结果 -#### 7.3、 bytes.Buffer +#### 7.3 bytes.Buffer 我们在前面的程序中使用了一个数据缓冲区,缓冲区是物理存储器的一个区域,当数据从一处移动到另一处时,用来临时存储数据。 若你需要高效的连接字符串或者操作 byte类型的切片时,bytes.Buffer将非常有用。 -### **8、用例** +### **8 用例** 我们将通过下一节内容说明以下用例: @@ -325,7 +327,7 @@ func main() { 在接下来的章节内容中,我们将看到如何使用不同数据库进行操作: -### **9、MySQL数据库** +### **9 MySQL数据库** MySQL是一个开源的关系数据库管理系统。在本节中,我们将看到如何连接到 MySQL 数据库,以及如何用 Go 执行基本的 CRUD 操作。 @@ -335,7 +337,7 @@ Go 没有附带 MySQL 驱动程序,但是标准库定义了操作 SQL 数据 我选择了最受欢迎的 github https://github.com/go-sql-driver/mysql,它是Mozilla公共许可证 2.0 版本。 -#### 9.1、连接 +#### 9.1 连接 ```go // 目录:data-storage/mysql/create/main.go @@ -397,7 +399,7 @@ username:password@protocol(address)/dbname?param=value 通过 Ping 方法可以检查是否连接有效,如果无效,则新建一个。 -#### 9.2、建表 +#### 9.2 建表 第一件事,你应该想在数据库中创建一张数据表,开始编写 SQL 脚本: @@ -477,5 +479,1028 @@ func createTeacher(firstname string, lastname string, db *sql.DB) (int64, error) * 有一个函数 createTeacher,它接受两个字符串参数 ( firstnamelastname) 和一个指向 sql.DB 的指针参数。 * 我们传递给 Exec 方法三个参数。 - * SQL查询(带有?作为值的占位符)和我们想注入到数据库中的值。 + * SQL查询字符串(带有?作为值的占位符)和我们想注入到数据库中的值。 +* db.Exec() 生成预编译语句。 + +##### 9.3.0.1 预编译语句(Prepared Statements) + +遵循MySQL的文档(https://dev.mysql.com/doc/apis-php/en/apis-php-php/en/apis-php-hpli.quickstart.prepared-statements.html),准备好的语句进行两个不同的操作 + +1. 准备阶段(prepare stage):这里,客户端(我们的Go程序)把模板发送到数据库服务器。服务器将对请求执行语法检查,它还将初始化后续步骤的资源。 +2. 绑定和执行阶段:客户端然后将值发送给服务器(在我们的例子中是 John 和 Doe ),然后,服务器将使用这些值构建查询,并最终执行查询。 + +##### 9.3.0.2 关于 SQL 注入 + +​ SQL 注入是一个常见的漏洞,从1988年到2012年,SQL 注入占高严重漏洞的20%。当攻击者(黑客)通过操纵输入到应用程序中的数据,将一系列 SQL 语句插入到查询中时,就发生了 SQL 注入。一种解决方案(不是唯一的)是在代码中使用准备好的语句来防止这种类型的攻击, 我强烈建议您使用准备好的语句,即使它们会导致对服务器的两次调用。 + +#### 9.4 读取一行数据 + +我们将用 select 语句进行读取数据,你可以使用 QueryRow 方法在数据库中进行读取一行数据,这个方法会返回一个 sql.Row 类型的指针实例, 该 sql.Row类型 有一个 Scan 方法,我们可以用它将从 DB 查询到的数据注入到Go变量中 + +```go +// data-storage/mysql/select/main.go + +func teacher(id int, db *sql.DB) (*Teacher, error) { + teacher := Teacher{id: id} + err := db.QueryRow("SELECT firstname, lastname FROM teacher WHERE id = ?", id).Scan(&teacher.firstname, &teacher.lastname) + if err != nil { + return &Teacher{}, err + } + return &teacher, nil +} +``` + +* 函数 teacher 将接受一个 int 整型和一个指向 sql.DB 的指针作为参数。 +* 该整型是数据库中教师的id。 +* 这个函数将使用 QueryRow 对数据库进行查询.。 +* 然后通过 *sql.Row 返回结果行。 +* 然后我们使用 Scan 方法。 + +此方法将从查询结果中提取数据,并将其复制到作为参数传递的变量中。注意Go将为您处理类型转换。 + +在我们的例子中,我们创建了一个 Teacher 结构体类型: + +```go +type Teacher struct { + id int + firstname string + lastname string +} +``` + +不能直接将 Teacher 变量传递给 Scan 方法,必须传递指针类型: + +```go +Scan(&teacher.firstname, &teacher.lastname) +``` + +使用 QueryRow 时,请注意两种常见情况: + +###### 9.4.0.1 查询没有结果 + +Scan 方法将返回一个错误: sql.ErrNoRows + +```go +var ErrNoRows = errors.New("sql: no rows in result set") +``` + +###### 9.4.0.2 多行数据结果 + +Scan 方法从结果集中读取第一条记录返回。 + +#### 9.5 读取多行数据 + +当您查询多行时,必须使用 query 方法并遍历这些行。 + +```go +// data-storage/mysql/selectMultiple/main.go + +rows, err := db.Query("SELECT id, firstname, lastname FROM teacher ") +if err != nil { + return nil, err +} +``` + +db.Query 查询将返回一个类型为 *sql.Rows 的元素,它表示由数据库服务器发送的结果流。读取完查询结果后,调用Close方法,我们将在函数的末尾使用一个 defer 语句来执行它。 + +```go +defer rows.Close() +teachers := make([]Teacher, 0) +for rows.Next() { + // 对结果集的每行数据进行循环迭代 +} +``` + +在前面的代码中,您可以看到一个用于遍历行的惯用方法。我们对 rows.Next() 使用 for 循环。其目的是创建一个空的Teacher 切片 (类型为 Teacher),然后遍历每一行: + +```go +for rows.Next() { + teacher := Teacher{} + if err := rows.Scan(&teacher.id, &teacher.firstname, &teacher.lastname); err != nil { + return nil, err + } + teachers = append(teachers, teacher) +} +``` + +当没有更多的行数据时,for 循环将停止(因为 rows. next() 将返回false)。Scan 用于获取行数据并设置结构字段: + +* for 循环的最后一条指令将把当前的 teacher 变量添加到 teachers 切片中。 +* 在这个循环执行的最后,我们有一个 teachers 切片! + +这里是源代码的完整功能,我们必须进行设计: + +```go +func teachers(db *sql.DB) (*[]Teacher, error) { + rows, err := db.Query("SELECT id, firstname, lastname FROM teacher ") + if err != nil { + return nil, err + } + defer rows.Close() + teachers := make([]Teacher, 0) + for rows.Next() { + teacher := Teacher{} + if err := rows.Scan(&teacher.id, &teacher.firstname, &teacher.lastname); err != nil { + return nil, err + } + teachers = append(teachers, teacher) + } + return &teachers, nil +} +``` + +#### 9.6 更新行(多行) + +如果想要更新一行(或一次更新几行),必须使用SQL的 update 语句,Exec方法对于这种操作是最优的。它返回一个 sql.Result 是一个具有两个方法的类型接口:LastInsertId()rowsaffaffected(),最后一个方法允许检查表中更改了多少行。 + +让我们举个例子,您想要更新 id为1 的老师名字: + +```go +// data-storage/mysql/update/main.go + +res, err := db.Exec("UPDATE teacher SET firstname = ? WHERE id = ?", "Daniel", 1) +if err != nil { + fmt.Println(err) + // 查询不成功时,会有一个报错信息 +} +``` + +通过 sql.Result ,我们可以使用 RowsAffected() 方法来检查只有一行受到影响: + +```go +affected, err := res.RowsAffected() +if err != nil { + fmt.Println(err) + return +} + +if affected != 1 { + fmt.Printf("Something went wrong %d rows were affected expected 1\n", affected) +} else { + fmt.Println("Update is a success") +} +``` + +RowsAffected() 也会返回一个 int64类型数值或者一个错误。 + +#### 9.7 并发 + +sql.DB 类型表示一个或多个数据库连接的池,我们可以安全地在并发程序中使用它。 + +### **10 PostgreSQL 数据库** + +正如MySQL, PostgreSQL 在1996年正式版发布,也是一个开源关系型数据库管理系统。 + +#### 10.1 驱动 + +我们需要通过一个 PostgreSQL的驱动程序,连接到数据库。 + +这个例子将使用Go仓库的 Wiki 推荐的驱动程序 https://github.com/lib/pq,它似乎也是一个非常受欢迎的仓库,总计超过4000 stars和70名贡献者。 + +驱动的文档可以在 godoc 上找到:https://godoc.org/github.com/lib/pq。 + +#### 10.2 连接 + +我们将使用 sql 包的 Open函数: + +```go +// data-storage/postgresql/connection/main.go +package main + +import ( + "database/sql" + "fmt" + + _ "github.com/lib/pq" +) + +func main() { + db, err := sql.Open("postgres", "host=localhost port=5432 user=postgres dbname=school password=pg123 sslmode=disable") + if err != nil { + fmt.Println(err) + return + } + err = db.Ping() + if err != nil { + fmt.Println(err) + return + } +} +``` + +* 你会注意到 sql.Open 的第一个参数是 "postgres"。 +* 在 Go程序内部,会根据参数进行匹配对应的驱动程序 +* 而第二个参数是 DSN (数据库名称) +* 这个字符串是如何连接数据库的作用, 它与 MySQL 的版本有点轻微不同, 我认为更多的是表达上: +* “host=localhost port=5432 user=postgres dbname=school password=pg123 sslmode=disable” + +在实际的应用程序中,您将激活 sslmode (在这里,为了测试目的,我们禁用了它)。此外,不建议在源代码中存储这些参数。例如,您应该从配置文件的值或环境变量构建 DSN 字符串。 + +其他有效参数如下: + +* connect_timeout 这是为等待连接打开而指定的超时。如果你将它设置 为零为空,它将无限期地等待。 +* sslcertsslkeysslrootcert: SSL 支持参数。 + +#### 10.3 新建表 + +PostgreSQL使用SQL语言,但与 MySQL 相比:CREATE TABLE 的语法是不同的。Postgres 类型和 MySQL 类型不一样。 + +这是相似的建表脚本: + +```sql +CREATE TABLE public.teacher +( + id serial, + create_time timestamp without time zone, + update_time timestamp without time zone, + firstname character varying(255), + lastname character varying(255), + PRIMARY KEY (id) +); +``` + +部分特别的说明: + +* serial 被用来作为主键key +* 你可以添加时区作为时间戳。(在 MySQL 中,你可以为每一个数据库连接指定一个时区,默认会使用服务器的时区) +* character varying(255) 被用作存储字符串。 + +为了新建表,我们会使用 Exec 方法执行建表脚本: + +```go +// load SQL script +// use os.Open("create_table.sql") to open the script +// then ioutil.ReadAll to load it's content +// execute the script +res, err := db.Exec(string(b)) +if err != nil { + fmt.Println(err) + return +} +// success, table created ! +``` + +语法类似于 MySQL 数据库中使用的语法。 + +#### 10.4 插入数据 + +对于插入(或更新),适合使用 Exec方法 用于 MySQL 数据库,我们可以对 PostgreSQL 数据库也使用这种方法。但它不是最优的。Exec 将返回一个 Row ,它定义了 LastInsertId方法。此方法将发出请求 + +```sql +SELECT LAST_INSERT_ID(); +``` + +对于PostgreSQL,这个方法将返回一个错误(若这个函数不存在)。相反,我们可以添加 SQL INSERT 语句查询 RETURNING id + +完整的SQL请求如下: + +```sql +INSERT INTO public.teacher (create_time, firstname, lastname) +VALUES (NOW(),$1, $2) +RETURNING id; +``` + +PostgreSQL 不支持"?"字符作为占位符,你需要使用占位符($1,$2) + +我们必须使用 QueryRow方法,然后调用 Scan方法获取插入后的 id值: + +```go +// data-storage/postgresql/insert/main.go + +func createTeacher(firstname string, lastname string, db *sql.DB) (int, error) { + insertedId := 0 + err := db.QueryRow("INSERT INTO public.teacher (create_time, firstname, lastname) VALUES (NOW(),$1, $2) RETURNING id;", firstname, lastname).Scan(&insertedId) + if err != nil { + return 0, err + } + if insertedId == 0 { + return 0, errors.New("something went wrong id inserted is equal to zero") + } + return insertedId, nil +} +``` + +* createTeacher 方法需要三个参数入参(firstname, lastname, 指向 sql.DB 的指针)。 +* 指向变量 InsertedID 的指针将作为参数传递给 Scan 方法。 +* Go 将使用 SQL 查询的结果(即插入的id)更新 insertedId 的值 + +如果报错,Go 会调用 db.QueryRow(..).Scan(...) 返回一个错误。 + +#### 10.5 读取一行数据 + +我们已经创建了一个类型结构体 Teacher ,它将保存与数据库中一个 teacher相关的数据 + +```go +type Teacher struct { + id int + firstname string + lastname string +} +``` + +下面是一个在数据库中检索教师的示例函数: + +```go +// data-storage/postgresql/select/main.go + +func teacher(id int, db *sql.DB) (*Teacher, error) { + teacher := Teacher{} + err := db.QueryRow("SELECT id, firstname, lastname FROM teacher WHERE id > $1 ", id).Scan(&teacher.id, &teacher.firstname, &teacher.lastname) + if err != nil { + return &teacher, err + } + return &teacher, nil +} +``` + +QueryRow 将从数据库中返回一个单独的数据行: + +* 你只会从中获取一行已处理的数据,即使你进行多行数据查询。 +* Scan 函数将从返回数据中提出对应的值。 + +#### 10.6 读取多行数据 + +为了读取多行数据,你应该使用 Query 方法: + +```go +// data-storage/postgresql/select-multiple/main.go +rows, err := db.Query("SELECT id, firstname, lastname FROM teacher") +``` + +Query 会返回一个指向 sql.Rows 的指针,或者一个错误。 + +你必须调用 rows.Close() 释放连接资源: + +```go +// close the connexion at the end of the function +defer rows.Close() +``` + +接着,我们使用 rows.Next() 进行循环迭代: + +```go +for rows.Next() { + //这这里处理每行当前行数据 +} +``` + +在循环中,我们使用 Scan 方法从当前行中提取数据: + +```go +err := rows.Scan(&id, &firstname, &lastname) +``` + +##### 10.6.1 Zoom(扫描方法) + +convertAssign 函数(在 sql 包中)会被调用: + +```go +func convertAssign(dest, src interface{}) error { + //... +} +``` + +* dest 参数是一个指向终点的变量 +* src 是数据库驱动返回的源参数 +* 此函数会检索 destsrc 参数,且此函数超过200+行的源代码。我们只介绍前面部分代码,不进行全局介绍。 + +```go +// extract of function convertAssign(提取 convertAssign 函数) +// (line 209, src/database/sql.go) +func convertAssign(dest, src interface{}) error { + // Common cases, without reflect. + switch s := src.(type) { + case string: + switch d := dest.(type) { + case *string: + if d == nil { + return errNilPtr + } + *d = s + return nil + case *[]byte: + if d == nil { + return errNilPtr + } + *d = []byte(s) + return nil + case *RawBytes: + if d == nil { + return errNilPtr + } + *d = append((*d)[:0], s...) + return nil + } + //... +} +``` + +函数由一个 switch 语句组成,我们使用了以下语句转换了 src 类型: + +```go +s := src.(type) +``` + +如果 src 变量是一个字符串,我们将提取 dest 参数的类型。 + +```go +d := dest.(type) +``` + +另外启动一个 switch 语句,若 src 是一个字符串,则 dest 是一个 *[]byte 指针,将执行以下小片段代码: + +```go +if d == nil { + return errNilPtr +} +*d = []byte(s) +return nil +``` + +该函数将测试目标指针是否为空。如果不是,dest 的值将被更改为 []byte(s) + +#### 10.7 更新一行(or 多行)数据 + +像 MySQL 一样,合适的方法是Exec。该方法将返回一个实现 sql.Result 接口的类型。res.RowsAffected 将返回表中受 UPDATE 语句影响的行数,你可以通过控制这个值来控制请求对表的影响: + +```go +// data-storage/postgresql/update/main.go + +res, err := db.Exec("UPDATE teacher SET firstname = $1 WHERE id = $2", "Daniel", 1) +// check that there is no error +// get the number of affected rows +affected, err := res.RowsAffected() +//... +``` + +对整个例子上看,与 MySQL 不同的只是格式化的占位符,使用 "$n" 代替 "?" ,n 是SQL语句中对应的参数位置。 + +### **11 MongoDB** + +2007年,MongoDB 由 10gen 公司创建。MongoDB是一个专有系统,是平台即服务应用程序的一部分,由其创建者作为一个开源数据库发布。 + +MongoDB 是 NoSQL 趋势的一部分,NoSQL 这个词是 Johan Oskarsson 在2009年一次关于数据库的聚会上创造的。 + +NoSQL 数据库没有严格的定义,但是它们有两个共同的特征: + +* 它们是面向文档的(它们存储文档而不是表中的行) +* 它们被设计成高度可伸缩的 + +本节将重点介绍基本的 MongoDB 操作(CRUD)。但是首先,让我们学习一些重要的词汇: + +#### 11.1 MongoDB 词汇 + +使用 MongoDB BSON格式,存储 JSON 格式的字符串; + +使用 MongoDB 文档(documents)格式,存储 JSON 的二进制数据。 + +文档设置(没有规定在集合中存储同类型的文档数据) + +集合设置 + +#### 11.2 驱动 + +要连接使用我们的数据库,可以使用 MongoDB 的官方驱动程序:github.com/mongodb/mongo-go-driver。 + +#### 11.3 连接 + +```go +// data-storage/mongodb/official-driver/connection/main.go +package main + +import ( + "context" + "fmt" + "time" + + "github.com/mongodb/mongo-go-driver/mongo" +) + +func main() { + client, err := mongo.NewClient(`mongodb://username:password@localhost:27094/db`) + if err != nil { + panic(err) + } + //... + +} +``` + +我们通过数据库连接的字符串,使用函数 NewClient 的方式,新建一个 mongo 客户端,其如下格式: + +```go +mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]] +``` + +您可以指定多个主机创建客户端。 请注意,我在例子中指定了连接字符串末尾的数据库,这不是强制性的。对于某些云数据库提供商,它必须在连接字符串中指定数据库以进行身份认证。 这些云提供商共享其客户端之间的 MongoDB 服务器,并通过数据库处理身份验证。 + +创建客户端之后,我们将连接到数据库: + +```go +ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) +defer cancel() +err = client.Connect(ctx) +if err != nil { + panic(err) +} +``` + +我们在以前代码片段中所做的第一件事是为连接创建context,context 是标准 Go Package(我专门为此包的章节),它将定义连接的特定超时(此处3秒)。 如果程序没有成功进行连接,则会将通过以下信息进行 panic: + +```go +panic: context deadline exceeded +``` + +连接到数据库之后,可以使用以下代码片段 ping 服务器 + +```go +err = client.Ping(ctx, nil) +if err != nil { + panic(err) +} +``` + +#### 11.4 新建一个文档 + +数据存储在文档中,文档是JSON字符串,文档存储在集合中, 因此,第一步是在数据库中检索集合: + +```go +collection := client.Database("db").Collection("school") +``` + +注意,如果集合不存在,它将被自动创建。 + +```go +// data-storage/mongodb/official-driver/create/main.go + +res, err := collection.InsertOne(context.Background(), bson.M{"hello": "world"}) +if err != nil { + panic(err) +} +id := res.InsertedID +fmt.Println(id) +``` + +这里我们使用 InsertOne 方法,该方法将 context 作为其第一个参数(这里同样可以定义一个超时参数),将文档作为第二个参数。此包没有强制使用指定的类型作为第二个参数。 + +```go +func (coll *Collection) InsertOne(ctx context.Context, document interface{}, opts...*options.InsertOneOptions) (*InsertOneResult, error) +//第二个参数类型是空接口类型 +``` + +在例子中,可以成功调用该函数: + +```go +bson.M{"firstname":"John", "lastname":"Doe", "create_time":time.Now()} +``` + +bson 是驱动程序的一个子包,M类型是 map[string]interface{} 的类型别名。下面是插入到数据库中的文档 + +```json +{ + "_id": { + "$oid": "5c0919b285d8ae1a8afe6c80" + }, + "lastname": "Doe", + "create_time": { + "$date": "2018-12-06T12:44:34.168Z" + }, + "firstname": "John" +} +``` + +MongoDB 生成 id,然后您可以看到 date 属性被专门处理(稍后在过滤器中使用)。 + +#### 11.5 读取一个文档 + +###### 11.5.0.1 属性查询 + +检索文档中的一个属性,我们新建一个过滤器变量: + +```go +filter := bson.D{{"firstname", "Jeanne"}} +``` + +bson是 Go MongoDB Driver的子包,它作为实用工具对 BSON 进行解析和生成。bson.D 类型表示一个BSON文档(D代表文档)。内部,bson.D作为一个元素,是 primitive.E 的切片, primitive.E带有两个公有字段Key(字符串类型)和Value(空类型接口)。 + +这里,我们过滤一个属性 **firstname** 设置为 **“Jeanne”**的文档,我们定义一个可以存储结果的变量: + +```go +type Teacher struct { + Firstname string + Lastname string +} +``` + +接着,我们新建一个类型为 Teacher 的结构体赋值给查询结果变量: + +```go +result := Teacher{} +``` + +然后,我们用集合的实例查询数据库: + +```go +// data-storage/mongodb/official-driver/read/main.go + +collection := client.Database(databaseName).Collection(collectionName) +err = collection.FindOne(context.Background(), filter).Decode(&result) +if err != nil { + panic(err) +} +// One document was found +fmt.Printf("%+v", result) +``` + +* 首先,检索集合(我们查询数据库,然后收集起来) +* 数据库命名和集合命名保存在里面的变量中 +* 使用 FindOne 方法,服务器仅会返回一个结果,匹配提供的过滤器。而此方法需要两个参数: + * context类型参数 + * filter +* 使用 FindOne 方法返回一个指向 SingleResult 元素类型的指针。 +* 然后我们直接调用 SingleResult 方法。(我们可以用两个不同的步骤来完成,但是这样代码更简单) + +如果没有结果怎么办?在这种情况下,Decode 将返回一个错误,我们的程序会 panic : + +```go +panic: mongo: no documents in result +``` + +###### 11.5.0.1 文档 id 查询 + +MongoDB 中的每个文档都有一个 property_id,我们可以将它与关系数据库引擎中的主键进行比较。 + +这个字段有一个特殊的类型,叫做 ObjectId ,它有以下属性: + +* 由 12 个字节组成 +* 由 4 个部分组成(从UNIX时代以来的秒数、机器标识符、进程id和计数器) +* 由客户端生成 + +在构建查询之前,必须创建一个 ObjectId 类型的变量 + +我们正在寻找 id="5c091d3b734016209db89f76" 的文档: + +```go +// data-storage/mongodb/official-driver/readById/main.go + +import "github.com/mongodb/mongo-go-driver/bson/objectid" + +//... +oid , err := objectid.FromHex("5c091d3b734016209db89f76") +if err != nil { + panic(err) +} +``` + +变量 oid 可以在查询中使用,让我们创建过滤器变量来检索文档: + +```go +filter := bson.M{"_id": oid} +``` + +接着,我们调用 FindOne 方法: + +```go +result := Teacher{} +err = collection.FindOne(context.Background(), filter).Decode(&result) +if err != nil { + panic(err) +} +fmt.Printf("%+v", result) +// {Id:ObjectID("5c091d3b734016209db89f76") Firstname:Jeannoti Lastname:Doe CreateTime:2018-12-06 13:59:39.338 +0100 CET} +``` + +我们创建一个全新的 Teacher 类型变量来保存查询结果。 + +结果集的属性通过Decode方法进行解析绑定(我们向 Decode 传递一个指向变量 result 的指针)。 + +###### 11.5.0.3 查询选择器 + +MongoDB 查询包含一个类似 json 的对象。要构建查询,必须使用查询选择器。这些选择器以美元符号开始,例如 **$eq** 表示等于,**$neq** 表示不等于。本节将回顾,然后,我们将构建一个查询。 + +**比较选择器** + +| 选择器 | 定义 | 查询示例 | 解释描述 | +| ------ | --------------------------------- | --------------------------------- | ---------------------------------- | +| $eq | equal(等于) | { firstname: { $eq: John } } | firstname属性值等于 John | +| $ne | not equal(不等于) | { lastname: { $ne: Doe } } | lastname属性值不等于 Doe | +| $gt | greater than(大于) | { age: { $gt: 12 } } | age属性值严格大于12 | +| $gte | greater than or equal(大于等于) | { age: { $gte: 42 } } | age属性值大于或等于42 | +| $lt | less than(小于) | { monthlyIncome: { $lt: 1000 } } | monthlyIncome属性值小于 1000 | +| $lte | less than or equal(小于等于) | { monthlyIncome: { $lte: 1000 } } | monthlyIncome属性值小于或等于 1000 | +| $in | in array (在数组内) | { age: { $in: [20,42] } } | age属性值等于 20 或 42 | +| $nin | not in array(不在数组内) | { age: { $nin: [20,30] } } | age属性值不等于 20 或 3 | + +**逻辑选择器** + +| 选择器 | 定义 | 查询示例 | 解释描述 | +| ------ | ------------- | ------------------------------------------------------------ | ------------------------------------------- | +| $and | AND(且) | { $and: [{ age: { $gt: 60 } },{ firstname: { $eq: John} } ] } | age属性值大于60 且 firstname属性值等于 John | +| $or | OR(或) | { $or: [{ age: { $gt: 12 } }, { age: { $lt: 3 } }] } | age属性值大于 12 或 小于 3 | +| $nor | NOR(异或) | { $nor: [{ age: { $gt: 12 } },{ age: { $lt: 3 } } ] } | age属性值不大于2 异或小于且不小于3 | +| $not | NOT(异或非) | { $not: [{ age: { $gt: 12 } }] } | age属性值不大于 12 | + +让我们为学校集合创建一个查询,我们想检索将 John 作为 FirstNameDoe作为 LastName 的所有老师: + +```json +{ + "$and": [{ + "firstname": { + "$eq": "John" + } + }, + { + "lastname": { + "$eq": "Doe" + } + } + ] +} +``` + +这里我们使用 $and 逻辑选择,$and的值是一个查询子句的数组。 + +这里的数组有两个元素:2个查询将由一个逻辑 AND 连接起来。我们查询 firstname的值等于 John 并且 lastname 的值等于 Doe的老师。 + +```go +// data-storage/mongodb/official-driver/query-advanced/main.go + +//.. + +q1 := bson.M{"firstname": bson.M{"$eq": "John"}} +q2 := bson.M{"lastname": bson.M{"$eq": "Doe"}} +``` + +q1q2 是 以 string 为 key 、空接口类型为 value 的 map 结构的 bson.M 类型数据。 + +开始分析 q1 变量:"firstname" 作为 key 同时 value 是以 "$eq" 为 key、"John" 为值的 bson.M 类型数据。要加入到两个查询,需要先创建一个 bson.A 类型的 BSON 数组(代替数组类型): + +```go +clauses := bson.A{q1, q2} +``` + +接着, 使用 $and 选择器定义主查询,然后作为 bson.M 类型的一个元素: + +```go +filter := bson.M{"$and":clauses } +``` + +发送查询请求后,可以通过使用 **Collection** 类型的Find 方法: + +```go +cur, err := collection.Find(context.Background(), filter) +if err != nil { + panic(err) +} +``` + +常见, Find 方法会返回一个 Cursor 游标或者报错信息,Cursor 是一个指向查询结果集的指针,我们可以通过使用 Next 方法循环迭代 Cursor,此方法执行后,如果下一行记录存在且没有错误,会返回一个布尔类型的值 true + +每个数据结果代表一个文档,通过调用 DecodeBytes 获取文档数据: + +```go +defer cur.Close(context.Background()) +for cur.Next(context.Background()) { + raw, err := cur.DecodeBytes() + if err != nil { + panic(err) + } + fmt.Printf("%s\n", raw) +} +if err := cur.Err(); err != nil { + panic(err) +} +``` + +当函数返回(returns)时,首先需要调用 **defer** cur.Close(context.Background()) 关闭 cursors 。在此之前,通过 for 循环,迭代指针游标(cursor)。在循环中,检索每个 raw 的文档值,将它赋值给 raw 变量。 + +在出错情况下,for 循环会停止, 下一个语句会继续执行。而我们会通过调用 Err 方法,获取错误信息,在这种情况程序会进行 panic。 + +####### 11.5.0.3.1 关于MongoDB cursors 的说明 + +Cursors 会出现超时,在一定的非 活动期间,意味着如果对结果集进行长时间迭代循环,会导致 cursor 在服务端超时关闭。在查询过程中,你可以指定 cursor 的类型,它主要有以下两种: + +* 可跟踪的:此类 cursor 可以保持打开状态,甚至循环到最后一条数据后,仍然可以重新使用起来。 +* 不可跟踪的:当循环到最后一条数据后,此类 cursor 会自动关闭。 + +默认是不可跟踪的 cursor。 + +#### 11.6 更新文档 + +在 MongoDB 中更新文档,你可以使用其提供的两个 BSON 对象: + +* 查询在更新的时候会查找文档(documents),因为它是过滤器 +* 需要更新时,查找到的文档(document)会作为数据被更新。 + +举个例子,我们需要更新一个文档,这个文档的属性名 **firstname**的值为 John ,同时更新属性名 **lastname** 的值为 CoffeeBean。 + +过滤器: + +```json +{ + "firstname": { + "$eq": "John" +} +``` + +简化查询后: + +```json +{"firstname":"John"} +``` + +更新如下: + +```json +{ + "$set": { + "lastname": "CoffeeBean" +} +``` + +接下来,创建 2 个 BSON 对象: + +```go +// data-storage/mongodb/official-driver/update/main.go +//... +filter := bson.M{"firstname": "John"} +update := bson.M{"$set": bson.M{"lastname": "CoffeeBean"}} +``` + +我们将使用 FindOneAndUpdate 方法,将 context 类型变量,bson类型过滤变量,还有需要更新的 bson,如下: + +```go +res := collection.FindOneAndUpdate(context.Background(), filter, update) +``` + +这个方法会返回一个指向 DocumentResult 类型的指针,我们通过 Decode 方法解析返回的文档(document)。如果过滤器(filter)没有返回文档数据,则也没有数据被更新,也就没有文档数据会被返回。这种情况下,会返回一个错误信息: + +```go +resDecoded := Teacher{} +err = res.Decode(&resDecoded) +if err != nil { + panic(err) +} +fmt.Printf("%+v", resDecoded) +``` + +程序将输出: + +```json +{Id:ObjectID("5c091d3b734016209db89f76") Firstname:John Lastname:CoffeeBean CreateTime:2018-12-06 23:59:39.338 +0100 CET} +``` + +### **12 ElasticSearch ** + +ElasticSearch 是一款开源搜索引擎软件,由 **Shay Banon** 开发,其第一个版本在2010年进行发布。2018年,在 **Stackoverflow**开发者调查中,ElasticSearch 跻身前10名最受欢迎的数据库。 + +ElasticSearch主要用于开发搜索引擎。它是用 Java 开发的,基于Apache Lucene。Apache Lucene是由Apache软件基金会开发的,是一个完全用 Java 编写的高性能、全功能的文本搜索引擎库。这种技术几乎适用于任何需要全文搜索的应用程序,特别是跨平台的应用程序。ElasticSearch被设计为易于扩展,并且它公开了一个REST接口来执行CRUD操作。 + +搜索引擎的基本单位不是一行而是一个文档。弹性搜索文档是一个 JSON 对象。每个文档的结构不是固定的。它是一个无模式数据库。 + +在本节中,我们将在 MIT 许可证下使用客户端库 **https://github.com/olivere/elastic** + +#### 12.1 词汇 + +定义一些重要概念: + +**Indexing** + +将数据存储到ElasticSearch中的动作。数据被组织成文档。 + +**Document** + +表示数据的 JSON对象。一个文档有一个属于其对应的索引(index) + +**Index** + +ElasticSearch集群可以包含多个索引(索引的复数 indces)。从关系的角度来看,索引可以看作是一个表。 + +**Type** + +ElasticSearch中存储的每个文档都有一个特定的类型。如果您想存储教师的名字、姓氏和到校日期,您将创建一个类型为teacher的文档。 + +**Field** + +类型由字段组成。 例如,类型教师将拥有FightName和LastName的字段 + +###### 12.1.0.1废弃类型(Type) + +请注意,类型在已弃用的过程中。 在未来的 Elasticsearch API 中将去掉类型的概念。 如果您想了解更多有关该弃用的信息,请参阅 https://www.elastic.co/guide/en/elasticsearch/reference/current/removal-of-types.html + +我们将遵循推荐的方法,是创建一个有字段类型的类型文档 + +#### 12.2 连接:初始化客户端 + +ElasticSearch 被设计为在集群服务器上运行的软件。服务器称为节点,集群有一个主节点,主节点不固定,主容量可以归属于其他节点。Go 客户端被设计为自动检测新节点并删除集群中不再使用的节点。 + +要创建客户端,你必须传递集群节点的地址(主机+端口): + +```go +// data-storage/elasticsearch/create-index/client/main.go +//... +client, err := elastic.NewClient(elastic.SetURL("http://127.0.0.1:9200")) +``` + +如果你的集群中有其他节点,只需将其他地址添加到 SetURL 函数即可: + +```go +client, err := elastic.NewClient(elastic.SetURL("http://127.0.0.1:9200", "http://127.0.0.1:9300")) +``` + +这个客户端被设计成可以在整个应用程序中使用,有了这个客户端,您可以执行 CRUD 操作。注意,您也可以用一个简单的 HTTP 请求来执行这些操作。 + +#### 12.3 新建一个索引 + +在存储任何文档之前,我们必须创建索引。ElasticSearch 中的文档存储在索引中,索引具有一个用字段定义类型的映射。我们将命名我们的索引“学校”。 + +首先需要定义映射: + +```go +const Mapping = `{ + "mappings": { + "_doc": { + "properties": { + "type": { "type": "keyword" }, + "firstname": { "type": "text" }, + "lastname": { "type": "keyword" }, + "create_time": { "type": "date" }, + "update_time": { "type": "date" } + } + } + } +}` +``` + +###### 12.3.1 使用 Http 请求 + +要创建索引,你必须发出一个 http 请求 + +```http +PUT http://127.0.0.1:9200/{indexName} +{ +"mappings" : { + //... + } +} +``` + +要使用 Go 发出请求,可以使用以下代码片段: + +```go +// data-storage/elasticsearch/create-index/rest/main.go +//... +client := &http.Client{} + +// prepare the request +request, err := http.NewRequest("PUT", "http://127.0.0.1:9200/school",strings.NewReader(Mapping)) +request.Header.Add("Content-Type", "application/json") + +// execute the request +response, err := client.Do(request) + if err != nil { + log.Fatal(err) + } else { + defer response.Body.Close() + contents, err := ioutil.ReadAll(response.Body) + if err != nil { + log.Fatal(err) + } + fmt.Printf("%s", contents) + } +} +``` + +它将输出内容如下: + +```json +{"acknowledged":true,"shards_acknowledged":true,"index":"school"} +``` + +##### 12.3.2 使用 Go 客户端 + +```go +// data-storage/elasticsearch/create-index/client/main.go +//... + +createIndex, err := client.CreateIndex("school").BodyString(Mapping).Do(ctx) +if err != nil { + // Handle error + fmt.Println(err) + return +} +if !createIndex.Acknowledged { + // Not acknowledged + fmt.Println("not acknoledge !") + return +} +fmt.Println("OK") +``` + +#### 12.4 插入/更新文档 + +使用 ElasticSearch,创建文档的端点也是更新文档的端点。 + +##### 12.4.1 使用 Http 的 Put 方式请求 +要创建文档,你只需在URL "http://127.0.0.1:9200/(/namexname}" 中提出请求,其中 **{id}** 是您你想要在索引 **{indexName}** 的文件的ID 想要在索引{IndexName}中添加。 \ No newline at end of file -- Gitee From da0d381ecc5340754c601e3ae6567ae597727604 Mon Sep 17 00:00:00 2001 From: windqiu Date: Sun, 13 Jun 2021 23:52:11 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E5=AE=8C=E6=88=90=E7=BF=BB=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...er 29 Data storage files and databases.md | 446 +++++++++++++++++- 1 file changed, 442 insertions(+), 4 deletions(-) diff --git a/Chapter 29 Data storage files and databases.md b/Chapter 29 Data storage files and databases.md index 4c98cc8..a04f255 100644 --- a/Chapter 29 Data storage files and databases.md +++ b/Chapter 29 Data storage files and databases.md @@ -482,14 +482,14 @@ func createTeacher(firstname string, lastname string, db *sql.DB) (int64, error) * SQL查询字符串(带有?作为值的占位符)和我们想注入到数据库中的值。 * db.Exec() 生成预编译语句。 -##### 9.3.0.1 预编译语句(Prepared Statements) +###### 9.3.0.1 预编译语句(Prepared Statements) 遵循MySQL的文档(https://dev.mysql.com/doc/apis-php/en/apis-php-php/en/apis-php-hpli.quickstart.prepared-statements.html),准备好的语句进行两个不同的操作 1. 准备阶段(prepare stage):这里,客户端(我们的Go程序)把模板发送到数据库服务器。服务器将对请求执行语法检查,它还将初始化后续步骤的资源。 2. 绑定和执行阶段:客户端然后将值发送给服务器(在我们的例子中是 John 和 Doe ),然后,服务器将使用这些值构建查询,并最终执行查询。 -##### 9.3.0.2 关于 SQL 注入 +###### 9.3.0.2 关于 SQL 注入 ​ SQL 注入是一个常见的漏洞,从1988年到2012年,SQL 注入占高严重漏洞的20%。当攻击者(黑客)通过操纵输入到应用程序中的数据,将一系列 SQL 语句插入到查询中时,就发生了 SQL 注入。一种解决方案(不是唯一的)是在代码中使用准备好的语句来防止这种类型的攻击, 我强烈建议您使用准备好的语句,即使它们会导致对服务器的两次调用。 @@ -1432,7 +1432,7 @@ const Mapping = `{ }` ``` -###### 12.3.1 使用 Http 请求 +##### 12.3.1 使用 Http 请求 要创建索引,你必须发出一个 http 请求 @@ -1503,4 +1503,442 @@ fmt.Println("OK") ##### 12.4.1 使用 Http 的 Put 方式请求 -要创建文档,你只需在URL "http://127.0.0.1:9200/(/namexname}" 中提出请求,其中 **{id}** 是您你想要在索引 **{indexName}** 的文件的ID 想要在索引{IndexName}中添加。 \ No newline at end of file +要创建文档,你只需在URL ""http://127.0.0.1:9200/{indexName}/{typeName}/{id}"" 中提出请求,其中 **{id}** 是你想要在索引 **{indexName}** 添加的文档 ID。 + +例子如下: + +```http +PUT http://127.0.0.1:9200/school/_doc/42 +{ + "type":"teacher", + "firstname":"John", + "lastname":"Doe", + "create_time":"2018-09-10 00:00:00" +} +``` + +使用以下代码块执行插入 + +```go +// data-storage/elasticsearch/insert/rest/main.go + +//... + +const NewTeacher = ` +{ + "type":"teacher", + "firstname":"John", + "lastname":"Doe", + "create_time":"2018-09-10T00:00:00" +}` + +const ClusterUrl = "http://127.0.0.1:9200" +const IndexName = "school" +const Username = "myUsername" +const Password = "myPassword" +const TypeName = "_doc" +const ObjectId = 42 + +func main() { + + url := fmt.Sprintf("%s/%s/%s/%d", ClusterUrl, IndexName, TypeName, ObjectId) + client := &http.Client{} + request, err := http.NewRequest("PUT", url, strings.NewReader(NewTeacher)) + request.Header.Add("Content-Type", "application/json") + // if your cluster requires authentification + token := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", Username, Password))) + request.Header.Add("Authorization", fmt.Sprintf("Basic %s", token)) + + response, err := client.Do(request) + if err != nil { + log.Fatal(err) + } else { + defer response.Body.Close() + contents, err := ioutil.ReadAll(response.Body) + if err != nil { + log.Fatal(err) + } + fmt.Printf("%s", contents) + } +} +``` + +以上程序的代码将输出一个 JSON对象: + +```json +{ + "_index": "school", + "_type": "_doc", + "_id": "42", + "_version": 1, + "result": "created", + "_shards": { + "total": 2, + "successful": 2, + "failed": 0 + }, + "_seq_no": 3, + "_primary_term": 1 +} +``` + +可以看到我们的文档已经成功地在两个碎片上创建,文档版本号为1。你将看到,每次更新文档时,此属性都会增加。 + +##### 12.4.2 使用 Go 客户端 + +首先,新建一个 Teacher 结构体 + +```go +// data-storage/elasticsearch/insert/client/main.go + +type Teacher struct { + Firstname string `json:"Firstname"` + Lastname string `json:"Lastname"` + CreateTime time.Time `json:"create_time"` + UpdateTime time.Time `json:"update_time"` +} +``` + +你可以新建一个元素,插入到 ElasticSearch: + +```go +teacher := Teacher{firstname: "John", lastname: "Doe", createTime: time.Now()} +``` + +客户端的 API 非常简单: + +```go +put, err := client.Index(). + Index("school"). + Type("_doc"). + Id("42"). + BodyJson(teacher). + Do(ctx) +if err != nil { + // Handle error + panic(err) +} +``` + +你必须用 BodyJson 方法精确索引名称、类型、Id以及最后的 JSON文档 + +put变量的类型是 elastic.IndexResponse,我们可以稍后检查文档是否已经创建或更新: + +```go +if put.Result == "updated" { + fmt.Println("document has been updated") + fmt.Printf("version N. : %d\n", put.Version) +} +if put.Result == "created" { + fmt.Println("document has been created") +} +``` + +#### 12.5 通过 Id 读取文档 + +要获得具有 id 的文档,你必须发送一个 GET 类型的 HTTP 请求。例如,如果您想在索引”学校“中检索 id 为 42 的 type_doc 文档,你必须发起以下请求: + +```http +GET http://127.0.0.1:9200/school/_doc/42 +``` + +##### 12.5.1 其他 + +首先,构建一个 URL: + +```go +url := fmt.Sprintf("%s/%s/%s/%d?pretty=true", ClusterUrl, IndexName, TypeName, ObjectId) +``` + +这里我们使用 Sprintf 来生成请求URL, ClusterUrlIndexNameTypeNameObjectId是在文件开头定义的常量。请注意,我们在请求中添加了参数 ?pretty=true,以通知服务器我们希望返回缩进的JSON结果 + +接着,我们开始新建一个标准的 Http 客户端,然后构建请求: + +```go +// data-storage/elasticsearch/read/by-id/rest/main.go +//... + +client := &http.Client{} +request, err := http.NewRequest("GET", url, nil) +// add the authorization header +request.Header.Add("Authorization", fmt.Sprintf("Basic %s", token)) +``` + +下一步,发送请求并获得返回结果: + +```go +response, err := client.Do(request) +if err != nil { + log.Fatal(err) +} else { + defer response.Body.Close() + contents, err := ioutil.ReadAll(response.Body) + if err != nil { + log.Fatal(err) + } + fmt.Printf("%s", contents) +} +``` + +上面代码片段将输出: + +```go +{ + "_index" : "school", + "_type" : "_doc", + "_id" : "42", + "_version" : 3, + "found" : true, + "_source" : { + "type" : "teacher", + "firstname" : "John", + "lastname" : "Doe", + "create_time" : "2018-09-10T00:00:00" + } +} +``` + +然后,您可以解析这个 JSON 字符串(使用 JSON . unmarshall)并提取 found 的布尔属性的值,并对其进行测试,以告知用户您的应用程序已经检索到一个结果 + +#### 12.5.2 Go 客户端 + +你可以使用 GET 方法: + +```go +// data-storage/elasticsearch/read/by-id/client/main.go +//... +get, err := client.Get(). + Index(IndexName). + Type(TypeName). + Id(ObjectId). + Do(ctx) +if err != nil { + // error + // index not found + // document with this id not found + // ... + panic(err) +} +if get.Found { + // document was found +} +``` + +这里有一个 *elastic.GetResult 类型的元素将被返回。 Found 属性允许你检查是否检索了文档,还可以获得版本(Version)、Id 等 + +#### 12.6 搜索 + +ElasticSearch 是一个很好的搜索引擎, 我们可以使用其余的 API 对索引执行搜索操作。我们的目标不是涵盖所有的引擎搜索功能,而是向你展示如何让它与 Go 一起工作。让我们开始吧! + +##### 12.6.1 基本请求 + +ElasticSearch 公布了一个为搜索而设计的 REST GET端点。例如,如果你想找到所有名字等于 John 的老师,我们可以使用下面的请求: + +``` +GET school/_search +{ + "query": { + "bool": { + "must": { + "match": { + "firstname": "Danny" + } + }, + "filter": { + "match": { + "type": "teacher" + } + } + } + } +} +``` + +##### 12.6.2 其他 + +我们已经创建了常量 SearchJSON,它将保存指向服务器的 JSON 查询。在实际情况中,你可能希望让用户来发起一个请求情况,但是要小心潜在的查询注入! + +```go +// data-storage/elasticsearch/read/search/rest/main.go +// ... + +url := fmt.Sprintf("%s/_search?pretty=true", ClusterUrl) +client := &http.Client{} +request, err := http.NewRequest("GET", url, strings.NewReader(SearchJSON)) +request.Header.Add("Content-Type", "application/json") +request.Header.Add("Authorization", fmt.Sprintf("Basic %s", token)) +``` + +接着,我们发起一个请求,准确来说前面小节的代码... + +如果没有返回结果,ElasticSearch 会返回 JSON 格式的结果,如下所示: + +```json +{ + "took" : 1, + "timed_out" : false, + "_shards" : { + "total" : 4, + "successful" : 4, + "skipped" : 0, + + "failed" : 0 + }, + "hits" : { + "total" : 0, + "max_score" : null, + "hits" : [ ] + } +} +``` + +如果你一个或多个结果返回,你必须处理以下 JSON: + +```json +{ + "took" : 2, + "timed_out" : false, + "_shards" : { + "total" : 4, + "successful" : 4, + "skipped" : 0, + "failed" : 0 + }, + "hits" : { + "total" : 1, + + "max_score" : 0.9808292, + "hits" : [ + { + "_index" : "school", + "_type" : "_doc", + "_id" : "72", + "_score" : 0.9808292, + "_source" : { + "type" : "teacher", + "firstname" : "Danny", + "lastname" : "Doe", + "create_time" : "2018-09-10T00:00:00" + } + } + ] + } +} +``` + +##### 12.6.3 Go 客户端 + +Go 客户端有很多方法来处理对 ElasticSearch 的查询,但也有聚合。它的 API 非常丰富。因此,本节将不深入讨论细节。 + +下面是如何构建一个查询的示例,该查询将返回 **firstname** 属性为 Danny 的所有文档: + +```go +// data-storage/elasticsearch/read/search/client/main.go +// ... + +q := elastic.NewBoolQuery() +q.Must(elastic.NewMatchQuery("firstname", "Danny")) +``` + +然后可以将此查询传递到客户端 + +```go +res, err := client.Search(). + Index(IndexName). // search in index "twitter" + Query(q). + Pretty(true). // pretty print request and response JSON + Do(ctx) // execute +if err != nil { + // do something when an error is raised +} +// Success! the query returned 0 or more results +``` + +res 变量是 *elastic.SearchResult 指针,该类型具有有趣的属性: + +int64 类型的请求耗时 + + *elastic.SearchHits 指针类型的结果集 + +如果查询超时,布尔值将被赋值 true值 + +看看,如何使用 res.Hits : + +```go +if res.Hits.TotalHits > 0 { + fmt.Printf("Found %d hits\n", res.Hits.TotalHits) + // Iterate through results + for _, hit := range res.Hits.Hits { + var t Teacher + err := json.Unmarshal(*hit.Source, &t) + if err != nil { + panic("impossible to deserialize") + } + // 享受你的最新 Teacher 实例吧! + fmt.Printf("Teacher found firsname %s - lastname : %s\n", t.firstname, t.lastname) + } +} else { + // No results +} +``` + +首先检查 TotalHits 属性是否大于零,如果是真的,那就值得检验一下结果 + +然后使用 for 循环遍历 Hits,它是 \*SearchHit 指针类型元素的一个片段。对于 \*hit.Source 我们可以访问由服务器返回的 **_source** json属性: + +```json +//... +"_source" : { + "type" : "teacher", + "firstname" : "John", + "lastname" : "Doe", + "create_time" : "2018-09-10T00:00:00" +} +//... +``` + +我们使用 JSON.unmarshal 将 JSON 字符串转换为 Teacher 类型的变量。变量 t 可以在之后被程序使用。 + +### 13 自我测试 + +#### 13.1 问题 + +1. 什么是文件模式? +2. 如何新建一个 SQL 数据库连接? +3. 在SQL数据库中,你可以使用哪个方法执行 INSERT 和 UPDATE 的查询构造? +4. 你可以使用哪个方法来解析 SQL请求的结果集? + +#### 13.2 答案 + + 1. 什么是文件模式? + 1. 文件模式说明权限的设置与文件相关联的。 + 2. 如何新建一个 SQL 数据库连接? + 1. 安装匹配的驱动(driver) + 2. 调用 sql.Open 方法 + 3. 在SQL数据库中,你可以使用哪个方法执行 INSERT 和 UPDATE 的查询构造? + 1. Exec + 4. 你可以使用哪个方法来解析 SQL请求的结果集? + 1. Scan + +### 14 关键点 + +* 使用 os.Create 方法新建一个文件 +* 使用 ioutil.WriteFile 方法新建一个文件并写入 +* 在 UNIX 系统中,每个文件都有设置权限 +* 你可以通过 os.Chmod 函数改变文件权限 +* 安装数据库驱动后,就可以在程序中使用 SQL 数据库了。 +* 不要忘了匿名导入你的 SQL 驱动 + * sql.Open 可以连接一个数据库 + * 它将返回一个指向 sql.DB指针,并拥有以下方法 + * Exec :执行原生查询(inserts、update、。。。) + * QueryRow :查询单行数据 + * Query:查询多行数据 + * 你可以使用 Scan 方法将结果集赋值给变量 +* 你还可以使用 **MongoDB** 和 **ElasticSearch** 数据库来扩大开源客户端。 + +------ + +1. 八进制是一种以8为基数的数字系统 (如十进制十六进制或二进制)。八进制中的每一个数字都等于0、1、2、3、4、5、6或7 +2. 资源:https://en.wikipedia.org/wiki/Data_buffer +3. 访问:https://www.postgresql.org/docs/9.0/libpq-ssl.html +4. NoSQL 表示不仅仅只有 SQL \ No newline at end of file -- Gitee From d1a21e48b83570d627d743139fabda518aeb7851 Mon Sep 17 00:00:00 2001 From: windqiu Date: Mon, 14 Jun 2021 00:48:23 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E5=AE=8C=E6=88=90=E7=BF=BB=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Chapter 29 Data storage files and databases.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Chapter 29 Data storage files and databases.md b/Chapter 29 Data storage files and databases.md index a04f255..5f7cc2f 100644 --- a/Chapter 29 Data storage files and databases.md +++ b/Chapter 29 Data storage files and databases.md @@ -117,7 +117,7 @@ open test.csv: file exists 哪种模式标记才是期望的呢? -### **6 文件模式 [sec: file-mode]** +### **6 文件模式** 在 UNIX 系统中,每一个文件都有权限设置: -- Gitee