本章前面在展示ChitChat应用的设计方案时,曾经提到过ChitChat应用包含了4种数据结构。虽然把这4种数据结构放到主源码文件里面也是可以的,但更好的办法是把所有与数据相关的代码都放到另一个包里面——ChitChat应用的data
包也因此应运而生。
为了创建data
包,我们首先需要创建一个名为data
的子目录,并创建一个用于保存所有帖子相关代码的thread.go
文件(在之后的小节里面,我们还会创建一个用于保存所有用户相关代码的user.go
文件)。在此之后,每当程序需要用到data
包的时候(比如处理器需要访问数据库的时候),程序都需要通过import
语句导入这个包:
import (
"github.com/sausheong/gwp/Chapter_2_Go_ChitChat/chitchat/data"
)
代码清单2-12展示了定义在thread.go
文件里面的Thread
结构,这个结构存储了与帖子有关的各种信息。
package data//加粗
import(
"time"
)
type Thread struct {
Id int
Uuid string
Topic string
UserId int
CreatedAt time.Time
}
正如代码清单2-12中加粗显示的代码行所示,文件的包名现在是data
而不再是main
了,这个包就是前面小节中我们曾经见到过的data
包。data
包除了包含与数据库交互的结构和代码,还包含了一些与数据处理密切相关的函数。隶属于其他包的程序在引用data
包中定义的函数、结构或者其他东西时,必须在被引用元素的名字前面显式地加上data
这个包名。比如说,引用Thread
结构就需要使用data.Thread
这个名字,而不能仅仅使用Thread
这个名字。
Thread
结构应该与创建关系数据库表threads
时使用的数据定义语言(Data Definition Language,DDL)保持一致。因为threads
表目前尚未存在,所以我们必须创建这个表以及容纳该表的数据库。创建chitchat数据库的工作可以通过执行以下命令来完成:
createdb chitchat
在创建数据库之后,我们就可以通过代码清单2-13展示的setup.sql
文件为ChitChat论坛创建相应的数据库表了。
代码清单2-13 用于在PostgreSQL
里面创建数据库表的setup.sql
文件
create table users (
id serial primary key,
uuid varchar(64) not null unique,
name varchar(255),
email varchar(255) not null unique,
password varchar(255) not null,
created_at timestamp not null
);
create table sessions (
id serial primary key,
uuid varchar(64) not null unique,
email varchar(255),
user_id integer references users(id),
created_at timestamp not null
);
create table threads (
id serial primary key,
uuid varchar(64) not null unique,
topic text,
user_id integer references users(id),
created_at timestamp not null
);
create table posts (
id serial primary key,
uuid varchar(64) not null unique,
body text,
user_id integer references users(id),
thread_id integer references threads(id),
created_at timestamp not null
);
运行这个脚本需要用到psql
工具,正如上一节所说,这个工具通常会随着PostgreSQL
一同安装,所以你只需要在终端里面执行以下命令就可以了:
psql –f setup.sql –d chitchat
如果一切正常,那么以上命令将在chitchat数据库中创建出相应的表。在拥有了表之后,程序就必须考虑如何与数据库进行连接以及如何对表进行操作了。为此,程序创建了一个名为Db
的全局变量,这个全局变量是一个指针,指向的是代表数据库连接池的sql.DB
,而后续的代码则会使用这个Db
变量来执行数据库查询操作。代码清单2-14展示了Db
变量在data.go
文件中的定义,此外还展示了一个用于在Web应用启动时对Db
变量进行初始化的init
函数。
代码清单2-14 data.go
文件中的Db
全局变量以及init
函数
Var Db *sql.DB
func init() {
var err error
Db, err = sql.Open("postgres", "dbname=chitchat sslmode=disable")
if err != nil {
log.Fatal(err)
}
return
}
现在程序已经拥有了结构、表以及一个指向数据库连接池的指针,接下来要考虑的是如何连接(connect)Thread
结构和threads
表。幸运的是,要做到这一点并不困难:跟ChitChat应用的其他部分一样,我们只需要创建能够在结构和数据库之间互动的函数就可以了。例如,为了从数据库里面取出所有帖子并将其返回给index
处理器函数,我们可以使用thread.go
文件中定义的Threads
函数,代码清单2-15给出了这个函数的定义。
代码清单2-15 threads.go
文件中定义的Threads
函数
func Threads() (threads []Thread, err error){
rows, err := Db.Query("SELECT id, uuid, topic, user_id, created_at FROM
threads ORDER BY created_at DESC")
if err != nil {
return
}
for rows.Next() {
th := Thread{}
if err = rows.Scan(&th.Id, &th.Uuid, &th.Topic, &th.UserId,&th.CreatedAt);
err != nil {
return
}
threads = append(threads, th)
}
rows.Close()
return
}
简单来讲,Threads
函数执行了以下工作:
- 通过数据库连接池与数据库进行连接;
- 向数据库发送一个SQL查询,这个查询将返回一个或多个行作为结果;
- 遍历行,为每个行分别创建一个
Thread
结构,首先使用这个结构去存储行中记录的帖子数据,然后将存储了帖子数据的Thread
结构追加到传入的threads
切片里面; - 重复执行步骤3,直到查询返回的所有行都被遍历完毕为止。本书的第6章将对数据库操作的细节做进一步的介绍。
本书的第6章将对数据库操作的细节做进一步的介绍。
在了解了如何将数据库表存储的帖子数据提取到Thread
结构里面之后,我们接下来要考虑的就是如何在模板里面展示Thread
结构存储的数据了。在代码清单2-9中展示的index.html
模板文件,有这样一段代码:
{{ range . }}
<div class="panel panel-default">
<div class="panel-heading">
<span class="lead"> <i class="fa fa-comment-o"></i> {{ .Topic }}</span>
</div>
<div class="panel-body">
Started by {{ .User.Name }} - {{ .CreatedAtDate }} - {{ .NumReplies }} posts.
<div class="pull-right">
<a href="/thread/read?id={{.Uuid }}">Read more</a>
</div>
</div>
</div>
{{ end }}
正如之前所说,模板动作中的点号(.)代表传入模板的数据,它们会和模板一起生成最终的结果,而{{ range . }}
中的.号代表的是程序在稍早之前通过Threads
函数取得的threads
变量,也就是一个由Thread
结构组成的切片。
range
动作假设传入的数据要么是一个由结构组成的切片,要么是一个由结构组成的数组,这个动作会遍历传入的每个结构,而用户则可以通过字段名访问结构里面的字段,比如,动作{{ .Topic }}
访问的是Thread
结构的Topic
字段。
注意,在访问字段时必须在字段名的前面加上点号,并且字段名的首字母必须大写。
用户除可以在字段名的前面加上点号来访问结构中的字段以外,还可以通过相同的方法调用一种名为方法(method)的特殊函数。比如,在上面展示的代码中,{{ .User.Name }}
、{{ .CreatedAtDate }}
和{{ .NumReplies }}
这些动作的作用就是调用结构中的同名方法,而不是访问结构中的字段。
方法是隶属于特定类型的函数,指针、接口以及包括结构在内的所有具名类型都可以拥有自己的方法。比如说,通过将函数与指向Thread
结构的指针进行绑定,可以创建出一个针对Thread
结构的方法,而传入方法里面的Thread
结构则称为接收者(receiver):方法可以访问接收者,也可以修改接收者。
作为例子,代码清单2-16展示了NumReplies
方法的实现代码。
代码清单2-16 thread.go
文件中的NumReplies
方法
func (thread *Thread) NumReplies() (count int) {
rows, err := Db.Query("SELECT count(*) FROM
posts where thread_id = $1", thread.Id)
if err != nil {
return
}
for rows.Next() {
if err = rows.Scan(&count);
err != nil {
return
}
rows.Close()
return
}
}
NumReplies
方法首先打开一个指向数据库的连接,接着通过执行一条SQL查询来取得帖子的数量,并使用传入方法里面的count
参数来记录这个值。最后,NumReplies
方法返回帖子的数量作为方法的执行结果,而模板引擎则使用这个值去代替模板文件中出现的{{ .NumReplies }}
动作。
通过为User
、Session
、Thread
和Post
这4种数据结构创建相应的函数和方法,ChitChat最终在处理器函数和数据库之间构建起了一个数据层,以此来避免处理器函数直接对数据库进行访问,图2-8展示了这个数据层和数据库以及处理器函数之间的关系。虽然有很多库都可以达到同样的效果,但亲自构建数据层能够帮助我们学习如何对数据库进行基本的访问,并藉此了解到实现这种访问并不困难,只需要用到一些简单直接的代码,这一点是非常有益的。
图2-8 通过结构模型连接数据库和处理器