Compojure

作者:Reuven Lerner

在我的上一篇文章中,我开始讨论 Compojure,这是一个用 Clojure 语言编写的 Web 框架。Clojure 已经在软件开发人员中引起了极大的兴奋,因为它将 Lisp 的美观和富有表现力的优雅与 Java 虚拟机 (JVM) 的效率和普遍性结合在一起。Clojure 还有其他特性,包括它著名的软件事务内存 (STM) 的使用,以避免多线程环境中的问题。

作为一名 Web 开发人员和长期的 Lisp 爱好者,我一直对编写和部署用 Clojure 编写的 Web 应用程序的可能性很感兴趣。Compojure 似乎是一个用于创建 Web 应用程序的简单框架,它建立在较低级别的系统之上,例如处理 HTTP 请求的“ring”。

在我的上一篇文章中,我解释了如何使用“lein”系统创建一个简单的 Web 应用程序,修改 project.clj 配置文件,并确定响应特定 URL 模式(“路由”)返回的 HTML。在这里,我尝试稍微推进应用程序,研究 Web 开发人员通常感兴趣的东西。即使您最终没有使用 Clojure 或 Compojure,我仍然认为您将从理解这些系统如何处理问题中学到一些东西。

数据库和 Clojure

由于 Clojure 构建在 JVM 之上,因此您可以在 Clojure 程序中使用与 Java 程序中相同的对象。换句话说,如果您想连接到 PostgreSQL 数据库,您可以使用与 Java 应用程序相同的 JDBC 驱动程序。

安装 PostgreSQL JDBC 驱动程序需要两个步骤。首先,您必须下载驱动程序,该驱动程序可在 https://jdbc.postgresql.ac.cn 获得。其次,您必须告诉 JVM 在哪里可以找到驱动程序定义的类。这可以通过设置(或添加到)CLASSPATH 环境变量来完成——也就是说,将驱动程序放在


export CLASSPATH=/home/reuven/Downloads:$CLASSPATH

完成此操作后,您可以告诉您的 Clojure 项目您想要包含 PostgreSQL JDBC 驱动程序,方法是在 defproject 宏中的 :dependencies 向量中添加两个元素


(defproject cjtest "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :dependencies [[org.clojure/clojure "1.5.1"]
                 [compojure "1.1.5"]
                 [hiccup "1.0.3"]
                 [org.clojure/java.jdbc "0.2.3"]
                 [postgresql "9.1-901.jdbc4"]]
  :plugins [[lein-ring "0.8.5"]]
  :ring {:handler cjtest.handler/app}
  :profiles
  {:dev {:dependencies [[ring-mock "0.1.5"]]}})

现在您只需要连接到数据库并与之交互。假设您已经在本地 PostgreSQL 服务器上创建了一个名为“cjtest”的数据库,您可以使用内置的 Clojure REPL (lein repl) 与数据库进行通信。首先,您需要加载数据库驱动程序并将其放入一个“sql”命名空间,以便您可以使用该驱动程序


