本專欄系列已探究 Google 和 Amazon 平台雲端運算。雖實現和結構不同,兩平台皆支援快速擴展部署。能快速經濟地組裝、測試、運行和維護 Java 應用程式,前所未有。但雲端非影響現今 Java 開發速度唯一因素。開源解決方案也能幫助迅速組裝軟體應用,因不再需編寫大量程式碼。手寫 ORM、日誌或測試框架時代已過去。這些問題隨時間逐漸解決,又在開源領域再次出現 — 再次面臨 — 但解決方案幾乎始終優於自行開發。
Java 技術首次亮相以來,Java 開發版圖巨變。得益於成熟開源框架和可靠租用式部署基礎設施,現可迅速經濟地裝配、測試、運行和維護 Java 應用程式。本系列中 Andrew Glover 探索使這種全新開發範例成為可能的各種技術和工具。
關聯式資料庫基本主導資料庫市場。但其他類似資料庫 — 包含面向物件和面向文件資料庫,兩者在面向關聯世界中極為不同 — 也不時發揮重要作用。CouchDB 是一種面向文件資料庫。無結構描述,允許以 JavaScript Object Notation (JSON) 字串形式儲存文件。
JSON 是輕量級資料交換格式,也是 Web 應用程式替代格式。類似 XML 但遠無那麼詳細。得益於輕量特性,正成為 Web 通用語言。
設想違規停車罰單。涵蓋:違規日期、時間、位置、車輛描述、車牌資訊、違規情況。罰單格式和數據因管轄權而異。即使單一管轄權標準停車罰單,內容也可能不同。警官開罰單可不填時間,或省略車型只填車牌詳情。地點可為兩街道組合 (如 Fourth 和 Lexington 交叉口) 或固定地址 (如 19993 Main Street)。但採集資訊語義大抵相似。
罰單數據點可在關聯式資料庫建模,但詳情麻煩。如如何有效在關聯式資料庫捕獲某交叉口?無街道交叉時資料庫是否用空欄位表示第二地址 (假設建模方式各欄捕獲不同街道名)?
這些情況下關聯式資料庫抽象化程度可能稍高。所需資訊已以文件 (罰單) 形式存在。為何不建模為文件?無需死守嚴格關聯結構描述,只需大致遵循高階結構描述語義。這正是 CouchDB 用武之地。允許靈活建模這些領域類型 — 成果是完備文件,無結構描述,而是使用與其他文件大致相似藍圖。
Google 獨創 MapReduce 是處理海量數據集概念框架。高度優化應用大量計算機分布式問題解決機制。包含 map 和 reduce 兩函數。map 接受大量輸入,分割為較小部分 (同時傳遞給其他進程)。reduce 將來自 map 所有單獨輸出整合為最終輸出。
CouchDB 可搜尋文件、文件屬性甚至關聯文件。實現方式用視圖而非 SQL。視圖本質是您採用 MapReduce 風格 (JavaScript) 編寫的函數;即只需編寫 map 和 reduce 函數。這些函數共同過濾或提取文件數據、或有效利用它們關係。事實上 CouchDB 夠靈活,只要底層文件未變化,只需運行這些函數一次,加快視圖處理。
CouchDB 最有趣處在設計方式。體現 Web 基本 (極成功) 概念。公開一組全面 REST 風格 API,允許建立、查詢、更新、刪除文件、視圖和資料庫。這讓 CouchDB 使用極簡單。不需其他驅動程式或平台開始開發:瀏覽器即能完成所有工作。豐富函式庫讓 CouchDB 使用極簡單 — 但內部看,它們僅透過 HTTP 利用 REST 概念。
類似 Web 本質特性,CouchDB 設計融入大量可擴展因子。用併發程式語言 Erlang 編寫,支援綁定分布式、容錯、不間斷應用程式。該語言 (現已開源) 由 Ericsson 開發,電信環境廣泛應用。
CouchDB 安裝因作業系統而異。Windows 需安裝 Microsoft C 編譯器 Cygwin 等依賴。Mac 需 Macports。Linux (如 Ubuntu) 安裝不那麼簡單。但非所有人都裝 Ubuntu 實例?
當然可方便獲取 Ubuntu 實例!Amazon EC2 是相對經濟、隨需應變使用 Ubuntu 方法。施展少許 EC2 魔法,迅速搭建 CouchDB 環境;完成可關閉它。
先找基本實例 EC2 AMI。選用 ami-ccf615a5 (Ubuntu 9.04 實例),撰寫時最新版。(閱讀時可能已有 9.10 AMI)。用 Eclipse 或 AWS Management Console 啟動 ami-ccf615a5 實例。確保設定允許 SSH 存取安全策略。(雖 CouchDB 用 HTTP,為簡單透過 SSH 通道通訊)。需用鍵值對。(需指導參見系列前兩文 "您也可以租用 EC2" 和 "使用方便的 EC2")。
啟動 Ubuntu 9.04 EC2 實例後,為它設定 ssh。(記住實例約 1 分鐘完全啟動,請耐心)。例:開啟終端用 ssh 設定新建實例:
aglover#> ssh -i .ec2/agkey.pem root@ec2-174-129-157-167.compute-1.amazonaws.com
AMI DNS 名稱 ec2-174-129-157-167.compute-1.amazonaws.com,鍵值對名稱 agkey。您的 DNS 和鍵值對不同。
雲端 Ubuntu 實例命令提示輸入:
apt-get update
aptitude install couchdb
命令自動安裝 CouchDB。注意不安裝最新版。需最新版須透過原始碼安裝。
命令結束後,用 ps -eaf 檢查 CouchDB 正常運行。將 ps 輸出傳給 egrep 查看路徑用 couchdb 進程。應見輸出如範例 1:
couchdb 1820 1 0 00:54 ? 00:00:00 /bin/sh -e /usr/bin/couchdb
-c /etc/couchdb/couch.ini -b -r 5 -p /var/run/couchdb.pid -o /
couchdb 1827 1820 0 00:54 ? 00:00:00 /bin/sh -e /usr/bin/couchdb
-c /etc/couchdb/couch.ini -b -r 5 -p /var/run/couchdb.pid -o /
couchdb 1828 1827 0 00:54 ? 00:00:00 /usr/lib/erlang/erts-5.6.5/bin/beam
-Bd -- -root /usr/lib/erlang -progname erl -- -home /v
couchdb 1836 1828 0 00:54 ? 00:00:00 heart -pid 1828 -ht 11
回本地機器,設 SSH 通道允許存取雲端運行 CouchDB 實例,如同自己機器。本地機器開新終端會話輸入:
ssh -i your key -L 5498:localhost:5984 root@your AMI DNS
本地機器開瀏覽器,網址列輸入 https://127.0.0.1:5498/。應見漂亮 JSON 歡迎訊息:
{"couchdb":"Welcome","version":"0.8.0-incubating"}
一切運轉正常,開始應用 CouchDB。
表徵狀態傳輸 (REST) 設計風格中,鬆散耦合 Web 應用程式依賴指定資源 — 如統一資源定位符 (URL)、統一資源標識符 (URI)、統一資源名稱 (URN) — 而非訊息。REST 明智採用 Web 經驗證成功基礎設施 — HTTP。即 REST 利用 HTTP 協定各面向,如 GET 和 POST 請求。這些請求滿足業務應用需求:建立、讀取、更新、刪除 (CRUD)。
CouchDB 透過 REST 風格 HTTP 介面公開數據,使用 CouchDB 相當簡單 (已在瀏覽器見識)。幾乎所有工作透過 HTTP 完成。
可選各種工具與 HTTP 交互。用 REST 介面時,我偏愛 Groovy HTTPBuilder RESTClient 擴充。HTTPBuilder — Apache Commons Project HTTPClient 包裝器 — 在 HTTP POST、GET、PUT、DELETE 語法添加 Groovy 風格。因 HTTPBuilder 用 Groovy 建立,編寫利用 REST 概念腳本 (如與 CouchDB 通訊) 簡單得不能再簡單。
為與 Java 開發 2.0 主題一致 — 快速、簡便、免費 (或便宜) — Groovy 方便 Grape (Groovy Advanced Packaging Engine 或 Groovy Adaptable Packaging Engine) 特性對與 HTTPBuilder 交互非常有用。Grape 是依賴管理器,允許 Groovy 腳本和類別在運行時 自動配置 自己特定依賴。簡化各種開源函式庫使用,因不需下載各種 JAR 即可開始編碼。例:用 Grape 編寫 Groovy 腳本用 HTTPBuilder,無需 HTTPBuilder JAR。Grape 在運行時或編譯時下載它們 (透過 Apache)。
透過註解和方法呼叫利用 Grape。例:用 @Grab 註解修飾方法或類別聲明。註解中指定主依賴相關中繼資料 (借助 Ivy 魔力,所有中間依賴悉數確定)。運行時或編譯時 (無論孰前孰後) Grape 下載依賴並確保在類路徑下。若已下載依賴 (如先前運行),Grape 仍確保類路徑含適當 JAR。
在 CouchDB 建立任意文件前,必須先建立資料庫。建立停車罰單資料庫,透過 HTTPBuilder 領域專用語言 (DSL) 用 RESTClient 發出 HTTP PUT,如範例 2:(本文所有 Groovy 程式碼可從 下載 區獲得。)
import static groovyx.net.http.ContentType.JSON
import groovyx.net.http.RESTClient
@Grab(group='org.codehaus.groovy.modules.http-builder', module='http-builder',
version='0.5.0-RC2')
def getRESTClient(){
return new RESTClient("https://localhost:5498/")
}
def client = getRESTClient()
def response = client.put(path: "parking_tickets",
requestContentType: JSON, contentType: JSON)
assert response.data.ok == true : "response from server wasn't ok"
CouchDB 應回應 {"ok":true}。範例 2 所示,HTTPBuilder 輕鬆解析 JSON 確保 ok 元素確為 true。
接著建立文件,保持停車罰單主題。建立停車罰單模型,記住罰單相關面向。另記住它們是警官填寫實際表單,有些欄位可不填或用預定義模式 — 考慮交叉口與準確地點。
用 HTTPBuilder 透過 HTTP PUT 在 CouchDB 建立文件 (如範例 2 建立資料庫)。CouchDB 處理 JSON 文件,必須遵循 JSON 名稱值格式。在 Groovy 建立類映射資料結構 (HTTPBuilder 轉換為有效 JSON)。如範例 3:
response = client.put(path: "parking_tickets/1234334325", contentType: JSON,
requestContentType: JSON,
body: [officer: "Kristen Ree",
location: "199 Baldwin Dr",
vehicle_plate: "Maryland 77777",
offense: "Parked in no parking zone",
date: "2009/01/31"])
assert response.data.ok == true : "response from server wasn't ok"
assert response.data.id == "1234334325" : "the returned ID didn't match"
範例 3 完成多項任務。首先對 CouchDB 文件發出 PUT 必須分配 UUID。CouchDB 可為您分配,或自行管理。範例 3 設定值 1234334325;隨後 UUID 附加到 URL。若 UUID 可用,CouchDB 為其分配執行 PUT 操作文件。put 呼叫 body 部分,注意指定各名稱相關值方式,幾乎與普通映射無異。例:指定警官名稱 Kristen Ree,罰單地點 199 Baldwin Dr。
範例 4 同樣技巧在 CouchDB 建立另一罰單:
def id = new Date().time
response = client.put(path: "parking_tickets/${id}", contentType: JSON,
requestContentType: JSON,
body: [officer: "Anthony Richards",
location: "Walmart Parking lot",
vehicle_plate: "Delaware 4433-OP",
offense: "Parked in non-parking space",
date: "2009/02/01"])
assert response.data.ok == true : "response from server wasn't ok"
assert response.data.id == "${id}" : "the returned ID didn't match"
每次透過 RESTClient 發出 PUT 斷言 JSON 回應 ok 為 TRUE,驗證 id 值存在。範例 4 未建立 UUID,用當前時間 — 非極簡單技巧,但不再滿足簡單交叉口。
CouchDB 成功建立新文件後,回傳含 UUID 和修訂 ID JSON。例:範例 4 驗證 JSON:
{"ok":true,"id":"12339892938945","rev":"12351463"}
您的 id 和 rev 值不同。注意可透過 response.data.id 捕獲 id 值。
CouchDB 透過修訂追蹤文件,可回傳先前文件版本 (透過修訂 ID),極類似 CVS 或 Subversion 方法。
建立停車罰單 (CouchDB 術語:文件) 後,在 CouchDB 建立視圖。記住視圖即實際 MapReduce 函數;必須定義它們。多數情況不需 reduce 函數;map 函數完成多數任務。顧名思義,是任務映射。例:映射希望過濾或查找任何 "事物" 或面向。
已定義兩罰單:一由 Officer Ree 開出,另由 Officer Richards 發出。例:查找 Officer Ree 所有罰單,編寫 map 函數過濾相應 officer 屬性。將結果傳給 CouchDB emit 函數。
可透過 CouchDB REST API 或管理介面 Futon 定義視圖。Futon 僅 Web 應用程式,可從 https://localhost:5498/_utils/ 下載。立即訪問 (假定已跟隨建立資料庫和文件),應見針對 parking_tickets 簡單介面,如圖 1:

選擇 parking_tickets 資料庫,隨後最右側見下拉列表 (Select view:)。選 Custom query... 定義自定義視圖,如圖 2:

Futon 介面允許定義 map 和 reduce 函數。(可能需點 查看代碼 連結)。Map 文字框定義簡單映射如範例 5:
function(doc) {
if(doc.officer == "Kristen Ree"){
emit(null, doc);
}
}
範例 5 map 函數用 JavaScript 定義。作用:透過文件 officer 屬性過濾 CouchDB 資料庫文件。僅警官名稱 Kristen Ree 時傳遞文件給 emit。圖 3 顯示 Futon 定義此函數位置:

指定文件名稱 (輸入 by_name) 和視圖名稱 (輸入 officer_ree)。這些名稱作為建立 URL 稍後調用視圖途徑 (URL 即 https://localhost:5498/parking_tickets/_view/by_name/officer_ree)。
現在透過 HTTPBuilder 使用該視圖:
response = client.get(path: "parking_tickets/_view/by_name/officer_ree",
contentType: JSON, requestContentType: JSON)
assert response.data.total_rows == 1
response.data.rows.each{
assert it.value.officer == "Kristen Ree"
}
視圖回傳 JSON 回應,只含一個文件:Officer Ree 1 月 31 日開出罰單。透過解析 JSON,範例 6 response 物件隱藏原始 HTTP 回應。可透過對 response data 屬性呼叫 toString 查看原始 JSON 回應。原始回應如範例 7:
{"total_rows":1,"offset":0,"rows": [
{"id":"1234334325","key":null,
"value":{"_id":"1234334325","_rev":"4205717256","officer":"Kristen Ree",
"location":"199 Baldwin Dr","vehicle_plate":"Maryland 77777",
"offense":"Parked in no parking zone","date":"2009/01/31"}}]}
從原始 JSON 文件看出,HTTPBuilder 極輕鬆解析 JSON,因支援透過類物件圖機制估算各屬性及對應值。
為演示方便,向資料庫再添加文件。跟隨範例請用 代碼下載 完成相同任務。
CouchDB emit 函數充當各種形式組織器。若 map 函數未添加限制 (如範例 5),emit 基本作用對傳入文件排序。例:希望按日期獲取所有罰單 (可視為 SQL ORDER BY),按文件 date 執行 emit,如範例 8:
function(doc) {
emit(doc.date, doc);
}
範例 9 對此視圖發出 HTTP GET (指定 dates 設計文件名,by_date 視圖名)。
response = client.get(path: "parking_tickets/_view/dates/by_date", contentType: JSON,
requestContentType: JSON)
assert response.data.total_rows == 4
查詢按日期順序回傳 parking_tickets 資料庫所有文件。assert 驗證 total_rows 屬性等於 4。關鍵點:視圖回傳結果及少許中繼資料 (如回傳文件數量);有助開始解析前查看原始回應。範例 10 顯示原始結果:
{"total_rows":4,"offset":0,"rows":[
{"id":"85d4dbf45747e45406e5695b4b5796fe","key":"2009/01/30",
"value":{"_id":"85d4dbf45747e45406e5695b4b5796fe","_rev":"1318766781",
"officer":"Anthony Richards","location":"54th and Main",
"vehicle_plate":"Virginia FCD-4444","offense":"Parked in no parking zone",
"date":"2009/01/30"}},
{"id":"1234334325","key":"2009/01/31",
"value":{"_id":"1234334325","_rev":"4205717256",
"officer":"Kristen Ree","location":"199 Baldwin Dr",
"vehicle_plate":"Maryland 77777","offense":"Parked in no parking zone",
"date":"2009/01/31"}},
{"id":"12345","key":"2009/01/31",
"value":{"_id":"12345","_rev":"1479261876",
"officer":"Anthony Richards","location":"1893 Main St",
"vehicle_plate":"Maryland 4433-OP","offense":"Parked in no parking zone",
"date":"2009/01/31"}},
{"id":"12339892938945","key":"2009/02/01",
"value":{"_id":"12339892938945","_rev":"12351463","officer":"Anthony Richards",
"location":"Walmart Parking lot","vehicle_plate":"Maine 4433-OP",
"offense":"Parked in non-parking space","date":"2009/02/01"}}]}
定義此類視圖時,可隨後傳遞值 — 即 emit 函數初始值。例:範例 8 視圖主要按日期排序。若希望按具體日期,將日期傳遞給視圖查詢。例:瀏覽器網址列輸入:
https://localhost:5498/parking_tickets/_view/dates/by_date?key="2009/01/31"
視圖只回傳 1 月 31 日發出罰單。瀏覽器視窗應見 JSON 風格文字,類似範例 11。注意用瀏覽器作查詢工具是查看 HTTP 請求原始 JSON 回應極簡單方式。
{"total_rows":4,"offset":1,"rows":[
{"id":"1234334325","key":"2009/01/31",
"value":{"_id":"1234334325","_rev":"4205717256","officer":"Kristen Ree",
"location":"199 Baldwin Dr","vehicle_plate":"Maryland 77777",
"offense":"Parked in no parking zone","date":"2009/01/31"}},
{"id":"12345","key":"2009/01/31",
"value":{"_id":"12345","_rev":"1479261876","officer":"Anthony Richards",
"location":"1893 Main St","vehicle_plate":"Maryland 4433-OP",
"offense":"Parked in handicap zone without permit","date":"2009/01/31"}}]}
可按需定製視圖功能。例:少量 JavaScript 字串操作,編寫查找 Main Street 開出罰單視圖,如範例 12:
function(doc) {
if(doc.location.toLowerCase().indexOf('main') > 0){
emit(doc.location, doc);
}
}
範例 12 可見:若文件 location 元素含 main,文件傳遞給 emit。記住搜尋相當廣泛。若文件 location 含 Germaine Street 字串也會回傳。對定義少量罰單,視圖回傳如範例 13:
{"total_rows":2,"offset":0,"rows":[
{"id":"123433432asdefasdf4325","key":"4th and Main",
"value":{"_id":"123433432asdefasdf4325","_rev":"498239926",
"officer":"Chris Smith","location":"4th and Main",
"vehicle_plate":"VA FGA-JD33","offense":"Parked in no parking zone",
"date":"2009/02/01"}},
{"id":"123433432223e432325","key":"54 and Main",
"value":{"_id":"123433432223e432325","_rev":"841089995",
"officer":"Kristen Ree","location":"54 and Main Street",
"vehicle_plate":"Maryland 77777","offense":"Parked in no parking zone",
"date":"2009/02/02"}}]}
注意 JSON 回應含 key 元素描述為何生成特定文件。此資訊相當有用。還需注意定義罰單數據在一致性方面做得不好:有些地點準確,有些不精確。雖可存關聯式資料庫,但同樣可用面向文件模型。另外,借助 Groovy 和 HTTPBuilder 高效解析 JSON 強大能力,獲取數據極輕鬆 (比原始 JDBC 簡單很多)。
CouchDB 迷人之處在於使用極簡單。關聯式資料庫也簡單,但此資料庫優勢在於只需熟悉 Web 瀏覽器即可掌握 API。此外,CouchDB 提供 REST 風格 API,可透過炫酷框架通訊,如 HTTPBuilder RESTClient。不受制於 HTTPBuilder;各種 Java 函式庫嘗試簡化 CouchDB 使用。極有潛力之一 jcouchdb,幫助避免 REST 化和 JSON 化,同時允許以程式設計方式用 Java 語言操作文件和視圖。
敬請期待下月專欄,回頭討論 Google App Engine。開放創新精神旗幟下,各種新框架不斷湧現,促進 Google App Engine 開發部署。將了解它們如何簡化 Google 雲平台上 Java 開發 2.0。