Mengimplementasikan Multitenancy Menggunakan Namespace

Namespaces API memungkinkan Anda mengaktifkan multitenancy dengan mudah di aplikasi, cukup dengan memilih string namespace untuk setiap tenant di web.xml menggunakan paket NamespaceManager.

Menetapkan namespace saat ini

Anda bisa mendapatkan, menetapkan, dan memvalidasi namespace menggunakan NamespaceManager. Pengelola namespace memungkinkan Anda menyetel namespace saat ini untuk API yang mendukung namespace. Anda sudah menetapkan namespace saat ini di awal menggunakan web.xml, dan datastore serta memcache akan otomatis menggunakan namespace tersebut.

Sebagian besar developer App Engine akan menggunakan domain Google Workspace (sebelumnya G Suite) mereka sebagai namespace saat ini. Dengan Google Workspace, Anda dapat men-deploy aplikasi ke domain apa pun yang Anda miliki, sehingga mekanisme ini dapat dengan mudah digunakan untuk mengonfigurasi namespace yang berbeda untuk domain yang berbeda. Kemudian, Anda dapat menggunakan namespace yang terpisah untuk memisahkan data di seluruh domain. Untuk informasi selengkapnya, lihat Memetakan Domain Kustom.

Contoh kode berikut menunjukkan cara menetapkan namespace saat ini ke domain Google Workspace yang digunakan untuk memetakan URL. Secara khusus, string ini akan sama untuk semua URL yang dipetakan melalui domain Google Workspace yang sama.

Anda dapat menyetel namespace di Java menggunakan antarmuka Filter servlet sebelum memanggil metode servlet. Contoh sederhana berikut menunjukkan cara menggunakan domain Google Workspace Anda sebagai namespace saat ini:

// Filter to set the Google Apps domain as the namespace.
public class NamespaceFilter implements Filter {

  @Override
  public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
      throws IOException, ServletException {
    // Make sure set() is only called if the current namespace is not already set.
    if (NamespaceManager.get() == null) {
      // If your app is hosted on appspot, this will be empty. Otherwise it will be the domain
      // the app is hosted on.
      NamespaceManager.set(NamespaceManager.getGoogleAppsNamespace());
    }
    chain.doFilter(req, res); // Pass request back down the filter chain
  }

Filter namespace harus dikonfigurasi di file web.xml. Perhatikan bahwa, jika ada beberapa entri filter, namespace pertama yang akan ditetapkan adalah namespace yang akan digunakan.

Contoh kode berikut menunjukkan cara mengonfigurasi filter namespace di web.xml:

<!-- Configure the namespace filter. -->
<filter>
    <filter-name>NamespaceFilter</filter-name>
    <filter-class>com.example.appengine.NamespaceFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>NamespaceFilter</filter-name>
    <url-pattern>/sign</url-pattern>
</filter-mapping>

Untuk informasi yang lebih umum tentang file web.xml dan memetakan jalur URL ke servlet, lihat Deskripsi Deployment: web.xml.

Anda juga dapat menetapkan namespace baru untuk operasi sementara, yang mereset namespace asli setelah operasi selesai, menggunakan pola try/finally yang ditunjukkan di bawah:

// Set the namepace temporarily to "abc"
String oldNamespace = NamespaceManager.get();
NamespaceManager.set("abc");
try {
  //      ... perform operation using current namespace ...
} finally {
  NamespaceManager.set(oldNamespace);
}

Jika Anda tidak menentukan nilai untuk namespace, namespace akan disetel ke string kosong. String namespace bersifat arbitrer, tetapi juga dibatasi maksimal 100 karakter alfanumerik, titik, garis bawah, dan tanda hubung. Secara lebih eksplisit, string namespace harus cocok dengan ekspresi reguler [0-9A-Za-z._-]{0,100}.

Berdasarkan konvensi, semua namespace yang diawali dengan "_" (garis bawah) dicadangkan untuk penggunaan sistem. Aturan namespace sistem ini tidak diterapkan, tetapi Anda dapat dengan mudah mengalami konsekuensi negatif yang tidak didefinisikan jika tidak mengikutinya.

Menghindari kebocoran data

Salah satu risiko yang umumnya terkait dengan aplikasi multitenant adalah bahaya kebocoran data ke seluruh namespace. Kebocoran data yang tidak disengaja dapat muncul dari banyak sumber, termasuk:

