猛然发现前面的都是废话居多,这个地方开始说到连接数据库的问题了。
我也是一遍按着教程学习一边进行翻译,有时候难免会卡壳啊……
这个地方的数据库连接还是有些问题的,因为现在版本比较新了,而书中的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过来的数据,所以我们在这里需要引入JDBC
和SQLite
,将他们的包依赖加入到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.db
和gusetbook.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
把假数据给替换掉了。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。