کش با Firebase

Looker Studio سیستم کش مخصوص به خود را برای گزارش ها دارد. هنگام ایجاد رابط خود، می‌توانید یک کش سفارشی برای تسهیل گزارش‌های سریع‌تر و اجتناب از محدودیت‌های نرخ APR پیاده‌سازی کنید.

به عنوان مثال، شما در حال ایجاد یک رابط هستید که داده های آب و هوای تاریخی 7 روز گذشته را برای یک کد پستی خاص ارائه می دهد. رابط شما بسیار محبوب شده است، اما API خارجی که داده‌ها را از آن واکشی می‌کنید دارای محدودیت‌های نرخ سختی است. API فقط داده های خود را روزانه به روز می کند، بنابراین برای یک کد پستی خاص، نیازی به واکشی چندین بار داده های مشابه در یک روز نیست. با استفاده از این راهنمای راه حل، می توانید یک کش روزانه برای هر کد پستی پیاده سازی کنید.

الزامات

  • پایگاه داده Firebase Realtime . اگر به یکی از آنها دسترسی ندارید، یک پروژه Google Cloud Platform (GCP) ایجاد کنید و راهنمای شروع را دنبال کنید تا نمونه پایگاه داده بیدرنگ Firebase خود را ایجاد کنید.
  • یک حساب سرویس GCP برای خواندن و نوشتن داده ها از پایگاه داده بیدرنگ Firebase.
  • یک اتصال دهنده انجمن که داده ها را از یک منبع واکشی می کند.

محدودیت ها

  • این راه حل با سرویس های پیشرفته استودیو Looker قابل استفاده نیست. وقتی از سرویس‌های پیشرفته استودیو Looker استفاده می‌کنید، کد رابط شما در Apps Script به داده‌ها دسترسی ندارد. بنابراین نمی توانید داده ها را با استفاده از Apps Script ذخیره کنید.
  • ویرایشگران گزارش و بینندگان نمی توانند این حافظه پنهان خاص را بازنشانی کنند.

راه حل

یک حساب خدماتی را پیاده سازی کنید

  1. یک حساب سرویس در پروژه Google Cloud خود ایجاد کنید .
  2. اطمینان حاصل کنید که این حساب سرویس دارای دسترسی BigQuery در پروژه ابری است.
    • نقش های مورد نیاز مدیریت هویت و دسترسی (IAM): Firebase Admin
  3. فایل JSON را دانلود کنید تا کلیدهای حساب های سرویس را دریافت کنید. محتوای فایل را در ویژگی های اسکریپت پروژه رابط خود ذخیره کنید. پس از افزودن کلیدها، باید مشابه این در رابط کاربری Apps Script به نظر برسد:
    ذخیره کلیدهای حساب سرویس در ویژگی های اسکریپت
  4. کتابخانه OAuth2 for Apps Script را در پروژه Apps Script خود قرار دهید .
  5. کد OAuth2 مورد نیاز را برای حساب سرویس پیاده سازی کنید:
firestore-cache/src/firebase.js
var SERVICE_ACCOUNT_CREDS = 'SERVICE_ACCOUNT_CREDS';
var SERVICE_ACCOUNT_KEY = 'private_key';
var SERVICE_ACCOUNT_EMAIL = 'client_email';
var BILLING_PROJECT_ID = 'project_id';

var scriptProperties = PropertiesService.getScriptProperties();

/**
 * Copy the entire credentials JSON file from creating a service account in GCP.
 * Service account should have `Firebase Admin` IAM role.
 */
function getServiceAccountCreds() {
  return JSON.parse(scriptProperties.getProperty(SERVICE_ACCOUNT_CREDS));
}