  • Menggunakan namespace dengan API App Engine yang belum mendukung namespace. Misalnya, Blobstore tidak mendukung namespace. Jika menggunakan Namespace dengan Blobstore, Anda harus menghindari penggunaan kueri Blobstore untuk permintaan pengguna akhir, atau kunci Blobstore dari sumber yang tidak dipercaya.
  • Menggunakan media penyimpanan eksternal (bukannya memcache dan datastore), melalui URL Fetch atau mekanisme lainnya, tanpa menyediakan skema pemisahan untuk namespace.
  • Menetapkan namespace berdasarkan domain email pengguna. Pada umumnya, Anda tidak ingin semua alamat email domain mengakses namespace. Menggunakan domain email juga mencegah aplikasi Anda menggunakan namespace hingga pengguna login.

Men-deploy namespace

Bagian berikut menjelaskan cara men-deploy namespace dengan alat dan API App Engine lainnya.

Membuat namespace per pengguna

Beberapa aplikasi perlu membuat namespace untuk masing-masing pengguna. Jika Anda ingin memisahkan data di tingkat pengguna untuk pengguna yang login, sebaiknya gunakan User.getUserId(), yang menampilkan ID permanen yang unik untuk pengguna tersebut. Contoh kode berikut menunjukkan cara menggunakan Users API untuk tujuan ini:

if (com.google.appengine.api.NamespaceManager.get() == null) {
  // Assuming there is a logged in user.
  namespace = UserServiceFactory.getUserService().getCurrentUser().getUserId();
  NamespaceManager.set(namespace);
}

Biasanya, aplikasi yang membuat namespace per pengguna juga menyediakan halaman landing tertentu kepada pengguna yang berbeda. Dalam hal ini, aplikasi harus menyediakan skema URL yang menentukan halaman landing yang akan ditampilkan kepada pengguna.

Menggunakan namespace dengan Datastore

Secara default, datastore menggunakan setelan namespace saat ini pada pengelola namespace untuk permintaan datastore. API menerapkan namespace saat ini ke objek Key atau Query saat dibuat. Oleh karena itu, Anda harus berhati-hati jika aplikasi menyimpan objek Key atau Query dalam bentuk serial, karena namespace dipertahankan dalam serialisasi tersebut.

Jika Anda menggunakan objek Key dan Query yang dideserialisasi, pastikan objek tersebut berperilaku sebagaimana mestinya. Sebagian besar aplikasi sederhana yang menggunakan datastore (put/query/get) tanpa menggunakan mekanisme penyimpanan lain akan berfungsi seperti yang diharapkan dengan menyetel namespace saat ini sebelum memanggil datastore API apa pun.

Objek Query dan Key menunjukkan perilaku unik berikut terkait namespace:

  • Objek Query dan Key mewarisi namespace saat ini ketika dibuat, kecuali jika Anda menetapkan namespace eksplisit.
  • Saat aplikasi membuat Key baru dari ancestor, Key baru akan mewarisi namespace ancestor.
  • Tidak ada API untuk Java guna menetapkan namespace Key atau Query secara eksplisit.
Contoh kode berikut menunjukkan pengendali permintaan SomeRequest untuk menambah jumlah untuk namespace saat ini dan namespace -global- yang diberi nama secara acak dalam entity datastore Counter singkat ini.

public class UpdateCountsServlet extends HttpServlet {

  private static final int NUM_RETRIES = 10;

  @Entity
  public class CounterPojo {

    @Id
    public Long id;
    @Index
    public String name;
    public Long count;

    public CounterPojo() {
      this.count = 0L;
    }

    public CounterPojo(String name) {
      this.name = name;
      this.count = 0L;
    }

    public void increment() {
      count++;
    }
  }

