Almacena en caché con Firebase

Looker Studio tiene su propio sistema de caché para los informes. Cuando crees tu puedes implementar una caché personalizada para generar informes y evitar los límites de la TAE.

Por ejemplo, estás creando un conector que proporciona datos meteorológicos históricos de los últimos 7 días para un código postal específico. Tu conector es cada vez más popular, pero la API externa de la que recuperas datos tiene una tasa estricta límites. La API solo actualiza los datos diariamente; por lo tanto, para un código postal específico, hay no es necesario recuperar los mismos datos varias veces durante un día. Uso puedes implementar una caché diaria para cada código postal.

Requisitos

  • Una base de datos de Firebase Realtime Si no tienes acceso a una, crea una proyecto de Google Cloud Platform (GCP) y sigue las Guía de primeros pasos para crear tu propio Firebase Instancia de Realtime Database.
  • Una cuenta de servicio de GCP para leer y escribir datos desde Firebase Realtime Base de datos.
  • Un conector de la comunidad que recupera datos de una fuente.

Limitaciones

  • Esta solución no se puede usar con los Servicios avanzados de Looker Studio. Cuándo debes usar los Servicios avanzados de Looker Studio, tu código de conector en Apps La secuencia de comandos no tiene acceso a los datos. Por lo tanto, no puedes almacenar en caché los datos con Apps Script.
  • Los editores y lectores de informes no pueden restablecer esta caché específica.

Solución

Cómo implementar una cuenta de servicio

  1. Crea una cuenta de servicio en tu proyecto de Google Cloud.
  2. Asegúrate de que esta cuenta de servicio tenga acceso a BigQuery en el proyecto de la nube.
    • Roles de Identity and Access Management (IAM) obligatorios: Firebase Admin
  3. Descarga el archivo JSON para obtener las claves de las cuentas de servicio. Guarda el espacio contenido en las propiedades de secuencia de comandos del proyecto de conector. Después de agregar claves, debería verse similar a lo siguiente en la IU de Apps Script:
    Guarda claves de cuentas de servicio en Propiedades de secuencia de comandos
  4. Incluye la biblioteca de OAuth2 para Apps Script en tu proyecto de Apps Script.
  5. Implementa el código de OAuth2 necesario para la cuenta de servicio:
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'
   
]);
}

Implementa código para leer y escribir desde Firebase

Usarás la API de REST de Firebase Database para leer y escribir en una instancia de Firebase Realtime Database El siguiente código implementa los métodos necesarios para antes de acceder a esta API.

Cómo implementar getData()

La estructura de tu código getData() existente sin almacenamiento en caché debería verse así:

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
 
};
}

Para usar el almacenamiento en caché en tu código getData(), sigue estos pasos:

  1. Determina el "fragmento" o 'unidad' de datos que deben almacenarse en caché.
  2. Crea una clave única para almacenar la unidad mínima de datos en la caché.
    Para la implementación de ejemplo, se usa zipcode de configparams como la clave.
    Opcional: Para la caché por usuario, crea una clave compuesta con la clave base y identidad del usuario. Ejemplo de implementación:
    js var baseKey = getBaseKey(request); var userEmail = Session.getEffectiveUser().getEmail(); var hasheduserEmail = getHashedValue(userEmail); var compositeKey = baseKey + hasheduserEmail;

  3. Si existen datos almacenados en caché, verifica si la caché está actualizada.
    En el ejemplo, los datos almacenados en caché para un código postal específico se guardan con el fecha actual. Cuando se recuperan datos de la caché, la fecha de esta es con la fecha actual.

    var cacheForZipcode = {
      data
    : <data being cached>,
      ymd
    : <current date in YYYYMMDD format>
    }
  4. Si los datos almacenados en caché no existen o no están actualizados, recupera los datos. de la fuente y almacenarlos en caché.

En el siguiente ejemplo, main.js incluye código getData() con almacenamiento en caché. cuando se implementa un plan.

Ejemplo de código

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'
   
]);
}

Recursos adicionales

El conector de Chrome UX facilita la creación de un panel basado en BigQuery de ~20 GB. a miles de usuarios. Este conector usa Firebase Realtime Database junto con el Servicio de caché de Apps Script para un enfoque de almacenamiento en caché de dos capas. Consulta code para conocer los detalles de la implementación.