Datastore 回呼

附註:如果其他某個應用程式使用 Remote API 呼叫 App Engine 應用程式,就「不會」觸發回呼。

回呼可讓您在持續性程序中的不同點執行程式碼。您可以使用這些回呼輕鬆實作記錄、取樣、修飾、稽核及驗證 (等) 跨功能的邏輯。Datastore API 目前支援在 put()delete() 作業前後執行的回呼。

PrePut

註冊特定種類的 PrePut 回呼時,系統會在放置該種類的任何實體之前叫用回呼;或者,如果未提供任何種類,會在放置任何實體之前就叫用回呼。

import com.google.appengine.api.datastore.PrePut;
import com.google.appengine.api.datastore.PutContext;
import java.util.Date;
import java.util.logging.Logger;

class PrePutCallbacks {
    static Logger logger = Logger.getLogger("callbacks");

    @PrePut(kinds = {"Customer", "Order"})
    void log(PutContext context) {
        logger.fine("Putting " + context.getCurrentElement().getKey());
    }

    @PrePut // Applies to all kinds
    void updateTimestamp(PutContext context) {
        context.getCurrentElement().setProperty("last_updated", new Date());
    }
}

使用 PrePut 註解的方法必須是一種執行個體方法,可傳回 void、接受 PutContext 做為其僅有的引數、不會擲回任何受檢例外狀況,且屬於具有無引數建構函式的類別。PrePut 回呼擲回例外狀況時,系統不會再繼續執行回呼,且在將任何遠端程序呼叫 (RPC) 核發給資料儲存庫之前,put() 作業會擲回例外狀況。

PostPut

註冊特定種類的 PostPut 回呼時,系統會在放置該種類的任何實體之後叫用回呼;或者,如果未提供任何種類,會在放置任何實體之後叫用回呼。

import com.google.appengine.api.datastore.PostPut;
import com.google.appengine.api.datastore.PutContext;
import java.util.logging.Logger;

class PostPutCallbacks {
    static Logger logger = Logger.getLogger("logger");

    @PostPut(kinds = {"Customer", "Order"}) // Only applies to Customers and Orders
    void log(PutContext context) {
        logger.fine("Finished putting " + context.getCurrentElement().getKey());
    }

    @PostPut // Applies to all kinds
    void collectSample(PutContext context) {
        Sampler.getSampler().collectSample(
            "put", context.getCurrentElement().getKey());
    }
}

使用 PostPut 註解的方法必須是一種執行個體方法,可傳回 void、接受 PutContext 做為其僅有的引數、不會擲回任何受檢例外狀況,且屬於具有無引數建構函式的類別。PostPut 回呼擲回例外狀況時,系統不會再繼續執行回呼,但 put() 作業的結果不會受到影響。請注意,如果 put() 作業本身失敗,系統根本不會叫用 PostPut 回呼。此外,與交易 put() 作業相關聯的 PostPut 回呼不會在交易成功修訂之前執行。

PreDelete

註冊特定種類的 PreDelete 回呼時,系統會在刪除該種類的任何實體之前叫用回呼;或者,如果未提供任何種類,會在刪除任何實體之前就叫用回呼。

import com.google.appengine.api.datastore.DeleteContext;
import com.google.appengine.api.datastore.PreDelete;
import java.util.logging.Logger;

class PreDeleteCallbacks {
    static Logger logger = Logger.getLogger("logger");