function getOauthService() {
  var serviceAccountCreds = getServiceAccountCreds();
  var serviceAccountKey = serviceAccountCreds[SERVICE_ACCOUNT_KEY];
  var serviceAccountEmail = serviceAccountCreds[SERVICE_ACCOUNT_EMAIL];

  return OAuth2.createService('FirebaseCache')
    .setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth')
    .setTokenUrl('https://accounts.google.com/o/oauth2/token')
    .setPrivateKey(serviceAccountKey)
    .setIssuer(serviceAccountEmail)
    .setPropertyStore(scriptProperties)
    .setCache(CacheService.getScriptCache())
    .setScope([
      'https://www.googleapis.com/auth/userinfo.email',
      'https://www.googleapis.com/auth/firebase.database'
    ]);
}

کد را برای خواندن و نوشتن از Firebase پیاده سازی کنید

شما از Firebase Database REST API برای خواندن و نوشتن در پایگاه داده Realtime Firebase استفاده خواهید کرد. کد زیر روش های مورد نیاز برای دسترسی به این API را پیاده سازی می کند.

پیاده سازی getData()

ساختار کد getData() موجود بدون ذخیره کش باید به شکل زیر باشد:

firestore-cache/src/without-caching.js
/*
 * This file is only to demonstrate how the `getData()` fucntion would look
 * like without the Firebase Realtime Database caching. It is not a part of
 * the connector code and should not be included in Apps Script / Clasp.
 */

function getData(request) {
  var requestedFields = getFields().forIds(
    request.fields.map(function(field) {
      return field.name;
    })
  );

  var fetchedData = fetchAndParseData(request);
  var data = getFormattedData(fetchedData, requestedFields);

  return {
    schema: requestedFields.build(),
    rows: data
  };
}

برای استفاده از کش در کد getData() خود، این مراحل را دنبال کنید:

  1. "تکه" یا "واحد" داده ای را که باید در حافظه پنهان ذخیره شود، تعیین کنید.
  2. یک کلید منحصر به فرد برای ذخیره حداقل واحد داده در حافظه پنهان ایجاد کنید.
    برای اجرای مثال، zipcode از configparams به عنوان کلید استفاده می شود.
    اختیاری : برای حافظه پنهان هر کاربر، یک کلید ترکیبی با کلید پایه و هویت کاربر ایجاد کنید. اجرای مثال:
    js var baseKey = getBaseKey(request); var userEmail = Session.getEffectiveUser().getEmail(); var hasheduserEmail = getHashedValue(userEmail); var compositeKey = baseKey + hasheduserEmail;

  3. اگر داده‌های کش وجود دارد، بررسی کنید که حافظه پنهان تازه است یا خیر.
    در مثال، داده های کش شده برای یک کد پستی خاص با تاریخ فعلی ذخیره می شوند. هنگامی که داده ها از حافظه پنهان بازیابی می شوند، تاریخ حافظه پنهان با تاریخ فعلی بررسی می شود.

    var cacheForZipcode = {
      data: <data being cached>,
      ymd: <current date in YYYYMMDD format>
    }
    
  4. اگر داده‌های کش وجود ندارد یا داده‌های حافظه پنهان تازه نیستند، داده‌ها را از منبع واکشی کرده و در حافظه پنهان ذخیره کنید.

در مثال زیر، main.js شامل کد getData() با کش پیاده سازی شده است.

کد نمونه

main.js

firestore-cache/src/main.js
// [start common_connector_code]
var cc = DataStudioApp.createCommunityConnector();

function getAuthType() {
  return cc
    .newAuthTypeResponse()
    .setAuthType(cc.AuthType.NONE)
    .build();
}

function getConfig(request) {
  var config = cc.getConfig();

  config
    .newTextInput()
    .setId('zipcode')
    .setName('Enter Zip Code')
    .setPlaceholder('eg. 95054');

  return config.build();
}

function getFields() {
  var fields = cc.getFields();
  var types = cc.FieldType;
  var aggregations = cc.AggregationType;

  fields
    .newDimension()
    .setId('zipcode')
    .setName('Zip code')
    .setType(types.TEXT);

  fields
    .newDimension()
    .setId('date')
    .setName('Date')
    .setType(types.YEAR_MONTH_DAY);

  fields
    .newMetric()
    .setId('temperature')
    .setName('Temperature (F)')
    .setType(types.NUMBER)
    .setIsReaggregatable(false);

  return fields;
}

