שליחת בקשות בכמות גדולה

במסמך הזה נסביר איך לקבץ באצווה קריאות ל-API כדי לצמצם את מספר החיבורים שהלקוח צריך לבצע.

המסמך הזה עוסק בשליחת בקשה באצווה באמצעות ספריית הלקוח של Java. דוגמה בסיסית זמינה גם ב ספריית הלקוח של Google API ל- .NET. המערכת של ממשק ה-API ל-EMM של Google Play משתמשת באותו HTTP בתור מערכת עיבוד אצווה של OData.

סקירה כללית

כל בקשה שהלקוח שולח דרך Google Play EMM API מובילה לסכום מסוים של תקורה. ה-API של Google Play ל-EMM תומך בקיבוץ באצווה של קריאות כדי לאפשר ללקוח לבצע מספר קריאות ל-API בבקשה אחת.

הנה כמה דוגמאות למצבים שבהם כדאי להשתמש בקיבוץ באצווה של קריאות:

  • דומיין נרשם עכשיו ויש לו הרבה נתונים להעלאה.
  • משתמש ביצע שינויים בנתונים בזמן שהאפליקציה לא הייתה מחוברת לאינטרנט, לכן האפליקציה צריכה לסנכרן כמות גדולה של נתונים מקומיים עם השרת.

במקרים כאלה, במקום לשלוח כל שיחה בנפרד, אפשר לקבץ את כל השיחות בבקשה אחת. תוכלו אפילו לקבץ בקשות עבור משתמשים מרובים, או עבור מספר ממשקי API של Google.

עם זאת, ניתן להגביל את 1, 000 קריאות בבקשה באצווה אחת. אם צריך לבצע יותר קריאות, תוכלו להשתמש במספר בקשות באצווה.

פירוט לגבי בקשות באצווה

בקשה באצווה מורכבת מכמה קריאות ל-API המאוגדות לבקשת JSON-RPC אחת. בקטע הזה נתאר בפירוט את התחביר של בקשות באצווה, ותופיע דוגמה בקטע הבא.

הערה: קבוצה של n בקשות מקובצות יחד נספרת כדי לחשב את מגבלת השימוש כבקשות n, ולא כבקשה אחת. הבקשה באצווה מחולקת לקבוצה של בקשות לפני העיבוד.

הפורמט של בקשה באצווה

ספריית הלקוח של Java מכילה קריאות ליצירת בקשות לכל קריאה ל-API של EMM ב-Google Play. לדוגמה, כדי להציג את רשימת כל האפליקציות המותקנות במכשיר, צריך להשתמש בפקודה הבאה:

AndroidEnterprise enterprise = ...;
InstallsListResponse response = enterprise.installs().list(enterpriseId, userId, deviceId)
  .execute();

יש שיחת batch() נוספת שיכולה להוסיף מספר בקשות לתור, כפי שמוצג כאן:

AndroidEnterprise enterprise = ...;
BatchRequest batchRequest = enterprise.batch();
enterprise.installs().list(enterpriseId, userId, deviceId1).queue(batchRequest, callback1);
enterprise.installs().list(enterpriseId, userId, deviceId2).queue(batchRequest, callback2);
enterprise.installs().list(enterpriseId, userId, deviceId3).queue(batchRequest, callback3);
batchRequest.execute();
כשמתבצעת קריאה אל batchRequest.execute(), כל הבקשות שבתור נשלחות בבת אחת לשרת כמערך JSON. השרת מחיל את הפרמטרים והכותרות של השאילתה החיצונית (לפי הכללים) על כל חלק, ולאחר מכן מתייחס לכל חלק כאילו היה בקשת JSON נפרדת.

תשובה לבקשה באצווה

השרת מבצע כל בקשה נפרדת ומקבץ את התוצאה לתגובה אחת שמורכבת ממערך יחיד. ספריית הלקוח מפצלת את התשובה הזו לתגובות נפרדות, וכל אחת מהן נשלחת לפונקציית הקריאה החוזרת שמועברת אל queue(). הקריאה החוזרת היא ממשק שמגדיר שיטה לכשל ושיטה להצלחה. לדוגמה, callback1 יוטמע כמופע של:

private class InstallsCallback implements JsonBatchCallback<InstallsListResponse> {

  @Override
  public void onSuccess(InstallsListResponse response, HttpHeaders responseHeaders) {
    ...
  }

  @Override
  public void onFailure(GoogleJsonError e, HttpHeaders responseHeaders) {
    ...
  }
}

הערה: השרת עשוי לבצע את הקריאות בכל סדר שהוא, לכן אין להסתמך על קבלת התוצאות לפי הסדר שצוין בבקשה. אם רוצים להבטיח ששתי קריאות יתבצעו בסדר מסוים, אי אפשר לשלוח אותן בבקשה אחת. במקום זאת, צריך לשלוח את הבקשה הראשונה בנפרד, ולהמתין לתגובה לפני ששולחים את הבקשה השנייה.