  /**
   * Increment the count in a Counter datastore entity.
   **/
  public long updateCount(String countName) {

    CounterPojo cp = ofy().load().type(CounterPojo.class).filter("name", countName).first().now();
    if (cp == null) {
      cp = new CounterPojo(countName);
    }
    cp.increment();
    ofy().save().entity(cp).now();

    return cp.count;
  }

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws java.io.IOException {

    // Update the count for the current namespace.
    updateCount("request");

    // Update the count for the "-global-" namespace.
    String namespace = NamespaceManager.get();
    try {
      // "-global-" is namespace reserved by the application.
      NamespaceManager.set("-global-");
      updateCount("request");
    } finally {
      NamespaceManager.set(namespace);
    }
    resp.setContentType("text/plain");
    resp.getWriter().println("Counts are now updated.");
  }

Menggunakan namespace dengan Memcache

Secara default, memcache menggunakan namespace saat ini dari pengelola namespace untuk permintaan memcache. Pada umumnya, Anda tidak perlu secara eksplisit menetapkan namespace di memcache, dan melakukannya dapat menimbulkan bug tidak terduga.

Namun, ada beberapa instance unik yang situasinya sesuai untuk menetapkan namespace secara eksplisit di memcache. Misalnya, aplikasi Anda mungkin memiliki data umum yang dibagikan di seluruh namespace (misalnya tabel yang berisi kode negara).

Cuplikan kode berikut menunjukkan cara menetapkan namespace secara eksplisit di memcache:

Secara default, memcache API untuk Java mengkueri pengelola namespace untuk namespace saat ini dari MemcacheService. Anda juga dapat menyatakan namespace secara eksplisit saat membuat memcache menggunakan getMemcacheService(Namespace). Untuk sebagian besar aplikasi, Anda tidak perlu menentukan namespace secara eksplisit.

Contoh kode berikut menunjukkan cara membuat memcache yang menggunakan namespace saat ini di pengelola namespace.

// Create a MemcacheService that uses the current namespace by
// calling NamespaceManager.get() for every access.
MemcacheService current = MemcacheServiceFactory.getMemcacheService();

// stores value in namespace "abc"
oldNamespace = NamespaceManager.get();
NamespaceManager.set("abc");
try {
  current.put("key", value); // stores value in namespace “abc”
} finally {
  NamespaceManager.set(oldNamespace);
}

Contoh kode ini secara eksplisit menentukan namespace saat membuat layanan memcache:

// Create a MemcacheService that uses the namespace "abc".
MemcacheService explicit = MemcacheServiceFactory.getMemcacheService("abc");
explicit.put("key", value); // stores value in namespace "abc"

Menggunakan namespace dengan Task Queue

Secara default, push queue menggunakan namespace saat ini seperti yang ditetapkan dalam pengelola namespace pada saat tugas dibuat. Pada umumnya, Anda tidak perlu menetapkan namespace secara eksplisit dalam task queue, dan hal itu dapat menyebabkan bug yang tidak terduga.

Nama tugas digunakan bersama di semua namespace. Anda tidak dapat membuat dua tugas dengan nama yang sama, meskipun keduanya menggunakan namespace berbeda. Jika ingin menggunakan nama tugas yang sama untuk banyak namespace, Anda cukup menambahkan setiap namespace ke nama tugas.

Saat tugas baru memanggil metode add() task queue, task queue akan menyalin namespace saat ini dan (jika ada) domain Google Workspace dari pengelola namespace. Saat tugas dijalankan, namespace saat ini dan namespace Google Workspace akan dipulihkan.

Jika namespace saat ini tidak ditetapkan dalam permintaan asal (dengan kata lain, jika get() menampilkan null), antrean tugas akan menetapkan namespace ke "" dalam tugas yang dijalankan.

Ada beberapa instance unik yang sesuai untuk menetapkan namespace secara eksplisit untuk tugas yang berfungsi di semua namespace. Misalnya, Anda dapat membuat tugas yang menggabungkan statistik penggunaan di seluruh namespace. Selanjutnya, Anda dapat menetapkan namespace tugas secara eksplisit. Contoh kode berikut menunjukkan cara menetapkan namespace secara eksplisit dengan antrean tugas.

Pertama, buat pengendali antrean tugas yang menambahkan jumlah dalam entity datastore Counter:

public class UpdateCountsServlet extends HttpServlet {

  private static final int NUM_RETRIES = 10;

  @Entity
  public class CounterPojo {

    @Id
    public Long id;
    @Index
    public String name;
    public Long count;

    public CounterPojo() {
      this.count = 0L;
    }

    public CounterPojo(String name) {
      this.name = name;
      this.count = 0L;
    }

    public void increment() {
      count++;
    }
  }

