2

猛然发现前面的都是废话居多,这个地方开始说到连接数据库的问题了。
我也是一遍按着教程学习一边进行翻译,有时候难免会卡壳啊……

这个地方的数据库连接还是有些问题的,因为现在版本比较新了,而书中的JDBC还是0.2.3的,API都换了一套新的了,在这里卡了半天不知个所以然来,具体的解决可以看文章里面的详细描述。

至于在Instarepl中查看数据库,这步我一直没有成功,不知道是不是Light Table的问题,如果有靠谱的light table的教程能不能也共享给小弟一下~感激不尽~(反正我是觉得它在mac下的bug还是很多的……经常崩溃次次复现啊……)

翻译的不好还请见谅啦~下面就开始正题~!


添加一些功能

让我们来看看如何为我们的「留言簿」创建UI。不要为不能马上理解代码而担心,它将贯穿接下来的章节。请不要太过专注于函数的实现细节,而是关心我们如何构建我们的应用,并且我们在不同的地方都使用了何种不同的逻辑。

在之前我们已经使用Hiccup写过一些HTML的标签。现在我们将使用Hiccup库的辅助功能来更好的实现这部分。

为了使用这部分的功能,我们将命名空间改成如下的样子:

(ns guestbook.routes.home
(:require [compojure.core :refer :all]
          [guestbook.views.layout :as layout]
          [hiccup.form :refer :all]))

现在,我们就要来创建一个函数来渲染生成已经有的消息的HTML。这个函数提供一段HTML,其中包含了所有存在的评论的列表。就目前,我们先简单的硬编码了几条测试的评论信息在里面。

(defn show-guests [] 
    [:ul.guests
        (for [{:keys [message name timestamp]}
                [{:message "Howdy" :name "Bob" :timestamp nil}
                {:message "Hello" :name "Bob" :timestamp nil}]]
            [:li
                [:blockquote message]
                [:p "-" [:cite name]]
                [:time timestamp]])])

接下来,我们更新一下home函数,让它能够显示出之前已经有的留言信息,来帮助接下来的访客能更加好的进行留言。

(defn home [& [name message error]]
    (layout/common
    [:h1 "Guestbook"]
    [:p "Welcome to my guestbook"]
    [:p error]
    ;here we call our show-guests function
    ;to generate the list of existing comments
    (show-guests)
    [:hr]
    ;here we create a form with text fields called "name" and "message" 
    ;these will be sent when the form posts to the server as keywords of 
    ;the same name
    (form-to [:post "/"]
        [:p "Name:"]
        (text-field "name" name)
        [:p "Message:"]
        (text-area {:rows 10 :cols 40} "message" message) 
        [:br]
        (submit-button "comment"))))

当我们再去看浏览器上的页面的时候,可以发现测试用的消息已经和表单一起显示在上面了。值得注意的是,现在的home函数已经支持了几个可选的参数,我们可以将参数中的值渲染到页面时去,当参数是nil的时候会渲染一个空的字符串到页面之中。

表单的创建通过向路由"/"进行POST来创建,所以我们在路由中添加一条来处理这个行为,这个路由将会触发一个等会就会来完成的辅助函数——save-message

(defroutes home-routes
    (GET "/" [] (home))
    (POST "/" [name message]
        (save-message name message)))

在这save-message函数将要检查姓名和信息是否有值,然后调用home方法。
当两个参数所需要的信息都被提供,那么在终端上就会输出出来,否则就是产生一个错误信息。

(defn save-message [name message]
  (cond
    (empty? name) (home name message "some dummy forgot to leave a name.")
    (empty? message) (home name message "don't you have something to say?")
    :else (do
           (println name message)
           (home))))

尝试添加新的消息就可以看到在控制台上会将输入的信息输出出来,然后你可以试着不去填写姓名或者是反馈信息,就可以看到错误信息也能被渲染出来了。

好了,现在已经添加了查看、添加消息的用户界面,但是目前我们还没有一个真实的存储这些信息的地方。

添加数据模型

因为需要存储访客POST过来的数据,所以我们在这里需要引入JDBCSQLite,将他们的包依赖加入到project.clj。现在 :dependencies 应该是现在下面所展示的样子:

:dependencies [[org.clojure/clojure "1.5.1"]
               [compojure "1.1.5"]
               [hiccup "1.0.4"]
               [ring-server "0.3.0"]
               ;;JDBC dependencies 
               [org.clojure/java.jdbc "0.2.3"]
               [org.xerial/sqlite-jdbc "3.7.2"]]

