Callback Datastore

Catatan: Callback tidak dipicu jika aplikasi App Engine dipanggil oleh beberapa aplikasi lain menggunakan Remote API.

Callback memungkinkan Anda mengeksekusi kode di berbagai titik dalam proses persistensi. Anda dapat menggunakan callback ini untuk menerapkan logika lintas fungsi dengan mudah seperti logging, pengambilan sampel, dekorasi, pengauditan, dan validasi (dan masih banyak lagi). Datastore API saat ini mendukung callback yang dijalankan sebelum dan setelah operasi put() dan delete().

PrePut

Saat Anda mendaftarkan callback PrePut dengan jenis tertentu, callback akan dipanggil sebelum entitas apa pun dari jenis tersebut di PUT (atau, jika tidak ada jenis yang diberikan, sebelum entitas apa pun di PUT):

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());
    }
}

Metode yang dianotasi dengan PrePut harus berupa metode instance yang menampilkan void, menerima PutContext sebagai satu-satunya argumen, tidak menampilkan pengecualian yang dicentang, dan termasuk dalam class dengan konstruktor no-arg. Saat callback PrePut menampilkan pengecualian, tidak ada callback yang dijalankan lebih lanjut, dan operasi put() akan menampilkan pengecualian sebelum RPC dikeluarkan ke datastore.

PostPut

Saat Anda mendaftarkan callback PostPut dengan jenis tertentu, callback akan dipanggil setelah entitas apa pun dari jenis tersebut menjalankan Put (atau, jika tidak ada jenis yang diberikan, setelah entitas apa pun menjalankan Put).

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());
    }
}

Metode yang dianotasi dengan PostPut harus berupa metode instance yang menampilkan void, menerima PutContext sebagai satu-satunya argumen, tidak menampilkan pengecualian yang dicentang, dan termasuk dalam class dengan konstruktor no-arg. Saat callback PostPut menampilkan pengecualian, tidak ada callback lebih lanjut yang dieksekusi tetapi hasil operasi put() tidak akan terpengaruh. Perhatikan bahwa jika operasi put() itu sendiri gagal, callback PostPut tidak akan dipanggil sama sekali. Selain itu, callback PostPut yang terkait dengan operasi put() transaksional tidak akan berjalan hingga transaksi berhasil di-commit.

PreDelete

Saat Anda mendaftarkan callback PreDelete dengan jenis tertentu, callback akan dipanggil sebelum entitas apa pun dari jenis tersebut dihapus (atau, jika tidak ada jenis yang diberikan, sebelum entitas apa pun dihapus):

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());
    }
}

Metode yang dianotasi dengan PreDelete harus berupa metode instance yang menampilkan void, menerima DeleteContext sebagai satu-satunya argumen, tidak menampilkan pengecualian yang dicentang, dan termasuk dalam class dengan konstruktor no-arg. Saat callback PreDelete menampilkan pengecualian, tidak ada callback yang dijalankan lagi dan operasi delete() akan menampilkan pengecualian sebelum RPC dikeluarkan ke datastore.

PostDelete

Saat Anda mendaftarkan callback PostDelete dengan jenis tertentu, callback akan dipanggil setelah entitas apa pun dari jenis tersebut dihapus (atau, jika tidak ada jenis yang diberikan, setelah entitas apa pun dihapus):

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());
    }
}

Metode yang dianotasi dengan PostDelete harus berupa metode instance yang menampilkan void, menerima DeleteContext sebagai satu-satunya argumen, tidak menampilkan pengecualian yang dicentang, dan termasuk dalam class dengan konstruktor no-arg. Saat callback PostDelete menampilkan pengecualian, tidak ada callback lebih lanjut yang dieksekusi tetapi hasil operasi delete() tidak akan terpengaruh. Perhatikan bahwa jika operasi delete() itu sendiri gagal, callback PostDelete tidak akan dipanggil sama sekali. Selain itu, callback PostDelete yang terkait dengan operasi delete() transaksional tidak akan berjalan hingga transaksi berhasil di-commit.

PreGet

Anda dapat mendaftarkan callback PreGet untuk dipanggil sebelum mendapatkan entity dari beberapa jenis tertentu (atau semua jenis). Anda dapat menggunakannya, misalnya, untuk memintas beberapa permintaan get dan mengambil data dari cache, bukan dari datastore.

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