function getSchema(request) {
  return {
    schema: getFields().build()
  };
}
// [end common_connector_code]

// [start caching_implementation]
function getData(request) {
  var requestedFields = getFields().forIds(
    request.fields.map(function(field) {
      return field.name;
    })
  );

  var cacheUpdateNeeded = true;
  var url = buildFirebaseUrl(request.configParams.zipcode);
  var cache = firebaseCache('get', url);

  if (cache) {
    var currentYmd = getCurrentYmd();
    cacheUpdateNeeded = currentYmd > cache.ymd;
  }

  if (cacheUpdateNeeded) {
    var fetchedData = fetchAndParseData(request);
    cache = {};
    cache.data = fetchedData;
    cache.ymd = currentYmd;
    firebaseCache('delete', url);
    firebaseCache('post', url, cache);
  }

  var data = getFormattedData(cache.data, requestedFields);

  var requestedFields = getFields().forIds(
    request.fields.map(function(field) {
      return field.name;
    })
  );

  var cache = getCachedData(request);
  var data = getFormattedData(cache, requestedFields);

  return {
    schema: requestedFields.build(),
    rows: data
  };
}

function getCachedData(request) {
  var cacheUpdateNeeded = true;
  var url = buildFirebaseUrl(request.configParams.zipcode);
  var cachedData = getFromCache(url);
  var currentYmd = getCurrentYmd();

  if (cachedData) {
    cacheUpdateNeeded = currentYmd > cachedData.ymd;
  }

  if (cacheUpdateNeeded) {
    var fetchedData = fetchAndParseData(request);
    freshData = {};
    freshData.data = fetchedData;
    freshData.ymd = currentYmd;
    deleteFromCache(url);
    putInCache(url, freshData);
    cachedData = freshData;
  }

  return cachedData.data;
}

function getCurrentYmd() {
  var currentDate = new Date();
  var year = currentDate.getFullYear();
  var month = ('0' + (currentDate.getMonth() + 1)).slice(-2);
  var date = ('0' + currentDate.getDate()).slice(-2);
  var currentYmd = year + month + date;
  return currentYmd;
}

// [end caching_implementation]

// [start common_getdata_implementation]
function fetchAndParseData(request) {
  // TODO: Connect to your own API endpoint and parse the fetched data.
  // To keep this example simple, we are returning dummy data instead of
  // connecting to an enpoint. This does not affect the caching.
  var parsedData = sampleData;
  return parsedData;
}

function getFormattedData(fetchedData, requestedFields) {
  var data = fetchedData.map(function(rowData) {
    return formatData(rowData, requestedFields);
  });
  return data;
}

function formatData(rowData, requestedFields) {
  var row = requestedFields.asArray().map(function(requestedField) {
    switch (requestedField.getId()) {
      case 'date':
        return rowData.date;
      case 'zipcode':
        return rowData.zipcode;
      case 'temperature':
        return rowData.temperature;
      default:
        return '';
    }
  });
  return {values: row};
}
// [end common_getdata_implementation]

var sampleData = [
  {
    date: '20190601',
    zipcode: '95054',
    temperature: 80
  },
  {
    date: '20190602',
    zipcode: '95054',
    temperature: 82
  },
  {
    date: '20190603',
    zipcode: '95054',
    temperature: 82
  },
  {
    date: '20190604',
    zipcode: '95054',
    temperature: 85
  },
  {
    date: '20190605',
    zipcode: '95054',
    temperature: 84
  },
  {
    date: '20190606',
    zipcode: '95054',
    temperature: 83
  },
  {
    date: '20190607',
    zipcode: '95054',
    temperature: 81
  }
];

firebase.js

firestore-cache/src/firebase.js
// [start firebase_access_implementation]

var FIREBASE_REALTIME_DB_BASE_URL = '.firebaseio.com';
var FIREBASE_REALTIME_DB_COLLECTION = '/cache';