דוגמה לבקשה באצווה

בדוגמה הבאה תוכלו לראות איך להציג רשימה של כל האפליקציות שמותקנות בכל המכשירים של המשתמש. הקריאות הראשונות משמשות לקבלת המזהה של הארגון ושל המשתמש, ולכן צריך לבצע אותן ברצף. אחרי שכל מזהי המכשירים יתקבלו באמצעות enterprise.devices().list(), נוכל לבצע בקשה באצווה כדי לאחזר את כל האפליקציות בכל המכשירים של המשתמש בבת אחת.

package com.google.playenterprise.example;

import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.googleapis.batch.BatchRequest;
import com.google.api.client.googleapis.batch.json.JsonBatchCallback;
import com.google.api.client.googleapis.json.GoogleJsonError;
import com.google.api.client.http.HttpHeaders;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestInitializer;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson.JacksonFactory;
import com.google.api.services.androidenterprise.AndroidEnterprise;
import com.google.api.services.androidenterprise.AndroidEnterprise.Installs;
import com.google.api.services.androidenterprise.AndroidEnterpriseScopes;
import com.google.api.services.androidenterprise.model.Device;
import com.google.api.services.androidenterprise.model.DevicesListResponse;
import com.google.api.services.androidenterprise.model.Enterprise;
import com.google.api.services.androidenterprise.model.Install;
import com.google.api.services.androidenterprise.model.InstallsListResponse;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * Lists all the apps installed on all devices of a given user.
 */
public class ListAllInstalls {
  private AndroidEnterprise enterprise;
  private final List<String> installList = new ArrayList<>();

  public static void main(String[] argv) throws Exception {
    if (argv.length != 2) {
      throw new IllegalArgumentException("Usage: ListAllInstalls email jsonFilename");
    } else if (!argv[0].contains("@")) {
      throw new IllegalArgumentException("First parameter should be a valid email.");
    }
    new ListAllInstalls().run(argv[0], argv[1]);
  }

  private void run(String userEmail, String jsonKeyPath) throws IOException {
    enterprise = createAndroidEnterprise(jsonKeyPath);

    // Get the enterprise id, user id, and user devices.
    String domain = userEmail.split("@")[1];
    List<Enterprise> results = enterprise.enterprises().list(domain).execute().getEnterprise();
    if (results.isEmpty()) {
      throw new RuntimeException("No enterprise found.");
    }
    String enterpriseId = results.get(0).getId();
    String userId = enterprise
        .users()
        .list(enterpriseId, userEmail)
        .execute()
        .getUser()
        .get(0)
        .getId();
    List<Device> devices = getAllDevices(enterpriseId, userId);

    // Batch all calls to get installs on all user devices.
    gatherAllInstalls(enterpriseId, userId, devices);

    for (String entry : installList) {
      // Do something.
      System.out.println(entry);
    }
  }

  private List<Device> getAllDevices(String enterpriseId, String userId) throws IOException {
    DevicesListResponse devices = enterprise.devices().list(enterpriseId, userId).execute();
    return devices.getDevice();
  }

  private void gatherAllInstalls(String enterpriseId, String userId, List<Device> devices)
      throws IOException {
    BatchRequest batchRequest = enterprise.batch();
    for (Device device : devices) {
      Installs.List list = enterprise
          .installs().list(enterpriseId, userId, device.getAndroidId());
      // Each callback can take the specifics of the associated request in its constructor.
      list.queue(batchRequest, new InstallsCallback(device.getAndroidId()));
    }
    // Executes all the queued requests and their callbacks, single-threaded.
    batchRequest.execute();
  }

  private class InstallsCallback extends JsonBatchCallback<InstallsListResponse> {
    private final String androidId;

    InstallsCallback(String androidId) {
      this.androidId = androidId;
    }

    @Override
    public void onSuccess(InstallsListResponse response, HttpHeaders responseHeaders) {
      for (Install install : response.getInstall()) {
        installList.add(androidId + "," + install.getProductId());
      }
    }

    @Override
    public void onFailure(GoogleJsonError e, HttpHeaders responseHeaders) {
      throw new RuntimeException("Error fetching a device");
    }
  }

  private AndroidEnterprise createAndroidEnterprise(String jsonKeyPath) throws IOException {
    HttpTransport httpTransport = new NetHttpTransport();
    JsonFactory jsonFactory = new JacksonFactory();

    InputStream is = new BufferedInputStream(new FileInputStream(jsonKeyPath));
    final Credential credential = GoogleCredential.fromStream(is, httpTransport, jsonFactory)
        .createScoped(AndroidEnterpriseScopes.all());

    HttpRequestInitializer httpRequestInitializer = new HttpRequestInitializer() {
      @Override
      public void initialize(HttpRequest request) throws IOException {
        credential.initialize(request);
      }
    };
    return new AndroidEnterprise.Builder(httpTransport, jsonFactory, httpRequestInitializer)
        .build();
  }
}