Anda dapat mendaftarkan callback PreQuery yang akan dipanggil sebelum menjalankan kueri untuk jenis tertentu (atau semua jenis). Anda dapat menggunakannya, misalnya, untuk menambahkan filter kesetaraan berdasarkan pengguna yang login. Callback meneruskan kueri; dengan memodifikasinya, callback tersebut dapat menyebabkan kueri yang berbeda dijalankan sebagai gantinya. Atau, callback dapat menampilkan pengecualian yang tidak dicentang untuk mencegah kueri dieksekusi.

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

Anda dapat mendaftarkan callback PostLoad agar dipanggil setelah memuat entity dari jenis tertentu (atau semua jenis). Di sini, "memuat" dapat berarti hasil get atau kueri. Hal ini berguna untuk mendekorasi entity yang dimuat.

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());
   }
}

Operasi Batch

Saat Anda menjalankan operasi batch (misalnya, put() dengan beberapa entity), callback Anda akan dipanggil satu kali per entity. Anda dapat mengakses seluruh batch objek dengan memanggil CallbackContext.getElements() pada argumen ke metode callback Anda. Hal ini memungkinkan Anda untuk menerapkan callback yang beroperasi di seluruh batch, bukan di satu elemen pada satu waktu.

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.");
        }
    }
}

Jika callback Anda hanya perlu dijalankan satu kali per batch, gunakan CallbackContext.getCurrentIndex() untuk menentukan apakah Anda melihat elemen pertama dari batch tersebut.

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());
        }
    }
}

Operasi Asinkron

Ada beberapa hal penting yang perlu diketahui tentang cara callback berinteraksi dengan operasi datastore asinkron. Saat Anda mengeksekusi put() atau delete() menggunakan API datastore asinkron, callback Pre* apa pun yang telah didaftarkan akan dijalankan secara sinkron. Callback Post* Anda juga akan dijalankan secara sinkron, namun Anda harus memanggil metode Future.get() mana pun terlebih dahulu untuk mendapatkan hasil operasi.

Menggunakan Callback dengan Layanan App Engine

Callback, seperti kode lain dalam aplikasi Anda, memiliki akses ke berbagai layanan backend App Engine, dan Anda dapat menentukan sebanyak yang Anda inginkan dalam satu class.

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();
        // ...
    }
}

Kesalahan Umum yang Perlu Dihindari

Ada sejumlah kekeliruan umum yang harus diketahui saat menerapkan callback.

Jangan Pertahankan Status Non-statis

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());
        }
    }
}

Jangan Buat Asumsi Tentang Urutan Eksekusi Callback

Callback Pre* akan selalu dijalankan sebelum callback Post*, tetapi hindari membuat asumsi apa pun tentang urutan callback Pre*yang dijalankan relatif terhadap callback Pre* lainnya, juga jangan membuat asumsi tentang urutan eksekusi callback Post* relatif terhadap callback Post* lainnya.

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!");
        }
    }
}

Satu Callback Per Metode

Meskipun class dapat memiliki jumlah metode callback yang tidak terbatas, satu metode hanya dapat dikaitkan dengan satu callback tunggal.

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

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

Jangan Lupa Mengambil Hasil Asinkron

Callback Post* tidak berjalan sampai Anda memanggil Future.get() untuk mengambil hasil operasi. Jika Anda lupa memanggil Future.get() sebelum selesai melayani permintaan HTTP, callback Post* tidak akan berjalan.

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.
    }
}

Hindari Pengulangan Terus-menerus

Jika Anda menjalankan operasi datastore di callback, berhati-hatilah agar tidak terjebak dalam infinite loop.

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

Menggunakan Callback dengan Eclipse

Jika Anda mengembangkan aplikasi dengan Eclipse, Anda perlu melakukan beberapa langkah konfigurasi untuk menggunakan callback datastore. Langkah-langkah ini adalah untuk Eclipse 3.7. Kami memperkirakan langkah ini tidak diperlukan dalam rilis Google Plugin For Eclipse mendatang.

  • Membuka dialog Properties untuk Project Anda (Project > Properties)
  • Membuka dialog Annotation Processing (Java Compiler > Annotation Processing)
  • Centang "Aktifkan pemrosesan anotasi"
  • Centang "Aktifkan pemrosesan di editor"
  • Buka dialog Factory Path (Java Compiler > Annotation Processing > Factory Path)
  • Klik "Tambahkan JAR Eksternal"
  • Pilih <SDK_ROOT>/lib/impl/appengine-api.jar (dengan SDK_ROOT adalah direktori level teratas penginstalan SDK Anda)
  • Klik "Oke"

Anda dapat memverifikasi bahwa callback dikonfigurasi dengan benar dengan menerapkan metode dengan beberapa callback (lihat cuplikan kode pada Satu Callback Per Metode). Tindakan ini akan menghasilkan error compiler.