/**
 * Returns the URL for a file in a firebase database.
 *
 * @param {string} fileName The filename in the database
 * @returns {string} The url for the file in the database
 */
function buildFirebaseUrl(fileName) {
  var serviceAccountCreds = getServiceAccountCreds();
  var projectId = serviceAccountCreds[BILLING_PROJECT_ID];

  if (fileName) {
    fileName = '/' + fileName;
  }
  var urlElements = [
    'https://',
    projectId,
    FIREBASE_REALTIME_DB_BASE_URL,
    FIREBASE_REALTIME_DB_COLLECTION,
    fileName,
    '.json'
  ];
  var url = urlElements.join('');
  return url;
}

/**
 * Generic method for handling the Firebase Realtime Database REST API.
 * For `get`: returns the data at the given url.
 * For `post`: posts the data in in firestore db at the given url and returns `undefined`.
 * For `delete`: deletes the data at the given url and returns `undefined`.
 *
 * @param {string} method Method for the REST API: `get`, `post`, or `delete`
 * @param {string} url REST endpoint
 * @param {string} [data] Data to be stored for `post` method
 * @returns {undefined|object} Returns data from the REST endpoint for `get`
 *          method. For other methods, returns `undefined`.
 */
function firebaseCache(method, url, data) {
  var oAuthToken = getOauthService().getAccessToken();

  var responseOptions = {
    headers: {
      Authorization: 'Bearer ' + oAuthToken
    },
    method: method,
    contentType: 'application/json'
  };

  // Add payload for post method
  if (method === 'post') {
    responseOptions['payload'] = JSON.stringify(data);
  }

  var response = UrlFetchApp.fetch(url, responseOptions);

  // Return value only for `get`.
  if (method === 'get') {
    var responseObject = JSON.parse(response);
    if (responseObject === null) {
      return null;
    } else {
      var autoKey = Object.keys(responseObject)[0];
      var returnValue = responseObject[autoKey];
    }
    return returnValue;
  }
}

function getFromCache(url) {
  return firebaseCache('get', url);
}

function deleteFromCache(url) {
  return firebaseCache('delete', url);
}

function putInCache(url, data) {
  return firebaseCache('put', url, data);
}

// [end firebase_access_implementation]

var SERVICE_ACCOUNT_CREDS = 'SERVICE_ACCOUNT_CREDS';
var SERVICE_ACCOUNT_KEY = 'private_key';
var SERVICE_ACCOUNT_EMAIL = 'client_email';
var BILLING_PROJECT_ID = 'project_id';

var scriptProperties = PropertiesService.getScriptProperties();

/**
 * Copy the entire credentials JSON file from creating a service account in GCP.
 * Service account should have `Firebase Admin` IAM role.
 */
function getServiceAccountCreds() {
  return JSON.parse(scriptProperties.getProperty(SERVICE_ACCOUNT_CREDS));
}

function getOauthService() {
  var serviceAccountCreds = getServiceAccountCreds();
  var serviceAccountKey = serviceAccountCreds[SERVICE_ACCOUNT_KEY];
  var serviceAccountEmail = serviceAccountCreds[SERVICE_ACCOUNT_EMAIL];

  return OAuth2.createService('FirebaseCache')
    .setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth')
    .setTokenUrl('https://accounts.google.com/o/oauth2/token')
    .setPrivateKey(serviceAccountKey)
    .setIssuer(serviceAccountEmail)
    .setPropertyStore(scriptProperties)
    .setCache(CacheService.getScriptCache())
    .setScope([
      'https://www.googleapis.com/auth/userinfo.email',
      'https://www.googleapis.com/auth/firebase.database'
    ]);
}

منابع اضافی

رابط کاربری Chrome UX داشبورد مبتنی بر جدول BigQuery 20 گیگابایتی را برای هزاران کاربر تسهیل می‌کند. این رابط از Firebase Realtime Database به همراه Apps Script Cache Service برای یک رویکرد کش دو لایه استفاده می کند. برای جزئیات پیاده سازی به کد مراجعه کنید.