数据存储回调

注意:如果其他应用通过 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 作为其唯一参数,不引发任何已检查的异常,并且属于具有 no-arg 构造函数的类。当 PrePut 回调引发异常时,不会再进一步回调,并且 put() 操作会在向数据存储区提交任何 RPC 之前引发异常。

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 作为其唯一参数,不引发任何已检查的异常,并且属于具有 no-arg 构造函数的类。当 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 作为其唯一参数,不引发任何已检查的异常,并且属于具有 no-arg 构造函数的类。当 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 作为其唯一参数,不引发任何已检查的异常,并且属于具有 no-arg 构造函数的类。当 PostDelete 回调引发异常时,不会再进一步回调,但 delete() 操作的结果不受影响。请注意,如果 delete() 操作本身失败,则根本不会调用 PostDelete 回调。此外,在事务成功提交之前,与事务性 delete() 操作关联的 PostDelete 回调不会运行。

PreGet

您可以注册 PreGet 回调,该回调会在获取特定种类(或所有种类)的实体之前得到调用。例如,您可以使用此方法,拦截一些获取请求,并从缓存而不是数据存储区中提取数据。

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) { }
}

不要忘记检索异步结果

除非您调用 Future.get() 检索操作结果,否则 Post* 回调不会运行。如果您在处理完 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。我们希望在未来的 Google Plugin For Eclipse 版本中不再需要这些步骤。

  • 打开项目的“属性”对话框(“项目”>“属性”)
  • 打开“注释处理”对话框(“Java 编译器”>“注释处理”)
  • 选中“启用注释处理”
  • 选中“启用在编辑器中处理”
  • 打开“工厂路径”对话框(“Java 编译器”>“注释处理”>“工厂路径”)
  • 点击“添加外部 JAR”
  • 选择 <SDK_ROOT> /lib/impl/appengine-api.jar(其中,SDK_ROOT 是 sdk 安装程序的顶级目录)
  • 点击“确定”。

通过实现一个有多个回调的方法,您可以验证回调是否配置正确(请参阅每个方法对应一个回调下的代码段)。这应该会产生一个编译器错误。