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
- Jangan Buat Asumsi Tentang Urutan Eksekusi Callback
- Satu Callback Per Metode
- Jangan Lupa Mengambil Hasil Asinkron
- Hindari Pengulangan Terus-menerus
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) { Futureresult = 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.