แคชด้วย Firebase

Looker Studio มีระบบแคชของตัวเองสําหรับรายงาน เมื่อคุณสร้าง คุณก็สามารถใช้แคชที่กำหนดเอง เพื่อทำให้รายงานที่รวดเร็วขึ้น หลีกเลี่ยงขีดจำกัดอัตราดอกเบี้ยต่อปี (APR)

เช่น คุณกำลังสร้างเครื่องมือเชื่อมต่อที่ให้ข้อมูลสภาพอากาศที่ผ่านมา สำหรับรหัสไปรษณีย์เฉพาะในช่วง 7 วันที่ผ่านมา เครื่องมือเชื่อมต่อของคุณเริ่มเปลี่ยนไป ได้รับความนิยม แต่ API ภายนอกที่คุณกำลังดึงข้อมูลนั้นมีอัตราที่เข้มงวด ขีดจำกัด API จะอัปเดตข้อมูลของตนทุกวันเท่านั้น ดังนั้นสำหรับรหัสไปรษณีย์หนึ่งๆ จะมี ไม่จำเป็นต้องดึงข้อมูลเดียวกันหลายครั้งภายใน 1 วัน การใช้ คุณสามารถใช้แคชรายวันสำหรับแต่ละรหัสไปรษณีย์ได้

ข้อกำหนด

  • ฐานข้อมูลเรียลไทม์ของ Firebase หากไม่มีสิทธิ์เข้าถึง ให้สร้าง โครงการ Google Cloud Platform (GCP) และปฏิบัติตาม คู่มือเริ่มต้นใช้งานเพื่อสร้าง Firebase ของคุณเอง อินสแตนซ์ Realtime Database
  • บัญชีบริการ GCP สำหรับอ่านและเขียนข้อมูลจาก Firebase เรียลไทม์ ฐานข้อมูล
  • Community Connector ที่ดึงข้อมูลจากแหล่งข้อมูล

ข้อจำกัด

  • โซลูชันนี้ใช้กับบริการขั้นสูงของ Looker Studio ไม่ได้ วันและเวลา คุณใช้บริการขั้นสูงของ Looker Studio ซึ่งเป็นรหัสเครื่องมือเชื่อมต่อในแอป สคริปต์ไม่มีสิทธิ์เข้าถึงข้อมูล คุณจึงไม่สามารถแคชข้อมูลได้ โดยใช้ Apps Script
  • เครื่องมือแก้ไขรายงานและผู้ดูจะไม่สามารถรีเซ็ตแคชที่ระบุนี้ได้

โซลูชัน

ใช้บัญชีบริการ

  1. สร้างบัญชีบริการในโปรเจ็กต์ Google Cloud
  2. ตรวจสอบว่าบัญชีบริการนี้มีสิทธิ์เข้าถึง BigQuery ในโปรเจ็กต์ที่อยู่ในระบบคลาวด์
    • บทบาท Identity and Access Management (IAM) ที่จำเป็น: Firebase Admin
  3. ดาวน์โหลดไฟล์ JSON เพื่อรับคีย์บัญชีบริการ เก็บไฟล์ เนื้อหาในพร็อพเพอร์ตี้สคริปต์ของโปรเจ็กต์เครื่องมือเชื่อมต่อ หลังจากเพิ่ม ควรมีลักษณะคล้ายกับคีย์ใน UI ของ Apps Script:
    วันที่ กำลังบันทึกคีย์บัญชีบริการในพร็อพเพอร์ตี้สคริปต์
  4. รวมไลบรารี OAuth2 สำหรับ 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

คุณจะใช้ REST API ของฐานข้อมูล Firebase ในการอ่านและเขียนไปยัง Firebase Realtime Database โค้ดต่อไปนี้จะใช้เมธอดที่จำเป็นสำหรับ ที่เข้าถึง 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. ระบุ "กลุ่ม" หรือ "unit" ที่ควรแคช
  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'
   
]);
}

แหล่งข้อมูลเพิ่มเติม

เครื่องมือเชื่อมต่อ UX ของ Chrome ช่วยอำนวยความสะดวกให้กับแดชบอร์ดตาม BigQuery ประมาณ 20 GB ผู้ใช้หลายพันคนได้ เครื่องมือเชื่อมต่อนี้ใช้ฐานข้อมูลเรียลไทม์ของ Firebase พร้อมด้วยบริการแคช Apps Script สำหรับวิธีการแคช 2 ชั้น โปรดดู code เพื่อดูรายละเอียดการติดตั้ง