My favorites | 中文(繁體) | Sign in
英文版或許有比此中譯版新的內容

透過 JDO 使用資料存放區

在可擴充的網路應用程式中儲存資料需要一些小技巧。使用者可以在指定時間內,與數十個網頁伺服器進行互動,而且使用者的下一個要求可能會送給不同的網頁伺服器,而不是處理前一個要求的伺服器。所有網頁伺服器需要資料互動,這些資料也是散佈在多部電腦中,而這些電腦也可能位於世界的各個角落。

有了「Google 應用服務引擎」之後,您就不需要擔心上述問題。「應用服務引擎」的基礎架構會透過簡單的 API 處理資料的散佈、複製以及負載平衡,而您會獲得強大的查詢引擎與交易功能。

「應用服務引擎」資料存放區是「應用服務引擎」提供的多種服務之一,它提供兩種 API:標準 API 和低階 API。 需要的話,您可以使用標準 API,輕鬆地將應用程式移植到其他的主機環境和資料庫技術。標準 API 可以將您的應用程式從「應用服務引擎」服務「解除聯結」。「應用服務引擎」也支援可直接提供服務功能的低階 API。您可以使用低階 API 實作新的介面卡介面,或在您的應用程式中直接使用 API。

「應用服務引擎」支援兩種不同的資料存放區 API 標準:Java Data Object (JDO) 和 Java Persistence API (JPA)。 這些介面是由 DataNucleus Access Platform (數種 Java 持續性標準的開放原始碼實作) 透過「應用服務引擎」資料存放區的介面卡所提供。

在訪客留言板中,我們將透過 JDO 介面擷取與張貼使用者所留下的訊息。

設定 DataNucleus Access Platform

Access Platform 需要一個設定檔,指示它使用「應用服務引擎」資料存放區做為 JDO 實作的後端。在最終的 WAR 中,這個檔案的名稱為 jdoconfig.xml,存放在 war/WEB-INF/classes/META-INF/ 目錄中。

如果您使用的是 Eclipse,這個檔案會建立為 src/META-INF/jdoconfig.xml。這個檔案會在您建置專案時自動複製到 war/WEB-INF/classes/META-INF/

如果您使用的不是 Eclipse,您可以直接建立 war/WEB-INF/classes/META-INF/ 目錄,或者由您的建置程序建立它,再從另一個位置複製設定檔。「使用 Apache Ant」所描述的 Ant 建置指令碼會從 src/META-INF/ 複製這個檔案。

jdoconfig.xml 檔案應該具備下列內容:

<?xml version="1.0" encoding="utf-8"?>
<jdoconfig xmlns="http://java.sun.com/xml/ns/jdo/jdoconfig"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="http://java.sun.com/xml/ns/jdo/jdoconfig">

    <persistence-manager-factory name="transactions-optional">
        <property name="javax.jdo.PersistenceManagerFactoryClass"
            value="org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManagerFactory"/>
        <property name="javax.jdo.option.ConnectionURL" value="appengine"/>
        <property name="javax.jdo.option.NontransactionalRead" value="true"/>
        <property name="javax.jdo.option.NontransactionalWrite" value="true"/>
        <property name="javax.jdo.option.RetainValues" value="true"/>
        <property name="datanucleus.appengine.autoCreateDatastoreTxns" value="true"/>
    </persistence-manager-factory>
</jdoconfig>

JDO 類別強化

建立 JDO 類別時,您可以使用 Java 註解來描述在資料存放區中儲存實例的方式,以及從資料存放區擷取時重新建立實例的方式。Access Platform 會使用編譯後的處理步驟 (DataNucleus 稱為「強化」類別),將類別連結至實作。

如果您使用的是 Eclipse 和「Google 外掛程式」,外掛程式會在建置程序中自動執行 JDO 類別強化。

如果您使用的是「使用 Apache Ant」所描述的 Ant 建置指令碼,這個建置指令碼包括必要的強化步驟。

如需 JDO 類別強化的詳細資訊,請參閱「使用 JDO」。

POJO 和 JDO 註解

JDO 可讓您透過任何 JDO 相容的介面卡 (例如 DataNucleus Access Platform),在資料存放區中儲存 Java 物件 (有時稱為 Plain Old Java Object 或 POJO)。「應用服務引擎 SDK」包含適用於「應用服務引擎」資料存放區的 Access Platform 外掛程式。這表示您可以儲存您在「應用服務引擎」資料存放區所定義的類別之實例,並使用 JDO API 以物件形式擷取這些實例。 您可以透過 Java 註解,指示 JDO 如何儲存與重新建構類別的實例。

讓我們建立 Greeting 類別來代表張貼在訪客留言板上的個別訊息。

guestbook 套件中建立 Greeting 新類別。(非 Eclipse 使用者,請在 src/guestbook/ 中建立 Greeting.java 檔案)。請為來源檔案設定下列內容:

package guestbook;

