本章前面在展示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函数执行了以下工作:

  1. 通过数据库连接池与数据库进行连接;
  2. 向数据库发送一个SQL查询,这个查询将返回一个或多个行作为结果;
  3. 遍历行,为每个行分别创建一个Thread结构,首先使用这个结构去存储行中记录的帖子数据,然后将存储了帖子数据的Thread结构追加到传入的threads切片里面;
  4. 重复执行步骤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 }}动作。

通过为UserSessionThreadPost这4种数据结构创建相应的函数和方法,ChitChat最终在处理器函数和数据库之间构建起了一个数据层,以此来避免处理器函数直接对数据库进行访问,图2-8展示了这个数据层和数据库以及处理器函数之间的关系。虽然有很多库都可以达到同样的效果,但亲自构建数据层能够帮助我们学习如何对数据库进行基本的访问,并藉此了解到实现这种访问并不困难,只需要用到一些简单直接的代码,这一点是非常有益的。

图2-8 通过结构模型连接数据库和处理器

results matching ""

    No results matching ""