    @PreDelete(kinds = {"Customer", "Order"})
    void checkAccess(DeleteContext context) {
        if (!Auth.canDelete(context.getCurrentElement()) {
            throw new SecurityException();
        }
    }

    @PreDelete // Applies to all kinds
    void log(DeleteContext context) {
        logger.fine("Deleting " + context.getCurrentElement().getKey());
    }
}

使用 PreDelete 註解的方法必須是一種執行個體方法,可傳回 void、接受 DeleteContext 做為其僅有的引數、不會擲回任何受檢例外狀況,且屬於具有無引數建構函式的類別。PreDelete 回呼擲回例外狀況時,系統不會再繼續執行回呼,且 delete() 作業會在將任何遠端程序呼叫 (RPC) 核發給資料儲存庫之前擲回例外狀況。

PostDelete

註冊特定種類的 PostDelete 回呼時,系統會在刪除該種類的任何實體之後叫用回呼;或者,如果未提供任何種類,會在刪除任何實體之後叫用回呼。

import com.google.appengine.api.datastore.DeleteContext;
import com.google.appengine.api.datastore.PostDelete;
import java.util.logging.Logger;

class PostDeleteCallbacks {
    static Logger logger = Logger.getLogger("logger");

    @PostDelete(kinds = {"Customer", "Order"})
    void log(DeleteContext context) {
        logger.fine(
            "Finished deleting " + context.getCurrentElement().getKey());
    }

    @PostDelete // Applies to all kinds
    void collectSample(DeleteContext context) {
        Sampler.getSampler().collectSample(
            "delete", context.getCurrentElement().getKey());
    }
}

使用 PostDelete 註解的方法必須是一種執行個體方法,可傳回 void、接受 DeleteContext 做為其僅有的引數、不會擲回任何受檢例外狀況,且屬於具有無引數建構函式的類別。PostDelete 回呼擲回例外狀況時,系統不會再繼續執行回呼,但 delete() 作業的結果不會受到影響。請注意,如果 delete() 作業本身失敗,系統根本不會叫用 PostDelete 回呼。此外,與交易 delete() 作業相關聯的 PostDelete 回呼不會在交易成功修訂之前執行。

PreGet

您可以將 PreGet 回呼註冊為在取得某些特定種類 (或全部種類) 的實體之前呼叫。例如,您可能會使用這個回呼攔截某些 get 要求,並從快取而不是資料儲存庫擷取資料。

import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.PreGetContext;
import com.google.appengine.api.datastore.PreGet;

class PreGetCallbacks {
  @PreGet(kinds = {"Customer", "Order"})
  public void preGet(PreGetContext context) {
    Entity e = MyCache.get(context.getCurrentElement());
    if (e != null) {
      context.setResultForCurrentElement(e);
    }
    // if not in cache, don't set result; let the normal datastore-fetch happen
  }
}

PreQuery

您可以將 PreQuery 回呼註冊為在針對特定種類 (或全部種類) 執行查詢之前呼叫。例如,您可能會使用這個回呼,根據登入的使用者新增等式篩選器。系統會將回呼傳送給查詢;修改之後,回呼可能會導致執行其他查詢。或者,回呼可能會擲回非受檢例外狀況,以防止執行查詢。

import com.google.appengine.api.datastore.PreQueryContext;
import com.google.appengine.api.datastore.PreQuery;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;

class PreQueryCallbacks {
  @PreQuery(kinds = {"Customer"})
  // Queries should only return data owned by the logged in user.
  public void preQuery(PreQueryContext context) {
    UserService users = UserServiceFactory.getUserService();
    context.getCurrentElement().setFilter(
        new FilterPredicate("owner", Query.FilterOperator.EQUAL, users.getCurrentUser()));
    }
}

PostLoad

您可以將 PostLoad 回呼註冊為在載入特定種類 (或全部種類) 的實體之後呼叫。此處的「載入」可能表示取得或查詢作業的結果,這對於修飾載入的實體而言非常實用。

import com.google.appengine.api.datastore.PostLoadContext;
import com.google.appengine.api.datastore.PostLoad;

class PostLoadCallbacks {
   @PostLoad(kinds = {"Order"})
   public void postLoad(PostLoadContext context) {
     context.getCurrentElement().setProperty("read_timestamp",
                                             System.currentTimeMillis());
   }
}

批次作業

執行批次作業 (例如具有多個實體的 put()) 時,系統會對每個實體叫用一次回呼。您可以對回呼方法的引數呼叫 CallbackContext.getElements(),以存取整個批次的物件。這可讓您對整個批次執行回呼,而不是每次僅回呼一個元素。

import com.google.appengine.api.datastore.PrePut;
import com.google.appengine.api.datastore.PutContext;

class Validation {
    @PrePut(kinds = "TicketOrder")
    void checkBatchSize(PutContext context) {
        if (context.getElements().size() > 5) {
            throw new IllegalArgumentException(
                "Cannot purchase more than 5 tickets at once.");
        }
    }
}

如果您需要每個批次僅執行一次回呼,請使用 CallbackContext.getCurrentIndex() 判定您查看的是否為批次的第一個元素。

import com.google.appengine.api.datastore.PrePut;
import com.google.appengine.api.datastore.PutContext;
import java.util.logging.Logger;

class Validation {
    static Logger logger = Logger.getLogger("logger");

