本文是使用 Google Web Toolkit(GWT)構建 Asynchronous JavaScript + XML(Ajax)應用程式系列文章的第 2 部分,介紹如何為 Web 應用程式構建 Apache Derby 資料庫,並使用它驅動 GWT。本系列文章的 第 1 部分 向您介紹了 GWT,並示範了如何使用它來為 Web 應用程式建立富客戶機前端。這一次,您將走進幕後,瞭解如何使用資料庫和用於將資料轉換為 GWT 可用格式的代碼,從而設定後端。閱讀完本文後,您將可以使前端和後端相互連信。
在本文中,您將安裝並設定資料庫 —— Web 應用程式的後端,來建立資料庫模式,並瞭解一些用於向其中填充資料的簡單工具。您將要使用的資料庫是 Apache Derby,100% 純 Java 關係型資料庫,該資料庫最初是在 Cloudscape 的名下開發的。最後,IBM® 收購了 Cloudscape 代碼,繼而將其開源版本貢獻給了 Apache 項目。Sun Microsystems 的 JavaDB 名下發行了同樣的一個項目,但兩者沒有絲毫混同之處。
|
| 查看 Ajax 資源中心,這是您的一站式 Ajax 編程模型資訊中心,包括文章、教程、論壇、blog、wiki、事件和新聞。所發生的任何事情都會在這裡介紹。 |
|
我選擇 Derby 並不是因為它有三個名字,而是因為它是輕量級的並且易於配置。與大多數關係型資料庫不同,Derby 可以在與 Java 端伺服器代碼所在的同一 JAVA 虛擬機器(JVM)中運行。(如果您喜歡,也可以在單獨的 JVM 上運行它。)這使開發和部署變得更容易,而且 Derby 的速度很快,是中小型 Web 應用程式的理想選擇。
開始之前,有幾點注意事項:第一,要讀懂本文,您應當掌握關係型資料庫、JDBC 和結構化查詢語言 (SQL)(SQL)的基礎知識。第二,為了達到示範目的,本文在代碼中提供了一些在生產系統中可能不太理想的內容。我在講述過程中嘗試將那些元素指出來,但是在這裡將不討論效能最佳化問題。
獲得 Derby
Derby 是作為 Apache DB 項目的一部分提供的。撰寫本文時,最新版本是 10.1.3.1 版。如果要在 Eclipse 整合式開發環境(IDE)中工作,則擷取 derby_core_plugin 和 derby_ui_plugin 兩個外掛程式就足夠了。如果不是,則可選擇滿足您需求的任何其他發行版。這些發行版中,有的只包含庫檔案,有的包含庫和文檔,有的包含帶有調試資訊的庫,還有只有原始碼的發行版。Derby 專以 Java 技術為基礎,可以在任何 1.3 或更高版本的 JVM 上運行。本文中的程式碼範例假定您使用的是 Java 1.4。
不使用 Eclipse 設定 Derby
如果不使用 Eclipse,請將下載的發行版解壓到您認為方便的任意位置。完成後,請確保檔案 lib/derby.jar 和 lib/derbytools.jar 位於 classpath 變數中。您可以在系統級執行此操作,這樣做可能有助於將環境變數 DERBY_INSTALL 設為 Derby 所在的目錄(包括 Derby 目錄本身,位於 /opt/bin/db-derby-10.1.3.1-bin)。還可以在 IDE 或啟動程式指令碼中執行此操作。如果需要以客戶機/伺服器模式和嵌入模式使用 Derby,則檔案 lib/derbyclient.jar 和 lib/derbynet.jar 還必須在 classpath 中。
使用 Eclipse 設定 Derby
如果使用 Eclipse,為開發所做的設定工作會比較輕鬆一點。要在 Eclipse 中設定 Derby,請完成以下步驟: 將兩個外掛程式檔案解壓縮。每個外掛程式檔案都有一個名為 plugin 的頂級目錄。 將該目錄中的內容複寫到 Eclipse 外掛程式目錄中。 在 Eclipse 中開啟您的項目。 單擊 Project > Add Apache Derby Nature 進入 Derby 夢幻世界。這樣做將把四個庫檔案添加到項目 classpath 中並為您提供對 ij 命令列提示符的訪問權。
圖 1 顯示了添加了 Derby Nature 之後的 Derby 菜單。
圖 1. Eclipse Derby 菜單
即使使用 Eclipse 進行開發,部署應用程式時也必須有相應的 JAR 檔案。我會在稍後的一篇文章裡詳細介紹此主題。
設計您的模式
在開始使用資料庫之前,請花點時間來瞭解一下資料庫應當儲存哪些內容。我尚未討論 Slicr 應用程式的需求,因此讓我們假定,您希望資料庫能夠儲存基本的客戶資訊和訂單資訊。
在產品的早期階段處理資料庫的技巧是使其保持簡單並且盡量少使用特定於資料庫系統的功能,即使這意味著初始時要在 Java 代碼中執行額外的處理。資料庫與第三方有很強的依賴關係,因此應避免讓資料庫決策來驅動應用程式的其餘部分。需要使程式與資料庫之間的聯絡點儘可能少,以便在某個點更改系統時,可以順利變更。壓力來自在改善資料庫效能時所做的的大部分事務都會要求您使用特定的系統,因此嘗試將此類最佳化工作延遲至項目必須進行最佳化的最後一刻。
資料庫設計的起點很簡單。客戶下訂單。訂單包括一份或多份比薩(此時,忽略快餐店可能出售其他食物)。比薩有無澆頭或多種澆頭之分,可按半張或整張比薩放澆頭。
建立客戶表
現在,您只需關心獲得足夠的客戶資訊來交付和確認訂單,如清單 1 所示。
清單 1. 客戶表
CREATE TABLE customers ( id int generated always as identity constraint cust_pk primary key, first_name varchar(255), last_name varchar(255), phone varchar(15), address_1 varchar(200), address_2 varchar(200), city varchar(100), state varchar(2), zip varchar(10)) |
CREATE 語句稍微有一些不符合標準的 SQL 文法。建立一個 ID 列,您需要 Derby 為該列中的每個新行自動加一。指定該行為的子句為: id int generated always as identity
Identity 列的其他選項為: generate by default as identity
差別在於 generate by default 允許您將自己的值放入該列中,然而 generate always 不允許這樣做。還將 ID 列標識為表的主鍵。
您總是希望資料庫中具有一個和現實中的數字完全沒有聯絡的 ID。最終團隊中總是會有某個成員試圖說服您使用電話號碼之類的資料作為主鍵,因為它將惟一地標識客戶。不要那樣做。您絕對不會希望由於有人移動和更改了電話號碼而更新整個資料庫。
建立訂單表
對於訂單表(參見清單 2),只需要將其綁定與一位客戶和一個日期相關聯,並允許折扣。您可以在代碼中計算價格的其餘部分。
清單 2. 訂單表
CREATE TABLE orders ( id int generated always as identity constraint ord_pk primary key, customer_id int constraint cust_foreign_key references customers, order_time timestamp, discount float) |
除了 id 主鍵以外,還聲明了 customer_id 列作為引用客戶表的外鍵。(如果在聲明中不包括外鍵,Derby 將假定引用的是其他表的主鍵。)這意味著 Derby 將驗證添加到這張表的任何 customer_id 實際上是否與系統中的客戶匹配。系統管理員將告訴您應當始終執行此匹配操作。但是,我認為在一些合理的情況下可能不需要資料庫一直都執行嚴格驗證。例如,可能需要先輸入資料,然後才能知道或驗證外鍵是什麼。此外,可能需要刪除外鍵但需要保留表行。例如,在本例中,您可能需要刪除一個客戶,但出於資料收集目的,需要保留客戶的訂單。您可以通過一些技巧使 Derby 允許那樣做,但是可能無法移植到其他資料庫系統。
建立澆頭表
最後一個資料庫設計問題是比薩和澆頭。恩,其實澆頭本身並不是什麼問題;那十分簡單,如清單 3 所示。
清單 3. 澆頭表
CREATE TABLE toppings( id int generated always as identity constraint top_pk primary key, name varchar(100), price float) |
問題在於,如何管理比薩與澆頭的關係。一張比薩就等同於一份訂單,涉及尺寸和一組澆頭。典型的資料庫標準會建議需要先建立一張比薩表,然後再建立一張將比薩 ID 與澆頭 ID 關聯起來的多對多表。這樣做有很多很好的屬性,其中一個事實是它允許在一個比薩上使用無數種澆頭。但是,管理表之間的資料庫關係會造成效能損耗。如果不需要如此多的澆頭,則可以將若干個澆頭欄位包括在比薩表中(topping_1、topping_2 等等)。從概念上講,那會更簡單點,但會導致難於(比方說)挖掘訂單資料來統計最受歡迎的澆頭。如果您特別敢於冒險,您可以用一個澆頭欄位並用一張位元影像或連接字串等內容來填充該欄位。但我真的不建議那樣做。
建立比薩表
經過一番考慮之後,我決定使用完全正規的表單。您希望對一個比薩使用足夠多的澆頭,將它們全都放在同一張表中會變得十分難看。因此,請使用清單 4 中所示的代碼。
清單 4. 比薩表
CREATE TABLE pizzas ( id int generated always as identity constraint piz_pk primary key, order_id int constraint order_foreign_key references orders, size int )CREATE TABLE pizza_topping_map ( id int generated always as identity constraint ptmap_pk primary key, pizza_id int constraint pizza_fk references pizzas, topping_id int constraint topping_fk references toppings, placement int) |
僅為了清晰起見,將用尺寸 1、2、3、4 分別表示小號、中號、大號和超大號。左半個比薩、整個比薩和右半個比薩的澆頭布置分別為 -1、0 或 1。並且每個映射都必須要有一個單獨的 ID,以便可以,比方說,允許額外添加意大利辣香腸,方法為讓意大利辣香腸作為澆頭在同一個比薩中出現兩次。
註:我是否曾提到過放在 constraint 之後的所有那些名稱在整個資料庫中必須是惟一的。它們必須是。Derby 實際上將在後台建立索引,並且每個索引都必須有一個惟一名稱。
對於您的資料庫模式,應當那樣做。現在可以將其置入資料庫中。
填充資料庫
模式已經有了;現在必須設定模式並準備一些初始資料。您將建立一個簡短的獨立程式來執行此設定過程。但是,那不是惟一的選擇。您可以使用 Derby ij 命令列直接輸入 SQL 命令,也可以使用一個圖形化 SQL 工具。不過,編程方法將給您提供一種可很好控制的方法來查看如何啟動 Derby 以及 Derby 與其他 JDBC 資料庫的區別。實際上,也可能把 SQL 模式儲存在它自己的 SQL 指令碼中。
開始時使用一些十分靜態資料 —— 第 1 部分的 Slicr 頁面中包括的比薩澆頭列表。同樣,這裡主要使用此方法,因為您要插入待用資料。設定一張澆頭表,其中每個澆頭都有一個名稱和一個底價。清單 5 中所示的代碼將設定該資料。目前,假定所有澆頭的價格都一樣。
清單 5. 在 Derby 中設定澆頭表
public class SlicrPopulatr { public static final String[] TOPPINGS = new String[] { "Anchovy", "Gardineria", "Garlic", "Green Pepper", "Mushrooms", "Olives", "Onions", "Pepperoni", "Pineapple", "Sausage", "Spinach" } public void populateDatabase() throws Exception { Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance(); Connection con = DriverManager.getConnection( "jdbc:derby:slicr;create=true"); con.setAutoCommit(false); Statement s = con.createStatement(); s.execute("DROP TABLE toppings"); s.execute("CREATE TABLE toppings(" + "id int generated always as identity constraint top_pk primary key, " + "name varchar(100), " + "price float)"); // // All the other create table statements from above would go here... // for (int i = 0; i < TOPPINGS.length; i++) { s.execute("insert into toppings values (DEFAULT, '" + TOPPINGS[i] + "', 1.25)"); } con.commit(); con.close(); try { DriverManager.getConnection("jdbc:derby:;shutdown=true"); } catch (SQLException ignore) {} } public static void main(String[] args) throws Exception { (new SlicrPopulatr()).populateDatabase(); } |
如果您熟悉 JDBC 的話,則不會對這段代碼中的大部分代碼感到陌生。不過,還有幾個特定於 Derby 的特性我必須介紹。開始時需要使用 Class.forName 方法裝入驅動類。由於要使用的是嵌入式版本的 Derby,因此驅動程式的類名為 org.apache.derby.jdbc.EmbeddedDriver。接下來,建立連接字串。Derby URL 的格式為: jdbc:derby:資料庫名稱;[attr=value]
資料庫名稱是引用資料庫時使用的名稱。您選什麼名稱都沒有關係,只要與在伺服器代碼中再次開啟資料庫時所用的名稱保持一致即可。
建立串連後,您就處於標準的 JDBC 中了。建立一個 Statement 來執行刪除和重新建立表的命令,這允許您在資料庫受損時通過此程式重設資料庫。(否則,Derby 將在嘗試建立一張已經存在的表時拋出異常)。建立表後,需要對澆頭數組中的每個條目使用一條 insert 語句。
insert 語句中的 SQL 代碼有一個您可能不需要的功能。我使用了關鍵字 DEFAULT 作為 Identity 列的預留位置。如果不在 insert 語句中指定欄位列表,則 Derby 希望使用 Identity 列中的關鍵字。
程式存在之前,需要進行一次特殊調用擷取一個與 URL "jdbc:derby:;shutdown=true" 的串連 —— 無需指定資料庫。此調用告訴 Derby 系統關閉並釋放所有可能活動的串連。
運行這個小程式後,您將在名為 derbyDb 的應用程式頂級目錄中看到一個目錄。此目錄將儲存 Derby 儲存資料的二進位檔案。請不要以任何方式更改那些檔案。
準備好資料用於 GWT
資料庫模式就緒並裝入了待用資料後,現在必須說明如何將資料通訊給客戶機,反之亦然。最後,您不得不序列化整個客戶機-伺服器串連上的資料。為了序列化運行,最後的資料類必須在 GWT 能看到和處理的位置,這意味著這些類必須定義在 client 軟體包中並且可由 GWT Java-to-JavaScript 編譯器編譯。
對於將要序列化的客戶機類還有一些附加限制。舉例來說,該類必須實現介面 com.google.gwt.user.client.rpc.IsSerializable,該介面是一個標記介面,不定義任何方法。此外,該類中的所有資料欄位本身必須是序列化的。(與普通的 Java 序列化一樣,您可以通過將欄位標記為 transient 使其免除被序列化。)
怎樣的欄位才是可序列化欄位。首先,該欄位可屬於一個實現了 IsSerializable 的類型,或者具有一個實現了 IsSerializable 的超類。或者,該欄位可以是基本類型之一,其中包括 Java 原語,所有原語封裝類,Date 和 String。序列化類別型的數組或集合也是序列化的。但是,如果要將一個 Collection 或 List 序列化,GWT 希望您用一個指定實際類型的 Javadoc 注釋對其評註,以便編譯器可以使其最佳化。清單 6 顯示了範例欄位和方法。
清單 6. 序列化欄位和方法
/** * @gwt.typeArgs <java.lang.Integer> */private List aList; /** * @gwt.typeArgs <java.lang.Double> * @gwt.typeArgs argument <java.lang.String> */public List doSomethingThatReturnsAList(List argument) { // Stuff goes here} |
註:方法列表中的參數只能由注釋中的名稱指定,而傳回值則不是。
注意,該表中的序列化對象中缺少處理 java.sql 和 JDBC 所必需的內容。無論您執行什麼操作來擷取設為資料對象的結果,都必須在伺服器端代碼中執行。
此刻,您進入了對象關係映射(Object-Relational Mapping,ORM)的世界,或者將資料從關係型資料庫結構轉換為 Java 程式的物件導向的結構。對於一個複雜的 Java 生產系統,可能需要使用一個預先存在的成熟的 ORM 系統,例如 Hibernate 或 Castor。這兩個系統都將自動把資料從資料庫裝入到所選的 Java 對象中。但是,它們也要求大量配置,然後才能開始。由於本文主要介紹 Derby 和 GWT,我提供了一個在開發開始時提供服務的快速轉換程式。最後,可以將其換為功能更強大的工具。
簡單的 ORM 轉換程式
首先,為所有資料表建立 bean 類。我使用 Topping 類作為樣本,因為它簡單並且已經包含有資料。對錶中的每個列使用普通 bean 命名規範,但將下滑線轉換為大小寫混合(例如,topping_id 變成 getToppingId)。清單 7 顯示了 Topping 類。
清單 7. Topping 類
package com.ibm.examples.client;import com.google.gwt.user.client.rpc.IsSerializable;public class Topping implements IsSerializable { private Integer id; private String name; private Double price; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Double getPrice() { return price; } public void setPrice(Double price) { this.price = price; }} |
接下來是簡單的 ORM 工具,如清單 8 所示。
清單 8. 簡單的 ORM 工具
package com.ibm.examples.server;import java.beans.PropertyDescriptor;import java.lang.reflect.Method;import java.sql.ResultSet;import java.util.ArrayList;import java.util.List;public class ObjectFactory { public static String convertPropertyName(String name) { String lowerName = name.toLowerCase(); String[] pieces = lowerName.split("_"); if (pieces.length == 1) { return lowerName; } StringBuffer result = new StringBuffer(pieces[0]); for (int i = 1; i < pieces.length; i++) { result.append(Character.toUpperCase(pieces[i].charAt(0))); result.append(pieces[i].substring(1)); } return result.toString(); } public static List convertToObjects(ResultSet rs, Class cl) { List result = new ArrayList(); try { int colCount = rs.getMetaData().getColumnCount(); while (rs.next()) { Object item = cl.newInstance(); for (int i = 1; i <= colCount; i += 1 ) { String colName = rs.getMetaData().getColumnName(i); String propertyName = convertPropertyName(colName); Object value = rs.getObject(i); PropertyDescriptor pd = new PropertyDescriptor(propertyName, cl); Method mt = pd.getWriteMethod(); mt.invoke(item, new Object[] {value}); } result.add(item); } } catch (Exception e) { e.printStackTrace(); return null; } return result; }} |
convertToObjects() 方法只簡單地在結果集中迴圈,使用 JavaBean 映射推斷 getter 屬性,並設定所有值。convertPropertyName() 方法將在 SQL 加下滑線的命名規範與 Java 大小寫混合的約定之間切換。
|
ORM 工具不具有的功能
如果把此工具缺少的所有有用的 ORM 功能彙集在一起,完全可以出一本書了。例如,工具不能: 避免建立同一個對象的多個版本。 允許回寫資料庫。 快速運行。 |
|
代碼所做的工作可能超出了您的想象。您可以在任何資料庫工具上立即運行它而無需進一步配置。早期開發時,模式可能更改,您無需使對應檔與資料庫保持同步。並且在需要時切換到功能更強大的工具也不難。
清單 9 顯示了啟動並執行這個工具,讀取回先前建立的所有 Topping 執行個體。
清單 9. 測試 ORM 工具
public class ToppingTestr { public static final String DRIVER = "org.apache.derby.jdbc.EmbeddedDriver"; public static final String PROTOCOL = "jdbc:derby:slicr;"; public static void main(String[] args) throws Exception { try { Class.forName(DRIVER).newInstance(); Connection con = DriverManager.getConnection(PROTOCOL); Statement s = con.createStatement(); ResultSet rs = s.executeQuery("SELECT * FROM toppings"); List result = ObjectFactory.convertToObjects(rs, Topping.class); for (Iterator itr = result.iterator(); itr.hasNext();) { Topping t = (Topping) itr.next(); System.out.println("Topping " + t.getId() + ": " + t.getName() + " is ___FCKpd___8quot; + t.getPrice()); } } finally { try { DriverManager.getConnection("jdbc:derby:;shutdown=true"); } catch (SQLException ignore) {} } }} |
此測試程式將建立一個與 Slicr 資料庫的 Derby 串連。(您將不再要求協議字串根據需要建立資料庫)。執行一個簡單的 SQL 查詢,然後將結果傳遞給工廠。然後可以自由地在結果清單內迴圈並退出資料庫。
下期精彩預告
現在已經安裝並配置了資料庫。建立了資料庫模式並且發現了一些簡單的工具可將資料放入資料庫中。閱讀了本系列文章中的兩篇之後,Slicr 項目現在已經具有了簡單但是可以工作的前端和後端。下一步是通訊。在本系列文章的第三篇文章中,您將瞭解 GWT 使用架構如何使遠端程序呼叫(Remote Procedure Call,RPC)可以輕鬆地編碼和管理。