import java.util.Date;
import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.IdentityType;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;
import com.google.appengine.api.users.User;

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Greeting {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Long id;

    @Persistent
    private User author;

    @Persistent
    private String content;

    @Persistent
    private Date date;

    public Greeting(User author, String content, Date date) {
        this.author = author;
        this.content = content;
        this.date = date;
    }

    public Long getId() {
        return id;
    }

    public User getAuthor() {
        return author;
    }

    public String getContent() {
        return content;
    }

    public Date getDate() {
        return date;
    }

    public void setAuthor(User author) {
        this.author = author;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public void setDate(Date date) {
        this.date = date;
    }
}

這個簡單類別定義了 3 個訊息的屬性:authorcontentdate。這三個私有欄位會透過 @Persistent 註解,以告知 DataNucleus 在「應用服務引擎」資料存放區中將它們儲存為物件的屬性。

這個類別定義了屬性的 getter 和 setter,但是僅供應用程式使用,JDO 並不需要。

這個類別還定義了名為 id 的欄位,這是一個包含 @Persistent@PrimaryKey 兩個註解的 Long。 「應用服務引擎」資料存放區具備實體金鑰概念,而且可以在物件上以不同方式代表金鑰。在這個案例中,金鑰欄位為長整數,而且會在儲存物件時自動設為唯一的數字 ID。

如需 JDO 註解的詳細資訊,請參閱「定義資料類別」。

PersistenceManagerFactory

每個使用資料存放區的要求都會建立新的 PersistenceManager 類別實例。 這個步驟是透過 PersistenceManagerFactory 類別的實例完成。

PersistenceManagerFactory 實例需要一些時間初始化,幸好您的應用程式只需要一個實例。這個實例可以儲存在靜態變數中,供多個要求和多個類別使用。一個簡單的執行方式是,透過靜態實例建立單一包裝函式類別。

guestbook 套件 (位於 src/guestbook/ 目錄的 PMF.java 的檔案) 中,建立 PMF 新類別,然後為它設定下列內容:

package guestbook;

import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManagerFactory;

public final class PMF {
    private static final PersistenceManagerFactory pmfInstance =
        JDOHelper.getPersistenceManagerFactory("transactions-optional");

    private PMF() {}

    public static PersistenceManagerFactory get() {
        return pmfInstance;
    }
}

建立與儲存物件

有了 DataNucleus 和 Greeting 類別,表單處理邏輯即可在資料存放區中儲存新的訊息。

按照所述指示,根據下列內容編輯 src/guestbook/SignGuestbookServlet.java

package guestbook;

import java.io.IOException;
import java.util.Date;
import java.util.logging.Logger;
import javax.jdo.PersistenceManager;
import javax.servlet.http.*;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;

import guestbook.Greeting;
import guestbook.PMF;

public class SignGuestbookServlet extends HttpServlet {
    private static final Logger log = Logger.getLogger(SignGuestbookServlet.class.getName());

    public void doPost(HttpServletRequest req, HttpServletResponse resp)
                throws IOException {
        UserService userService = UserServiceFactory.getUserService();
        User user = userService.getCurrentUser();

        String content = req.getParameter("content");
        Date date = new Date();
        Greeting greeting = new Greeting(user, content, date);

        PersistenceManager pm = PMF.get().getPersistenceManager();
        try {
            pm.makePersistent(greeting);
        } finally {
            pm.close();
        }

        resp.sendRedirect("/intl/zh-TW/guestbook.jsp");
    }
}

這個程式碼會呼叫建構函式,藉此建立新的 Greeting 實例。如要將實例儲存至資料存放區,它會使用 PersistenceManagerFactory 建立 PersistenceManager,然後將實例傳送至 PersistenceManagermakePersistent() 方法。註解和位元組碼強化會從這裡接手。makePersistent() 傳回之後,新物件將儲存到資料存放區。

使用 JDOQL 查詢

JDO 標準定義了持續性物件的查詢機制,稱為 JDOQL。您可以使用 JDOQL 查詢「應用服務引擎」資料存放區中的實體,再以 JDO 強化物件形式擷取結果。

在這個範例中,我們會將查詢程式碼直接寫入 guestbook.jsp,讓事情簡單些。對於較大型的應用程式,建議您將查詢邏輯委派給另一個類別。

依據下列內容,編輯 war/guestbook.jsp 並添加粗體標示的程式碼:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.util.List" %>
<%@ page import="javax.jdo.PersistenceManager" %>
<%@ page import="com.google.appengine.api.users.User" %>
<%@ page import="com.google.appengine.api.users.UserService" %>
<%@ page import="com.google.appengine.api.users.UserServiceFactory" %>
<%@ page import="guestbook.Greeting" %>
<%@ page import="guestbook.PMF" %>

<html>
  <body>

<%
    UserService userService = UserServiceFactory.getUserService();
    User user = userService.getCurrentUser();
    if (user != null) {
%>
<p>Hello, <%= user.getNickname() %>! (You can
<a href="<%= userService.createLogoutURL(request.getRequestURI()) %>">sign out</a>.)</p>
<%
    } else {
%>
<p>Hello!
<a href="<%= userService.createLoginURL(request.getRequestURI()) %>">Sign in</a>
to include your name with greetings you post.</p>
<%
    }
%>

<%
    PersistenceManager pm = PMF.get().getPersistenceManager();
    String query = "select from " + Greeting.class.getName();
    List<Greeting> greetings = (List<Greeting>) pm.newQuery(query).execute();
    if (greetings.isEmpty()) {
%>
<p>The guestbook has no messages.</p>
<%
    } else {
        for (Greeting g : greetings) {
            if (g.getAuthor() == null) {
%>
<p>An anonymous person wrote:</p>
<%
            } else {
%>
<p><b><%= g.getAuthor().getNickname() %></b> wrote:</p>
<%
            }
%>
<blockquote><%= g.getContent() %></blockquote>
<%
        }
    }
    pm.close();
%>

    <form action="/sign" method="post">
      <div><textarea name="content" rows="3" cols="60"></textarea></div>
      <div><input type="submit" value="Post Greeting" /></div>
    </form>

  </body>
</html>

如要準備查詢,請呼叫 PersistenceManager 實例的 newQuery() 方法,並採用查詢文字做為字串。這個方法會傳回查詢物件。查詢物件的 execute() 方法會執行查詢,然後傳回適當類型的結果物件的 List<>。查詢字串必須包含要查詢的類別完整名稱,包括套件名稱。

重新建置專案,並重新啟動伺服器。瀏覽 http://localhost:8080/。輸入一則訊息並提交。訊息會出現在表單上。輸入另一則訊息並提交。兩則訊息會同時顯示。嘗試使用這些連結登出與登入,並嘗試在登入與未登入的狀況下提交訊息。

提示:在真實世界的應用程式中,建議您在顯示使用者所提交的內容 (例如此應用程式中的訊息) 時,逸出 HTML 字元。JavaServer Pages Standard Tag Library (JSTL) 包含可逸出 HTML 字元的常式。「應用服務引擎」內含 JSTL (以及其他 JSP 相關的執行階段 JAR),因此您不需要在應用程式中納入它們。如要在 http://java.sun.com/jsp/jstl/functions 標記程式庫尋找 escapeXml 函式,請參閱「Sun J2EE 1.4 教學課程」的詳細資訊。

JDOQL 簡介

我們的訪客留言板目前會顯示所有張貼到系統的訊息。它也可以按照建立順序顯示訊息。當訪客留言板擁有眾多訊息時,為求實用性,可以只顯示近期訊息並將最近的訊息置頂。我們可以透過調整資料存放區的查詢,完成這個步驟。

您可以使用 JDOQL (一種可擷取資料物件的類 SQL 查詢語言),透過 JDO 介面執行查詢。在我們的 JSP 頁面中,下列程式碼會定義 JDOQL 查詢字串:

    String query = "select from " + Greeting.class.getName();

換言之,JDOQL 查詢字串如下:

select from guestbook.Greeting

這個查詢會向資料存放區要求目前已儲存的 Greeting 類別的所有實例。

查詢可以納入條件,並限制結果物件的屬性必須符合這些條件,藉此篩選結果。例如,如果只想擷取 author 屬性為 alfred@example.comGreeting 物件,您可以使用下列查詢:

select from guestbook.Greeting where author == 'alfred@example.com'

查詢可以依據屬性值,指定結果的傳回順序。如要使用張貼順序的相反順序 (從最新到最舊) 擷取所有 Greeting 物件,您可以使用下列查詢:

select from guestbook.Greeting order by date desc

查詢可以限制只傳回某個範圍內的結果。如果只要擷取最近的 5 則訊息,您可以搭配使用 order byrange,如下所示:

select from guestbook.Greeting order by date desc range 0,5

讓我們為訪客留言板應用程式執行這個步驟。在 guestbook.jsp 中,使用下列內容取代 query 定義:

    String query = "select from " + Greeting.class.getName() + " order by date desc range 0,5";

在訪客留言板張貼超過 5 則的訊息。只有最近的 5 則訊息會以逆時間順序 (最新的訊息在最前面) 顯示。

如需查詢和 JDOQL 的詳細資訊,請參閱「查詢和索引」。

接下來...

每個網路應用程式都會透過範本或其他機制,從應用程式碼動態傳回產生的 HTML。大部分網路應用程式也需要服務靜態內容,例如影像、CSS 樣式表或 JavaScript 檔案。為了效率起見,「應用服務引擎」會將應用程式原始碼和資料檔案以不同方式處理。讓我們為這個應用程式建立一個 CSS 樣式表 (靜態檔案)。

繼續瀏覽「使用靜態檔案」一節。