    @PrePut
    void log(PutContext context) {
        if (context.getCurrentIndex() == 0) {
            logger.fine("Putting batch of size " + getElements().size());
        }
    }
}

非同步作業

關於回呼如何與非同步資料儲存庫作業互動的問題,有一些重要注意事項。使用 async datastore API 執行 put()delete() 時,您註冊的任何 Pre* 回呼都會同步執行。您的 Post* 回呼也將同步執行,但會在您呼叫任何 Future.get() 方法以擷取作業結果之後執行。

將回呼與 App Engine 服務搭配使用

回呼與應用程式中的其他任何程式碼一樣,可以存取完整範圍的 App Engine 後端服務,您也可以在單一類別中定義任意數量的回呼。

import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.DeleteContext;
import com.google.appengine.api.datastore.PrePut;
import com.google.appengine.api.datastore.PostPut;
import com.google.appengine.api.datastore.PostDelete;
import com.google.appengine.api.datastore.PutContext;
import com.google.appengine.api.memcache.MemcacheService;
import com.google.appengine.api.memcache.MemcacheServiceFactory;
import com.google.appengine.api.taskqueue.Queue;
import com.google.appengine.api.taskqueue.QueueFactory;
import com.google.appengine.api.urlfetch.URLFetchService;
import com.google.appengine.api.urlfetch.URLFetchServiceFactory;

class ManyCallbacks {

    @PrePut(kinds = {"kind1", "kind2"})
    void foo(PutContext context) {
      MemcacheService ms = MemcacheServiceFactory.getMemcacheService();
      // ...
    }

    @PrePut
    void bar(PutContext context) {
      DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
      // ...
    }

    @PostPut(kinds = {"kind1", "kind2"})
    void baz(PutContext context) {
        Queue q = QueueFactory.getDefaultQueue();
        // ...
    }

    @PostDelete(kinds = {"kind2"})
    void yam(DeleteContext context) {
        URLFetchService us = URLFetchServiceFactory.getURLFetchService();
        // ...
    }
}

應避免的常見錯誤

實作回呼時,有一些要注意的常見錯誤。

請勿維持非靜態狀態

import java.util.logging.Logger;
import com.google.appengine.api.datastore.PrePut;

class MaintainsNonStaticState {
    static Logger logger = Logger.getLogger("logger");
    // ERROR!
    // should be static to avoid assumptions about lifecycle of the instance
    boolean alreadyLogged = false;

    @PrePut
    void log(PutContext context) {
        if (!alreadyLogged) {
            alreadyLogged = true;
            logger.fine(
                "Finished deleting " + context.getCurrentElement().getKey());
        }
    }
}

請勿假設回呼執行順序

Pre* 回呼一律在 Post* 回呼之前執行,但假設某個 Pre* 回呼以相對於其他 Pre* 回呼的順序執行並不安全,假設某個 Post* 回呼以相對於其他 Post* 回呼的順序執行也不安全。

import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.PrePut;

import java.util.HashSet;
import java.util.Set;

class MakesAssumptionsAboutOrderOfCallbackExecution {
    static Set<Key> paymentKeys = new HashSet<Key>();

