人們非常喜愛 Ajax 應用程序,以至於他們十分樂於使用 Ajax 應用程序而不想使用等效的桌面程序。但惟一的問題是出現在網路無法訪問的時候怎麼辦。這是必須要用離線功能的場景。Apache Derby 是支持 Ajax 應用程序實現離線訪問的優秀選擇。了解如何使用 Apache Derby 作為本地資料庫,該資料庫可以實現 Ajax 應用程序的離線使用。
先決條件和系統要求
本文將使用 Apache Derby 作為客戶端資料庫。Derby 可以單獨下載,但是也被綁定到 Java™ 6 中並被稱為 Java DB。在本文中,我們將把 Derby V10.4.1.3 與 Java 5 和 Java 6 結合使用。我們將利用 Java Applet 在瀏覽器中啟用 Derby 並且使用 JavaScript 訪問 Applet。因此,強烈建議熟悉 Java Applet 和 JavaScript。Derby 允許使用普通 JDBC 和 SQL,因此需要熟悉這些內容(請參閱 參考資料)。
Apache Derby
Apache Derby 是任何一個 Java 應用程序都可以使用的嵌入式資料庫。它是非常有用的工具,因此綁定在 Java Platform, Standard Edition (Java SE) V6 中。雖然嵌入式資料庫的應用不計其數,但是許多人都不知道用 Derby 可以實現的一些客戶端功能。我們將通過構建一個簡單的地址本應用程序研究其中一些應用。我們將從利用 Apache Derby 的 Java Applet 開始,最終實現一個使用 Derby 作為緩存的基於 Ajax 的應用程序。
數據訪問
對於一篇有關資料庫技術的文章,應當首先從資料庫代碼開始討論。首先,讓我們定義用於存儲聯繫人的簡單的表模式,如下所示:
您可以設想更複雜的聯繫人模式,如添加多個電話號碼、地址等。但是,對於我們的應用程序來說,使用目前的這種模式剛剛好。當然,存在一個與 Contact 表相對應的 Java 類。在本例中,我們將遵循 Active Record 模式並用能夠執行所有資料庫操作的類封裝資料庫行。其代碼如下所示:
public class Contact { private Integer id; private String firstName; private String lastName; private String email; public static List<Contact> getContacts(String clause){ if (clause == null) clause = ""; String sql = SELECT_SQL + clause; Connection conn = DbManager.getConnection(); List<Contact> contacts = new ArrayList<Contact>(); try { ResultSet cursor = conn.createStatement().executeQuery(sql); while (cursor.next()){ Contact c = new Contact(); c.setId(cursor.getInt(1)); c.setFirstName(cursor.getString(2)); c.setLastName(cursor.getString(3)); c.setEmail(cursor.getString(4)); contacts.add(c); } } catch (SQLException e) { e.printStackTrace(); } return contacts; } public static List<Contact> getAllContacts(){ return Contact.getContacts(null); } public static Contact getContact(String clause){ List<Contact> results = Contact.getContacts(clause); if (results == null || results.size() != 1){ return null; } else return results.get(0); } public void save(){ if (id == null) insert(); else update(); } public void delete(){ Connection conn = DbManager.getConnection(); String sql = "delete from Contact where id=?"; try{ PreparedStatement ps = conn.prepareStatement(sql); ps.setInt(1, id); ps.executeUpdate(); System.out.println("Deleted contact id="+id); } catch (SQLException e){ e.printStackTrace(); } } private void insert() { Connection conn = DbManager.getConnection(); try { PreparedStatement ps = conn.prepareStatement(INSERT_SQL, PreparedStatement.RETURN_GENERATED_KEYS); ps.setString(1, firstName); ps.setString(2, lastName); ps.setString(3, email); ps.executeUpdate(); ResultSet autoRs = ps.getGeneratedKeys(); if (autoRs.next()){ id = autoRs.getInt(1); } System.out.println("Contact saved new id = " + id); } catch (SQLException e) { e.printStackTrace(); } } private void update(){ Connection conn = DbManager.getConnection(); try{ PreparedStatement ps = conn.prepareStatement(UPDATE_SQL); ps.setString(1, firstName); ps.setString(2, lastName); ps.setString(3, email); ps.setInt(4, id); ps.executeUpdate(); System.out.println("Contact updated with id="+id); } catch (SQLException e){ e.printStackTrace(); } } |
該類將完成很多任務,但是所有內容都非常簡單。它有與資料庫列對應的欄位及用於每個欄位的常用存取器(getter 和 setter)。它擁有執行所有創建/更新/刪除(CReate Update Delete,CRUD)操作及其附帶 SQL 查詢的方法。例如,查詢聯繫人的方法是靜態方法,因此可以執行類似 Contact.getAllContacts() 的操作。保存和刪除操作是實例方法,因此對個別聯繫人調用這些方法。此處沒有顯示查詢,因為查詢是標準的 SQL。該類還有常用的 getter 和 setter,但是為了簡短起見並未顯示(請參閱 下載 以獲得完整的代碼清單)。該類將用 Derby 構造基本的客戶端存儲。首先,我們將使用它作為 Applet UI 的一部分,但是稍後該 Applet 將把它用於 JavaScript。注意,對於每個方法,我們將調用實用程序類 DbManager 以獲得連接。
package org.developerworks.addressbook; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DbManager { static{ try { Class.forName("org.apache.derby.jdbc.EmbeddedDriver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection(){ try { Connection conn = DriverManager.getConnection("jdbc:derby:contacts; create=true"); return conn; } catch (SQLException e) { e.printStackTrace(); } return null; } } |
這是所有特定於 Derby 的代碼所在的位置。實際上,並不是特別特定於 Derby。我們使用的是嵌入式資料庫,但是我們處理它的方式和通過 JDBC 訪問的其他資料庫一樣。我們將在類的靜態初始化器中把 EmbeddedDriver 用於驅動程序類。另外對於 getConnection 方法,我們將添加額外的參數 create=true 來告訴 Derby 在資料庫尚不存在時創建資料庫。注意,不需要像使用 JDBC 那樣用到用戶名或密碼 — 這是使用嵌入式資料庫的優點之一。您看到了所有的數據訪問代碼。它看起來非常類似於在任何一個應用程序中都可以看到的資料庫代碼;而資料庫剛好是嵌入式 Derby 資料庫。您可以設想為應用程序創建其他模型,這樣可以存儲專用於應用程序的數據,但是位於客戶端。讓我們查看一個利用清單 2 中所示的數據訪問代碼的簡單應用程序。
Applet UI
首先使用一個非常簡單的 Applet,該 Applet 將使用數據訪問代碼。
public class AddressBookApplet extends JApplet { private static final long serialVersionUID = 1L; private static final String[] columns = { "First Name", "Last Name", "Email", "Id"}; public AddressBookApplet() { this.setLayout(new GridLayout(1,0)); JPanel panel = buildUi(); this.add(panel); } public Contact addContact(String firstName, String lastName, String email) { Contact c = new Contact(); c.setFirstName(firstName); c.setLastName(lastName); c.setEmail(email); c.save(); return c; } public void deleteContact(Integer id){ Contact c= new Contact(); c.setId(id); c.delete(); } public Object[][] loadContacts() { List<Contact> book = Contact.getAllContacts(); Object[][] contacts = new Object[book.size()][4]; int cnt = 0; for (Contact contact : book){ contacts[cnt++] = contact.toArray(); } return contacts; } private JPanel buildUi() { JPanel panel = new JPanel(); panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); final DefaultTableModel dataModel = createDataModel(); final JTable table = createTable(dataModel); //Lots of Swing/UI Code omitted for brevity } private JTable createTable(final DefaultTableModel dataModel) { final JTable table = new JTable(dataModel); table.setPreferredScrollableViewportSize(new Dimension(500, 70)); table.setFillsViewportHeight(true); return table; } private DefaultTableModel createDataModel() { Object[][] contacts = loadContacts(); final DefaultTableModel dataModel = new DefaultTableModel(contacts, columns); return dataModel; } } |
這段代碼大部分是構建 UI 的典型 Swing 代碼。所有 UI 代碼都是在位於類底部的私有方法中完成的。buildUi 方法處理 Swing 組件的創建,但是為了簡短起見而省略了大部分內容。更有趣的是三個公共方法(除了構造函數之外):addContact、deleteContact 和 loadContacts。這三個方法實質上都是先前開發的數據訪問代碼的包裝器。實際上,我們不需要將 Applet 用於最終應用程序的 UI,但是它提供了測試代碼的簡單方法。如果使用的是 Eclipse,則只需在 Applet 類上右鍵單擊並選擇 Run As > Java Applet。
Eclipse 在這裡並不神秘,它只是使用 JDK 的 Applet Viewer。如果使用的不是 Eclipse,則可以在命令行中調用此程序。不管採用哪種方法,您都應當會得到類似圖 3 的內容。
添加和刪除聯繫人以測試應用程序。這是開發和測試稍後使用 JavaScript 訪問的客戶端資料庫代碼的簡單方法。Applet UI 是一個近乎完美的單元測試。我們幾乎可以獲取它並將其添加到 Web 頁面中,但並不完全如此。首先,有一些安全注意事項需要處理。
安全性
我們將為 Applet 中使用的 JAR 設置簽名。如果這裡繼續遵循計劃,有一件事好到令人難以置信。Derby 將給我們提供嵌入到客戶機中的持久資料庫(所有內容都存儲在客戶機中)。它有幾分像 HTTP Cookie,但是眾所周知,那些 Cookie 在每個域中不可以超過 4 KB。客戶機中的 Derby 資料庫的限制是什麼?答案是要麼很多,要麼很少。
默認情況下,Applet 無法訪問本地文件系統,因此 Derby 無法在客戶機中存儲任何數據。那麼使用 Derby 是做白日夢么?幸運的是,它不是。關鍵是您必須給 Applet 設置數字簽名。有簽名的 Applet 將獲得本地文件系統的訪問權,這樣如果數據來自有簽名的 Applet,則 Derby 可以持久存儲這些數據。我們只需給 Applet 設置簽名。
$ keytool -genkey -alias sigs -keystore sigstore -keypass password -storepass password What is your first and last name? [Unknown]: Michael What is the name of your organizational unit? [Unknown]: developerWorks What is the name of your organization? [Unknown]: IBM What is the name of your City or Locality? [Unknown]: San Jose What is the name of your State or Province? [Unknown]: CA What is the two-letter country code for this unit? [Unknown]: US Is CN=Michael, OU=developerWorks, O=IBM, L=San Jose, ST=CA, C=US correct? [no]: yes $ jarsigner -keystore sigstore -storepass password -keypass password -signedjar addrbook.jar derby.jar sigs Warning: The signer certificate will expire within six months. |
正如您所見,我們使用兩個 JDK 工具給 Applet(技術上是包含 Applet 的 JAR)設置簽名。首先,使用 keytool 創建用於保存生成的加密密鑰的密鑰庫。還可以使用它執行創建 SSL 證書之類的任務。在擁有密鑰后,結合使用該密鑰與 jarsigner 工具來給 JAR 設置簽名。注意,我們包括了 Derby JAR,以及包含自定義代碼的 addrbook JAR。最終獲得了一個自簽名 Applet 的示例。這對於開發來說沒問題,但是通常不適用於任何面向用戶的代碼。在這種情況下,您將需要來自受信任提供商(如 VeriSign)的密鑰/證書。關鍵原因是因為需要在客戶機中存儲數據,我們需要執行這些附加步驟才能符合 Java 語言的客戶端安全模型。牢記這些安全事項並且擁有一顆可以正常工作的 Applet 后,我們現在已經準備好從 JavaScript 使用 Applet。
帶有 JavaScript 的 Applet
可以將 Applet 用於 Web 應用程序中的所有內容,但是在 Web 應用程序的 UI 中使用標準的 HTML 和 JavaScript 更加常見。我們仍然可以這樣做並且使用 Applet 作為訪問嵌入式 Derby 資料庫的方法。只需在 JavaScript 與 Applet 之間完成一些集成。
集成 Applet
創建代碼以實現 JavaScript 與 Java Applet 之間的通信聽起來可能十分棘手,或者聽上去像是某種新技術。實際上,這類集成從 Netscape Navigator 時代起就一直在進行,並且在那時使用的技術仍然起作用。首先,讓我們查看僅用於將 Applet 嵌入到頁面中的 HTML 代碼。
<applet alt="Address Book Applet" name="addrBookApplet" code="org.developerworks.addressbook.AddressBookApplet" width="400" height="200" archive="addrbook.jar, derby.jar"> </applet> |
這是非常標準的 Applet 嵌入代碼。它將應用不推薦使用的 <applet> 標記。它仍然受跨瀏覽器支持。它是可以與 Microsoft® 和 Mozilla 瀏覽器結合使用的惟一標記。因此如果不需要使用它,則需要使用 JavaScript 來判斷瀏覽器(確定用戶在使用哪種瀏覽器)。如果是 Internet Explorer,則將使用 <object> 標記。否則,將 <embed> 標記與嵌套的 <param> 標記結合用於 height、width 和 archive 屬性之類的內容。此外,如果使用 <object> 標記,則還需要一個可編寫腳本的參數以允許 JavaScript 調用 Applet 的 Java 方法。如果使用 <applet> 標記,則不需要這樣做。
您應當知道的另一個屬性是 MAYSCRIPT。此屬性用於授予 Applet 許可權以在頁面中執行 JavaScript。此屬性會非常有用,但是在本例中不需要它。我們將使用 JavaScript 訪問 Applet(JavaScript 將調用 Applet 的 Java 方法)。但是,該 Applet 將不會調用 JavaScript,因此不需要 MAYSCRIPT 屬性。那麼如何從 JavaScript 中調用那些 Java 方法?
function saveContact(firstName, lastName, email){ var applet = document.addrBookApplet; var newContact = applet.addContact(firstName, lastName, email); addContactToUi(newContact); } |
在 saveContact 函數中執行的第一個操作是獲得 Applet 中的句柄,方法是使用 Applet 的名稱(清單 5 中的 name 屬性)。我們將在其中直接調用 addContact 方法,然後它將在清單 6 的代碼中返回一個新 Contact 對象。我們將把該對象傳遞給另一個 JavaScript 函數以用新聯繫人更新 UI。不需要更多內容。就是這樣簡單。可以將 Applet 僅用於持久性,並且可以將 JavaScript 用於所有其他內容。
使用沒有頭文件的 Applet
現在可以使 Applet 沒有頭文件(就是一個沒有 UI 的代碼庫)。為此,只需刪除清單 3 中的所有 UI 並且只在頂部保留那些公共方法。需要略微調整 UI 嵌入代碼。
<applet alt="Headless Applet" name="headlessApplet" code="org.developerworks.addressbook.HeadlesApplet" width="1" height="1" archive="addrbook.jar, derby.jar"> </applet> |
此處惟一有趣的是把寬度和高度都設為 1。這實際上將使 Applet 在頁面中不可見。因此,最終用戶根本不知道頁面中有一個 Applet。它將是一個不可見的 helper。當然,如果更改 Applet 的名稱,則還需要調整 JavaScript,這是另外一處需要修改的內容。我們已經準備好使用此客戶端持久化工具來進一步增強 Web 應用程序。
創建 Ajax 緩存
Applet 能夠執行用 Ajax 可以完成的許多相同操作,並且如我們所見,Applet 可以完成更多操作。在本例中,我們只對 “多出來” 的操作感興趣。Applet 可以與伺服器進行通信,但是我們將堅持用 Ajax 完成該操作。使用 Derby 作為嵌入式客戶端資料庫能夠使我們完成單獨用 Ajax 無法完成的操作。更為重要的是:可以將 Derby 用作存儲來自伺服器的數據的強大客戶端緩存。
使用 Derby 作為緩存
下面是我們的想法:在伺服器中保存聯繫人,而所有添加和刪除之類的操作都通過 Ajax 調用完成。但是,將在客戶機中的 Derby 中保存相同信息並將其用作緩存。因此,可以從 Derby 中載入所有聯繫人。
function init(){ var book = document.headlessApplet.loadContacts(); var i = 0; var contact = {}; for (i=0;i<book.length;i++){ contact = {firstName:book[i][0], lastName:book[i][1], email:book[i][2], id:book[i][3]}; addContactToUi(contact); } } |
該函數將從 Applet 中載入聯繫人,然後進行遍歷以將每個聯繫人添加到 UI 中。這是在頁面首次裝入時調用的代碼。通常,我們將從伺服器中獲得聯繫人,並且等待伺服器響應會導致產生延遲。使用 Derby 緩存就不會產生延遲。但是,需要確保伺服器與 Derby 緩存中的聯繫人之間保持同步。
保持同步
必須確保緩存是準確的,因此需要確保它與伺服器同步。完成此操作的最簡單方法是向伺服器發送非同步更新並且在處理期間,更新緩存和應用程序的 UI。
function addContact(){ var contact = {}; var firstName = document.getElementById("firstName").value; var lastName = document.getElementById("lastName").value; var email = document.getElementById("email").value; var req = getXhr(); // get the browser specific XMLHttpRequest object var params = "firstName="+username + ",lastName="+lastName+",email="+email; req.open("POST", "/contact/create", true); req.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); req.setRequestHeader("Content-length", params.length); req.setRequestHeader("Connection", "close"); req.onreadystatechange = handleResponse; req.send(params); // update cache saveContact(firstName, lastName, email); } |
這段代碼大部分是使用 XMLHttpRequest 對象的典型 Ajax 代碼。只需照常發送 XMLHttpRequest。這將是非同步的,因此 req.send() 調用將立即返回。然後可以調用清單 6 中的 saveContact() 函數。注意,我們註冊了一個處理程序(通過設置 req.onreadystatechange 屬性)。使用這種方法,代碼需要處理調用伺服器失敗的情況。這可能會在伺服器臨時關閉或者在用戶遇到網路問題時發生。這裡可以變得複雜一些並將更新放入隊列,這樣可以在稍後伺服器或網路恢復時重新執行更新。此外,我們可以把 saveContact 調用移到處理程序中。該 UI 的響應能力並不十分強大,但是用這種方法,我們只需在成功更新伺服器時更新 UI 和緩存。
結束語
我們已經看到使用 Derby 作為嵌入式資料庫有多麼簡單,也看到了如何把這個資料庫和將數據持久化到客戶機的 Java Applet 結合使用。這裡有一些必須考慮的安全因素 — 只有經過簽名的 Applet 才可以將數據寫入客戶機的文件系統 — 但是我們查看了給 Applet 設置簽名的過程和工具。牢記這些內容后,我們可以輕鬆地從 JavaScript 中訪問 Applet 並把 Derby 轉換為支持 Ajax 的 Web 應用程序的緩存。即使伺服器臨時不可用,也可以訪問數據。因而,Derby 可以成為任何 “離線” 策略的關鍵部分,從而在伺服器不可用時訪問數據。(責任編輯:A6)
[火星人 ] 用 Apache Derby 構建離線 Ajax已經有539次圍觀