(require '[clojure.java.jdbc :as sql])

然后,您需要告诉 Clojure 您要连接的主机、数据库和端口。最简单的方法是创建一个“db”映射来构建 PostgreSQL 需要的查询字符串


(def db {:classname "org.postgresql.Driver" 
	 :subprotocol "postgresql"
	 :subname (str "//" "localhost" ":" 5432 "/" "cjtest")
	 :user "reuven"
	 :password ""})

完成这些设置后,您现在可以发出数据库命令。最简单的方法是使用“sql”命名空间内的 with-connection 宏,它使用驱动程序进行连接,然后让您发出命令。例如,如果您想创建一个新表,其中包含一个 serial(即自动更新的主键)列和一个文本列,您可以执行以下操作


(sql/with-connection db 
    (sql/create-table :foo [:id :serial] [:stuff :text]))

如果您随后在 psql 中检查,您将看到该表确实已创建,使用了您指定的类型。如果要插入数据,可以使用 sql/insert-values 函数


(sql/with-connection db (sql/insert-values 
 ↪:foo [:stuff] ["first post"]))

接下来,您将得到以下映射,不仅表明数据已插入,而且还表明 PostgreSQL 的序列对象自动为其分配了一个 ID


{:stuff "first post", :id 1}

如果您想检索所有已插入的数据怎么办?您可以使用 sql/with-query-results 函数,使用标准 doseq 函数迭代结果


(sql/with-connection db
    (sql/with-query-results resultset ["select * from foo"]
        (doseq [row resultset] (println row))))

或者,如果您只想获取“stuff”列的内容,可以使用


(sql/with-connection db
    (sql/with-query-results resultset ["select * from foo"]
        (doseq [row resultset] (println (:stuff row)))))
数据库和 Compojure

现在您已经了解了如何从 Clojure REPL 执行基本数据库操作,您可以将其中一些代码放入您的 Compojure 应用程序中。例如,假设您想要一个预约日历。现在,让我们假设已经定义了一个 PostgreSQL “appointments”数据库


CREATE TABLE Appointments (
       id SERIAL,
       meeting_at TIMESTAMP,
       meeting_with TEXT,
       notes TEXT
);

INSERT INTO Appointments (meeting_at, meeting_with, notes) 
      VALUES ('2013-july-1 12:00', 'Mom', 'Always good to see Mom');

您现在希望能够在您的 Web 应用程序中转到 /appointments 并查看当前的预约列表。为此,您需要在您的 Web 应用程序中添加一个路由,使其调用一个函数,然后该函数转到数据库并检索所有这些元素。

在您可以这样做之前,您需要将 PostgreSQL JDBC 驱动程序加载到您的 Clojure 应用程序中。您可以在 handler.clj 中的命名空间声明的 :require 部分中最容易地完成此操作


(ns cjtest.handler
  (:use compojure.core)
  (:require [compojure.handler :as handler]
            [compojure.route :as route]
            [clojure.java.jdbc :as sql]))

(我在 REPL 中使用“require”函数手动完成了此操作,语法略有不同。)

然后,您在 handler.clj 中包含相同的“db”定义,以便您的数据库连接字符串仍然可用。

然后,您在 defroutes 宏中添加新行,添加新的 /appointments URL,这将调用 list-appointments 函数


(defroutes app-routes
  (GET "/" [] "Hello World")
  (GET "/appointments" [] list-appointments)
  (GET "/fancy/:name" [name] say-hello)
  (route/resources "/")
  (route/not-found "Not Found"))

最后,您定义 list-appointments,这是一个执行 SQL 查询的函数,然后抓取结果记录并将其转换为 HTML 中的项目符号列表


(defn list-appointments
  [req]
  (html
   [:h1 "Current appointments"]
    [:ul
     (sql/with-connection db
         (sql/with-query-results rs ["select * from appointments"]
           (doall
            (map format-appointment rs))))]))

请记住,在像 Clojure 这样的函数式语言中,其思想是从数据库获取结果,然后以某种方式处理它们,将其交给另一个函数进行显示(或进一步处理)。上面的函数使用 Hiccup HTML 生成系统生成 HTML 输出。使用 Hiccup,您可以轻松创建(如上面的函数中所示)H1 标题,后跟一个“ul”列表。

真正的魔力发生在调用 sql/with-query-results 时。该函数将数据库调用的结果放入 rs 变量中。然后,您可以对该结果集执行许多不同的操作。在本例中,让我们将每个记录转换为最终 HTML 中的“li”标记。最简单的方法是将一个函数应用于结果集的每个元素。在 Clojure 中(与许多函数式语言一样),您可以使用 map 函数来执行此操作,该函数将项目集合转换为长度相等的新集合。

format-appointment 函数做什么?您可以想象,它将预约记录转换为 HTML


(defn format-appointment [one-appointment]
 (html [:li (:meeting_at one-appointment)
	 " : "
	 (:meeting_with one-appointment)
	 " (" (:notes one-appointment) ")" ]))

换句话说,您将把记录视为哈希,然后使用 Clojure 的简写语法从中检索元素(键)。您将其包装到 HTML 中,然后可以将其显示给用户。将您的显示功能分解为两个函数的优点是,您现在可以更改预约的显示方式,而无需修改在用户请求 /appointments 时调用的主函数。

清单 1. handler.clj:简单预约簿系统的源代码

(ns cjtest.handler
  (:use compojure.core hiccup.core clj-time.format clj-time.coerce)
  (:require [compojure.handler :as handler]
            [compojure.route :as route]
            [clojure.java.jdbc :as sql]))

(defn say-hello
  [req]
  (html [:p [:b "Hello, "  (get (get req :route-params) :name) ]]))


(def db {:classname "org.postgresql.Driver"
         :subprotocol "postgresql"
         :subname (str "//" "localhost" ":" 5432 "/" "cjtest")
         :user "reuven"
         :password ""})

(defn format-meeting [one-meeting]
  (html [:li (:meeting_at one-meeting)
         " : "
         (:meeting_with one-meeting)
         " (" (:notes one-meeting) ")" ]))

(defn new-meeting-form
  [ req ]
  (html [:form {:method "POST" :action "/create-meeting"}
         [:p "Meeting at (in 2013-06-28T11:08 format): " [:input 
         ↪{:type "text" :name "meeting_at"}]]
         [:p "Meeting with: " [:input {:type "text" 
         ↪:name "meeting_with"}]]
         [:p "Notes: " [:input {:type "text" :name "notes"}]]
         [:p [:input {:type "submit" :value "Add meeting"}]]]))

(defn list-meetings
  [req]
  (html
   [:h1 "Current meetings"]
   [:ul
    (sql/with-connection db
      (sql/with-query-results rs ["select * from appointments"]
        (doall
         (map format-meeting rs))))]))


(defn create-meeting
  [req]
  (sql/with-connection db
    (let [form-params (:form-params req)
          meeting-at-string (get form-params "meeting_at")
          meeting-at-parsed (clj-time.format/parse
(clj-time.format/formatters
                  :date-hour-minute)
                  meeting-at-string)
          meeting-at-timestamp (clj-time.coerce/to-timestamp
          ↪meeting-at-parsed)
          meeting-with (get form-params "meeting_with")
          notes (get form-params "notes")]
      (sql/insert-values :appointments
                         [:meeting_at :meeting_with :notes]
                         [meeting-at-timestamp meeting-with notes]))
    "Added!"))

(defroutes app-routes
  (GET "/" [] "Hello World")
  (GET "/meetings" [] list-meetings)
  (GET "/new-meeting" [] new-meeting-form)
  (POST "/create-meeting" [] create-meeting)
  (GET "/fancy/:name" [name] say-hello)
  (route/resources "/")
  (route/not-found "Not Found"))

(def app
  (handler/site app-routes))
插入数据

假设您还想将数据插入到您的预约簿中。为此,您需要一个 HTML 表单,然后将其提交到您网站上的 URL。让我们首先创建一个简单的表单——像往常一样,编写为一个函数


(defn new-meeting-form
  [ req ]
  (html [:form {:method "POST" :action "/create-meeting"}
         [:p "Meeting at (in 2013-06-28T11:08 format): " 
         ↪[:input {:type "text" :name "meeting_at"}]]
         [:p "Meeting with: " [:input {:type "text" 
          ↪:name "meeting_with"}]]
         [:p "Notes: " [:input {:type "text" :name "notes"}]]
         [:p [:input {:type "submit" :value "Add meeting"}]]]))

请注意 Hiccup 库如何再次让您轻松定义 HTML 标记。在本例中,由于它是一个表单,因此您需要告诉表单应该提交到哪个 URL。因此,在本例中,这将是 /create-meeting URL。因此,您需要在您的 defroutes 宏调用中定义 /new-meeting 和 /create-meeting


(defroutes app-routes
  (GET "/" [] "Hello World")
  (GET "/meetings" [] list-meetings)
  (GET "/new-meeting" [] new-meeting-form)
  (POST "/create-meeting" [] create-meeting)
  (GET "/fancy/:name" [name] say-hello)
  (route/resources "/")
  (route/not-found "Not Found"))

如您所见,路由区分 GET 和 POST 请求。因此,对 /create-meeting 的 GET 请求不会有任何效果(也就是说,它将导致显示“未找到”消息);需要 POST 请求才能使其工作。

当您想向数据库添加新会议时,所有内容都会结合在一起。您从提交的表单中获取参数,然后将它们插入到数据库中。

我仍在学习 Clojure 和 Compojure,并继续发现新的函数库,这些库可以更轻松地创建 HTML 表单和使用数据库。例如,我最近发现了 SQLKorma,一个看起来几乎像 Ruby 的 ActiveRecord 的库,因为它提供了一个 DSL 来创建数据库查询。

Clojure 的强大功能,与所有 Lisp 一样,部分基于您以小步骤完成所有操作,然后将这些步骤组合起来以获得全部功能的理念。例如,这是我编写的用于向数据库添加新记录(会议)的函数


(defn create-meeting
  [req]
  (sql/with-connection db
    (let [form-params (:form-params req)
          meeting-at-string (get form-params "meeting_at")
          meeting-at-parsed (clj-time.format/parse 
          ↪(clj-time.format/formatters
                   :date-hour-minute)
                   meeting-at-string)
          meeting-at-timestamp (clj-time.coerce/to-timestamp 
          ↪meeting-at-parsed)
          meeting-with (get form-params "meeting_with")
          notes (get form-params "notes")]
   (sql/insert-values :appointments
                      [:meeting_at :meeting_with :notes]
                      [meeting-at-timestamp meeting-with notes]))
    "Added!"))

该函数的第一个和最后一部分在许多方面与您在 Compojure 之外执行的数据库行插入类似。您使用 sql/with-connection 连接到数据库,并在其中使用 sql/insert-values 将行插入到特定表中。

我相信,此函数有趣的部分是中间发生的事情。使用“let”形式(它执行名称到值的本地绑定),我可以从提交的 HTML 表单元素中抓取值,为它们输入数据库做准备。

我进一步利用了 Clojure 的“let”允许您根据先前绑定的名称绑定名称这一事实。因此,我可以将 meeting-at-string 设置为 HTML 表单值,然后将 meeting-at-parsed 设置为将字符串转换为解析的 Clojure 值后获得的值,然后将 meeting-at-timestamp 设置为将其转换为 Clojure 和 PostgreSQL 都可以轻松处理的数据类型。

这里的大部分繁重工作是由 clj-time 包完成的,该包处理各种不同的日期/时间包。

最后,您可以转到 /new-meeting,在 HTML 表单中输入适当的数据,并将该数据保存到数据库。然后您可以转到 /meetings 并查看您设置的所有会议的完整列表。

结论

我一直很喜欢 Lisp,并且经常希望我能找到一种方法在我的日常工作中实际使用它。(并不是说我不喜欢 Ruby 和 Python,但我在大学里接受的洗脑非常有效。)使用 Clojure 作为一种语言,以及使用 Compojure 开发 Web 应用程序,是一种令人耳目一新的体验——我打算继续尝试,也鼓励您尝试。

资源

Clojure 语言的主页位于 https://clojure.net.cn,其中包含大量文档。Compojure 的文档位于其主页 http://compojure.org,Hiccup 的文档位于 https://github.com/weavejester/hiccup

我在这里引用的 SQLKorma 库位于 http://www.sqlkorma.com

日期和时间例程可在 GitHub 上的 https://github.com/KirinDave/clj-time 中找到,它们为任何在 Clojure 中处理日期和时间的人提供了大量有用的功能。

我在 Wikibooks 上找到了许多关于从 Clojure 中使用 SQL 和 JDBC 的优秀示例:https://wikibooks.cn/wiki/Clojure_Programming/Examples/JDBC_Examples

关于 Clojure 的两本好书是 Stuart Halloway 和 Aaron Bedra 编写的 Programming Clojure(由 Pragmatic Programmers 出版)以及 Chas Emerick、Brian Carper 和 Christophe Grand 编写的 Clojure Programming(由 O'Reilly 出版)。在过去一两年里,我都读过这两本书,并且出于不同的原因喜欢它们,没有明显的偏好。

Reuven M. Lerner 是一位资深的 Web 开发人员,提供 Python、Git、PostgreSQL 和数据科学方面的培训和咨询服务。他撰写了两本编程电子书(Practice Makes Python 和 Practice Makes Regexp),并在 http://lerner.co.il/newsletter 上为程序员发布免费的每周新闻通讯。Reuven 的 Twitter 账号是 @reuvenmlerner,与妻子和三个孩子住在以色列的莫迪因。

加载 Disqus 评论