    @PrePut(kinds = "Order")
    void prePutOrder(PutContext context) {
        Entity order = context.getCurrentElement();
        paymentKeys.addAll((Collection<Key>) order.getProperty("payment_keys"));
    }

    @PrePut(kinds = "Payment")
    void prePutPayment(PutContext context) {
        // ERROR!
        // Not safe to assume prePutOrder() has already run!
        if (!paymentKeys.contains(context.getCurrentElement().getKey()) {
            throw new IllegalArgumentException("Unattached payment!");
        }
    }
}

每種方法一個回呼

即使類別可以擁有不限數量的回呼方法,一種方法也只能與一個回呼相關聯。

import com.google.appengine.api.datastore.PrePut;
import com.google.appengine.api.datastore.PostPut;

class MultipleCallbacksOnAMethod {
    @PrePut
    @PostPut // Compiler error!
    void foo(PutContext context) { }
}

請勿忘記擷取非同步結果

Post* 回呼會在您呼叫 Future.get() 以擷取作業結果之後執行。如果您在完成提供 HTTP 要求之前忘記呼叫 Future.get(),您的 Post* 回呼將不會執行。

import com.google.appengine.api.datastore.AsyncDatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.PostPut;
import com.google.appengine.api.datastore.PutContext;

import java.util.concurrent.Future;

class IgnoresAsyncResult {
    AsyncDatastoreService ds = DatastoreServiceFactory.getAsyncDatastoreService();

    @PostPut
    void collectSample(PutContext context) {
        Sampler.getSampler().collectSample(
            "put", context.getCurrentElement().getKey());
    }

    void addOrder(Entity order) {
        Future result = ds.put(order);
        // ERROR! Never calls result.get() so collectSample() will not run.
    }
}

避免無限迴圈

如果您在回呼中執行資料儲存庫作業,請小心不要進入無限迴圈。

import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.PrePut;
import com.google.appengine.api.datastore.PutContext;

class InfiniteLoop {
    DatastoreService ds = DatastoreServiceFactory.getDatastoreService();

    @PrePut
    void audit(PutContext context) {
        Entity original = ds.get(context.getCurrentElement().getKey());
        Entity auditEntity = new Entity(original.getKind() + "_audit");
        auditEntity.setPropertiesFrom(original);
        // INFINITE LOOP!
        // Restricting the callback to specific kinds would solve this.
        ds.put(auditEntity);
    }
}

將回呼與 Eclipse 搭配使用

如果您使用 Eclipse 開發應用程式,需要執行少量設定步驟以使用資料儲存庫回呼。以下說明適用於 Eclipse 3.7 的步驟。未來在 Eclipse 適用的 Google 外掛程式,我們預期不再需要執行這些步驟。

  • 開啟「專案」的「屬性」對話方塊 (「專案」>「屬性」)
  • 開啟「註解處理」對話方塊 (「Java 編譯器」>「註解處理」)
  • 勾選 [啟用註解處理]
  • 勾選 [在編輯器中啟用處理]
  • 開啟「原廠路徑」對話方塊 (「Java 編譯器」>「註解處理」>「原廠路徑」)
  • 按一下 [新增外部 JAR]
  • 選取 <SDK_ROOT>/lib/impl/appengine-api.jar (其中 SDK_ROOT 是您 sdk 安裝的頂層目錄)
  • 按一下 [確定]

您可以採用多個回呼實作一種方法,驗證是否正確設定回呼 (請參閱每種方法一個回呼中的程式碼片段)。這樣應該會產生一個編譯器錯誤。

本頁內容對您是否有任何幫助?請提供意見:

傳送您對下列選項的寶貴意見...

這個網頁
Java 適用的 App Engine 標準環境