因为添加了新的依赖包,所以我们需要在REPL中重新联接一下我们的项目。首先到connect tab(联接区)去取消连接,然后再按前面教的重新做一遍就好了。

现在,我们就做好了所有的准备工作,可以为应用加上数据模型了。我们在文件夹src/guestbook/models下新建一个命名空间 guestbook.models.db。右击文件夹就可以创建这个文件,命名为db.clj

(ns guestbook.models.db
  (:require [clojure.java.jdbc :as sql])
  (:import java.sql.DriverManager))

这里值得注意的是:我们使用:require关键词来引用其它的Clojure的命名空间,而使用关键词:import来引用JAVA的类。

接下来,我们来创建数据库的连接的描述,这个描述就是一个map,包含了JDBC引擎的类名,协议的名称以及被使用的数据库文件的名称。

(def db {:classname  "org.sqlite.JDBC",
         :subprotocol   "sqlite",
         :subname "db.sq3"})

现在我们已经有了关于数据库信息的一个断言,接下来就来写一个函数来新建一个用来储存消息的表:

(defn create-guestbook-table []
  (sql/with-connection
    db
    (sql/create-table
      :guestbook
      [:id "INTEGER PRIMARY KEY AUTOINCREMENT"]
      [:timestamp "TIMESTAMP DEFAULT CURRENT_TIMESTAMP"]
      [:name "TEXT"]
      [:message "TEXT"])
    (sql/do-commands "CREATE INDEX timestamp_index ON guestbook (timestamp)")))

在这个函数之中,使用了一个with-connection的声名,它可以保证在调用完SQL之后可以正确的释放和清除数据库连接。在它里面,我们调用了create-function,然后传入了一个key——也相当于表的名字,后面再跟着传入一系列的vectors——这些就是这张表的列。在最下面,我们使用时间戳来创建这张表的索引。

现在到Instarepl中去要引用guestbook.models.db,然后运行(create-guestbook-table)方法。

(use 'guestbook.models.db)
(create-guestbook-table)

[

!!!! 需要注意一下引用的 java jdbc 的变化,我在看书的时候使用了0.3.3的版本,会报错说没有with-connnection这个函数,经查询得知现在这个函数在clojure.java.jdbc.deprecated 中了 详细信息…

这个地方坑了我好一会。

]

现在你应该已经使用repl创建了一张表出来了,创建好表之后就可以把repl的live模式关闭了,不然重复创建表是会报错的。
现在表也创建好了,就可以写一个函数来从数据库中读取消息了,添加到db.clj中:

(defn read-guests []
  (sql/with-connection
    db
    (sql/with-query-results res
      ["SELECT * FROM guestbook ORDER BY timestamp DESC"]
      (doall res))))

在这里我们使用read-guests来运行查询语句并且获得返回的结果。在返回结果之前我们调用了函数doall,是因为res是一个懒惰的对象,它的值没有存储在内存中。

使用doall可以强制的把res中的值都取出来,如果我们不这么做的话,我们就没法在数据库连接关闭之前把结果取出来了。

我们也需要新增一个函数,用来保存新的消息到留言簿的表中去。将这个函数命名为insert-value并且传入姓名和消息,进行数据存储。

(defn save-message [name message]
  (sql/with-connection
    db
    (sql/insert-values
      :guestbook
      [:name :message :timestamp]
      [name message (new java.util.Date)])))

到现在,我们已经完成了数据库的读、写操作的函数。我们可以先到REPL中去测试一下,首先需要重新执行(use 'guestbook.models.db)使Instarepl可以使用新增加的函数。但是这里有一个问题:guestbook.models.dbgusetbook.routes.home两个命名空间里面都有函数save-message。如果我们想要重新载入guestbook.models.db,那么就会提示你说save-message已经被定义了。为了避免这个问题,我们可以在重新载入guestbook.models.db之前使用ns-unmap命令来移出已经定义的save-mesage方法。

(ns-unmap 'user 'save-message)
(use 'guestbook.models.db)

现在,我们执行如下语句,就可以看到存入数据库的数据是什么了。

(save-message "Bob" "hello")
(read-guests)

数据层做的差不多了,接下来又可以回到home把假数据给替换掉了。


__SSSamuel
109 声望0 粉丝

知乎ID:Samuel