التخزين المؤقت باستخدام Firebase

تحتوي أداة Looker Studio على نظام ذاكرة تخزين مؤقت خاص بها للتقارير. عند إنشاء موصل، يمكنك تنفيذ ذاكرة تخزين مؤقت مخصصة لتسهيل إعداد التقارير تجنُّب حدود معدل الفائدة المئوية السنوية

على سبيل المثال، في حال إنشاء موصِّل يوفّر بيانات الطقس السابقة خلال آخر 7 أيام لرمز بريدي معيّن. يبدو أن الموصل شائعة ولكن تخضع واجهة برمجة التطبيقات الخارجية التي تجلب البيانات منها بمعدل صارم الحدود. تقوم واجهة برمجة التطبيقات بتحديث بياناتها يوميًا فقط، لذا بالنسبة للرمز البريدي المحدد، ليست هناك حاجة لجلب البيانات نفسها عدة مرات خلال يوم واحد. استخدام هذا دليل الحلول، يمكنك تنفيذ ذاكرة تخزين مؤقت يومية لكل رمز بريدي.

المتطلبات

  • قاعدة بيانات Firebase في الوقت الفعلي إذا لم يكن لديك إذن الوصول إليه، أنشئ Google Cloud Platform (GCP) وتتبع دليل البدء لإنشاء موقعك الإلكتروني على Firebase مثيل قاعدة البيانات في الوقت الفعلي.
  • حساب خدمة Google Cloud Platform لقراءة البيانات وكتابتها من Firebase في الوقت الفعلي قاعدة البيانات.
  • يشير ذلك المصطلح إلى موصِّل مجتمع يجلب البيانات من مصدر.

القيود

  • لا يمكن استخدام هذا الحلّ مع الخدمات المتقدّمة في Looker Studio. فعندما استخدام خدمات Looker Studio المتقدمة، رمز الموصِّل في التطبيقات لا يمكن للنص البرمجي الوصول إلى البيانات. وبالتالي لا يمكنك تخزين البيانات باستخدام "برمجة تطبيقات Google".
  • ولا يمكن لمحرِّري التقارير والمشاهدين إعادة ضبط ذاكرة التخزين المؤقت هذه.

الحل

تنفيذ حساب خدمة

  1. أنشِئ حساب خدمة في مشروعك على Google Cloud.
  2. تأكَّد من أنّ حساب الخدمة هذا لديه إذن الوصول إلى BigQuery في المشروع على السحابة الإلكترونية.
    • أدوار "إدارة الهوية وإمكانية الوصول" (IAM) المطلوبة: Firebase Admin
  3. نزِّل ملف JSON للحصول على مفاتيح حسابات الخدمة. تخزين المحتوى في خصائص النصوص البرمجية لمشروع الموصِّل. بعد إضافة من المفترض أن تبدو مشابهة لما يلي في واجهة مستخدم "برمجة تطبيقات Google":
    حفظ مفاتيح حساب الخدمة في "خصائص النص البرمجي"
  4. تضمين مكتبة OAuth2 لبرمجة التطبيقات في مشروع "برمجة تطبيقات Google"
  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 لقراءة بيانات Firebase والكتابة إليها. قاعدة بيانات الوقت الفعلي. تنفذ التعليمة البرمجية التالية الطرق اللازمة بالوصول إلى واجهة برمجة التطبيقات هذه.

تنفيذ الطريقة 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 لوحة البيانات استنادًا إلى BigQuery بحجم يقارب 20 غيغابايت إلى آلاف المستخدمين. يستخدم هذا الموصِّل "قاعدة بيانات Firebase في الوقت الفعلي" بالإضافة إلى خدمة ذاكرة التخزين المؤقت لبرمجة التطبيقات لتوفير خيار التخزين المؤقت المكوّن من طبقتين. عرض للحصول على تفاصيل التنفيذ.