  /**
   * Increment the count in a Counter datastore entity.
   **/
  public long updateCount(String countName) {

    CounterPojo cp = ofy().load().type(CounterPojo.class).filter("name", countName).first().now();
    if (cp == null) {
      cp = new CounterPojo(countName);
    }
    cp.increment();
    ofy().save().entity(cp).now();

    return cp.count;
  }

dan,

// called from Task Queue
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
  String[] countName = req.getParameterValues("countName");
  if (countName.length != 1) {
    resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
    return;
  }
  updateCount(countName[0]);
}

Kemudian, buat tugas dengan servlet:

public class SomeRequestServlet extends HttpServlet {

  // Handler for URL get requests.
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {

    // Increment the count for the current namespace asynchronously.
    QueueFactory.getDefaultQueue()
        .add(TaskOptions.Builder.withUrl("/_ah/update_count").param("countName", "SomeRequest"));
    // Increment the global count and set the
    // namespace locally.  The namespace is
    // transferred to the invoked request and
    // executed asynchronously.
    String namespace = NamespaceManager.get();
    try {
      NamespaceManager.set("-global-");
      QueueFactory.getDefaultQueue()
          .add(TaskOptions.Builder.withUrl("/_ah/update_count").param("countName", "SomeRequest"));
    } finally {
      NamespaceManager.set(namespace);
    }
    resp.setContentType("text/plain");
    resp.getWriter().println("Counts are being updated.");
  }
}

Menggunakan namespace dengan Blobstore

Blobstore tidak tersegmentasi berdasarkan namespace. Untuk mempertahankan namespace di Blobstore, Anda perlu mengakses Blobstore melalui media penyimpanan yang memahami namespace (saat ini hanya memcache, datastore, dan task queue). Misalnya, jika Key blob disimpan dalam entity datastore, Anda dapat mengaksesnya dengan Key atau Query datastore yang mengetahui namespace.

Jika aplikasi mengakses Blobstore melalui kunci yang disimpan dalam penyimpanan berbasis namespace, Blobstore itu sendiri tidak perlu disegmentasikan berdasarkan namespace. Aplikasi harus menghindari kebocoran blob di antara namespace dengan cara:

  • Tidak menggunakan com.google.appengine.api.blobstore.BlobInfoFactory untuk permintaan pengguna akhir. Anda dapat menggunakan kueri BlobInfo untuk permintaan administratif (seperti membuat laporan tentang semua blob aplikasi). Namun, menggunakannya untuk permintaan pengguna akhir dapat menyebabkan kebocoran data karena semua data BlobInfo tidak dikelompokkan menurut namespace.
  • Tidak menggunakan kunci Blobstore dari sumber yang tidak tepercaya.

Menetapkan namespace untuk Kueri Datastore

Di konsol Google Cloud, Anda dapat menetapkan namespace untuk kueri Datastore.

Jika tidak ingin menggunakan default-nya, pilih namespace yang ingin digunakan dari menu drop-down.

Menggunakan namespace dengan Loader Massal

Loader massal mendukung flag --namespace=NAMESPACE yang memungkinkan Anda menentukan namespace yang akan digunakan. Setiap namespace ditangani secara terpisah dan, jika ingin mengakses semua namespace, Anda harus melakukan iterasi.

Instance Index baru mewarisi namespace SearchService yang digunakan untuk membuatnya. Setelah Anda membuat referensi ke indeks, namespace-nya tidak dapat diubah. Ada dua cara untuk menetapkan namespace untuk SearchService sebelum menggunakannya untuk membuat indeks:

  • Secara default, SearchService baru akan menggunakan namespace saat ini. Anda dapat menetapkan namespace saat ini sebelum membuat layanan:
// Set the current namespace to "aSpace"
NamespaceManager.set("aSpace");
// Create a SearchService with the namespace "aSpace"
SearchService searchService = SearchServiceFactory.getSearchService();
// Create an IndexSpec
IndexSpec indexSpec = IndexSpec.newBuilder().setName("myIndex").build();
// Create an Index with the namespace "aSpace"
Index index = searchService.getIndex(indexSpec);

Anda dapat menentukan namespace di SearchServiceConfig saat membuat layanan:

// Create a SearchServiceConfig, specifying the namespace "anotherSpace"
SearchServiceConfig config =
    SearchServiceConfig.newBuilder().setNamespace("anotherSpace").build();
// Create a SearchService with the namespace "anotherSpace"
searchService = SearchServiceFactory.getSearchService(config);
// Create an IndexSpec
indexSpec = IndexSpec.newBuilder().setName("myindex").build();
// Create an Index with the namespace "anotherSpace"
index = searchService.getIndex(indexSpec);