ফায়ারবেসের সাথে ক্যাশে

প্রতিবেদনের জন্য লুকার স্টুডিওর নিজস্ব ক্যাশে সিস্টেম রয়েছে। আপনার সংযোগকারী তৈরি করার সময়, আপনি দ্রুত প্রতিবেদনের সুবিধার্থে একটি কাস্টম ক্যাশে প্রয়োগ করতে পারেন এবং APR হার-সীমা এড়াতে পারেন।

উদাহরণস্বরূপ, আপনি একটি সংযোগকারী তৈরি করছেন যা একটি নির্দিষ্ট জিপ কোডের জন্য গত 7 দিনের ঐতিহাসিক আবহাওয়ার ডেটা প্রদান করে। আপনার সংযোগকারীটি বেশ জনপ্রিয় হয়ে উঠছে তবে আপনি যে বাহ্যিক API থেকে ডেটা আনছেন তার কঠোর হারের সীমা রয়েছে৷ API শুধুমাত্র প্রতিদিন তার ডেটা আপডেট করে, তাই একটি নির্দিষ্ট জিপ কোডের জন্য, একই ডেটা এক দিনের মধ্যে একাধিকবার আনতে হবে না। এই সমাধান নির্দেশিকা ব্যবহার করে, আপনি প্রতিটি জিপ কোডের জন্য একটি দৈনিক ক্যাশে প্রয়োগ করতে পারেন।

প্রয়োজনীয়তা

  • একটি ফায়ারবেস রিয়েলটাইম ডাটাবেস । আপনার যদি একটিতে অ্যাক্সেস না থাকে তবে একটি Google ক্লাউড প্ল্যাটফর্ম (GCP) প্রকল্প তৈরি করুন এবং আপনার নিজস্ব Firebase রিয়েলটাইম ডেটাবেস উদাহরণ তৈরি করতে শুরু করুন নির্দেশিকা অনুসরণ করুন৷
  • ফায়ারবেস রিয়েলটাইম ডেটাবেস থেকে ডেটা পড়তে এবং লেখার জন্য একটি GCP পরিষেবা অ্যাকাউন্ট।
  • একটি কমিউনিটি সংযোগকারী যা একটি উৎস থেকে ডেটা নিয়ে আসে।

সীমাবদ্ধতা

  • এই সমাধানটি লুকার স্টুডিও অ্যাডভান্সড সার্ভিসের সাথে ব্যবহার করা যাবে না। আপনি যখন Looker Studio Advanced Services ব্যবহার করেন, তখন Apps Script-এ আপনার সংযোগকারী কোডের ডেটাতে অ্যাক্সেস থাকে না। সুতরাং আপনি অ্যাপস স্ক্রিপ্ট ব্যবহার করে ডেটা ক্যাশে করতে পারবেন না।
  • রিপোর্ট সম্পাদক এবং দর্শকরা এই নির্দিষ্ট ক্যাশে রিসেট করতে পারবেন না।

সমাধান

একটি পরিষেবা অ্যাকাউন্ট বাস্তবায়ন করুন

  1. আপনার Google ক্লাউড প্রকল্পে একটি পরিষেবা অ্যাকাউন্ট তৈরি করুন
  2. ক্লাউড প্রোজেক্টে এই পরিষেবা অ্যাকাউন্টের BigQuery অ্যাক্সেস আছে কিনা তা নিশ্চিত করুন।
    • প্রয়োজনীয় আইডেন্টিটি এবং অ্যাক্সেস ম্যানেজমেন্ট (IAM) ভূমিকা: Firebase Admin
  3. পরিষেবা অ্যাকাউন্ট কী পেতে JSON ফাইলটি ডাউনলোড করুন। আপনার সংযোগকারী প্রকল্পের স্ক্রিপ্ট বৈশিষ্ট্যে ফাইলের বিষয়বস্তু সংরক্ষণ করুন। কীগুলি যোগ করার পরে, অ্যাপস স্ক্রিপ্ট UI-তে এটির মতো দেখতে হবে:
    স্ক্রিপ্ট বৈশিষ্ট্যে পরিষেবা অ্যাকাউন্ট কী সংরক্ষণ করা হচ্ছে
  4. আপনার Apps স্ক্রিপ্ট প্রকল্পে Apps স্ক্রিপ্ট লাইব্রেরির জন্য OAuth2 অন্তর্ভুক্ত করুন
  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 ব্যবহার করবেন। নিম্নলিখিত কোড এই 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. ক্যাশে ডেটার ন্যূনতম ইউনিট সংরক্ষণ করতে একটি অনন্য কী তৈরি করুন।
    উদাহরণ বাস্তবায়নের জন্য, configparams থেকে zipcode কী হিসাবে ব্যবহার করা হচ্ছে।
    ঐচ্ছিক : প্রতি-ব্যবহারকারী ক্যাশের জন্য, বেস কী এবং ব্যবহারকারীর পরিচয় সহ একটি যৌগিক কী তৈরি করুন। উদাহরণ বাস্তবায়ন:
    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 সংযোগকারী হাজার হাজার ব্যবহারকারীকে একটি ~20GB BigQuery টেবিলের উপর ভিত্তি করে একটি ড্যাশবোর্ডের সুবিধা দেয়৷ এই সংযোগকারী দুটি স্তরযুক্ত ক্যাশিং পদ্ধতির জন্য Apps স্ক্রিপ্ট ক্যাশে পরিষেবা সহ Firebase রিয়েলটাইম ডেটাবেস ব্যবহার করে। বাস্তবায়নের বিশদ বিবরণের জন্য কোড দেখুন।