Firebase로 캐시

Looker Studio에는 보고서를 위한 자체 캐시 시스템이 있습니다. 맞춤 캐시를 구현하면 더 빠른 보고서와 연이율 제한을 피하세요

예를 들어 과거 날씨 데이터를 제공하는 커넥터를 만드는 경우 특정 우편번호의 경우 지난 7일간 커넥터가 많이 있지만 데이터를 가져오는 외부 API의 속도가 엄격합니다. 제한하기 때문입니다. API는 데이터를 매일만 업데이트하므로 특정 우편번호의 경우 하루에 같은 데이터를 여러 번 가져올 필요가 없습니다. 사용 솔루션 가이드에 따라 각 우편번호에 일일 캐시를 구현할 수 있습니다.

요구사항

  • Firebase 실시간 데이터베이스 액세스할 수 없는 경우 Google Cloud Platform (GCP) 프로젝트와 나만의 Firebase를 만들기 위한 시작하기 가이드 실시간 데이터베이스 인스턴스입니다.
  • Firebase 실시간에서 데이터를 읽고 쓸 GCP 서비스 계정 데이터베이스.
  • 소스에서 데이터를 가져오는 커뮤니티 커넥터

제한사항

  • 이 솔루션은 Looker Studio 고급 서비스와 함께 사용할 수 없습니다. 날짜 앱의 커넥터 코드인 Looker Studio Advanced Services를 사용하는 경우 스크립트에서 데이터에 액세스할 수 없습니다. 따라서 데이터를 캐시할 수 없으며 애플리케이션을 실행할 수 있습니다
  • 보고서 수정 권한 사용자 및 보기 권한 사용자는 이 특정 캐시를 재설정할 수 없습니다.

솔루션

서비스 계정 구현

  1. Google Cloud 프로젝트에서 서비스 계정을 만듭니다.
  2. 이 서비스 계정에 클라우드 프로젝트의 BigQuery 액세스 권한이 있는지 확인하세요.
    • 필요한 Identity and Access Management (IAM) 역할: Firebase Admin
  3. JSON 파일을 다운로드하여 서비스 계정 키를 가져옵니다. 파일의 커넥터 프로젝트의 스크립트 속성에 있을 수 있습니다. 먼저 키는 Apps Script UI에서 다음과 같이 표시됩니다.
    스크립트 속성에 서비스 계정 키 저장
  4. Apps Script 프로젝트에 Apps Script용 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를 사용하여 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. '청크' 결정 또는 'unit' 캐시되어야 하는 데이터의 양을 나타냅니다
  2. 캐시에 최소 데이터 단위를 저장할 고유 키를 만듭니다.
    구현 예에서는 configparamszipcode가 사용됩니다. 을 키로 사용합니다.
    선택사항: 사용자별 캐시의 경우 기본 키와 사용자 ID 구현 예시:
    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() 코드가 포함됩니다. 있습니다.

예시 코드

기본.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를 기반으로 한 대시보드를 지원합니다. 수많은 사용자에게 제공할 수 있습니다 이 커넥터는 Firebase 실시간 데이터베이스를 사용합니다. Apps Script Cache Service를 함께 제공합니다. 자세한 내용은 코드를 참조하세요.