Mettre en cache avec Firebase

Looker Studio dispose de son propre système de mise en cache pour les rapports. Lorsque vous créez votre vous pouvez implémenter un cache personnalisé pour générer plus rapidement des rapports évitez les taux TAEG.

Par exemple, vous créez un connecteur qui fournit des données météorologiques historiques. au cours des sept derniers jours pour un code postal spécifique. Votre connecteur va bientôt commencer populaire, mais l'API externe à partir de laquelle vous récupérez les données a un taux strict des limites. L'API ne met à jour ses données que quotidiennement. Par conséquent, pour un code postal spécifique, n'a pas besoin d'extraire les mêmes données plusieurs fois au cours d'une même journée. Utilisation vous pouvez mettre en œuvre un cache quotidien pour chaque code postal.

Conditions requises

  • Une base de données Firebase Realtime Si vous n'en avez pas, créez un projet Google Cloud Platform (GCP) et suivez le Guide de démarrage pour créer votre propre Firebase Instance Realtime Database.
  • Un compte de service GCP pour lire et écrire des données à partir de Firebase Realtime base de données.
  • Un connecteur de communauté qui récupère des données à partir d'une source.

Limites

  • Cette solution ne peut pas être utilisée avec les services avancés Looker Studio. Quand ? vous utilisez les services avancés de Looker Studio, votre code de connecteur dans les applications. Le script n'a pas accès aux données. Vous ne pouvez donc pas mettre en cache les données à l'aide d'Apps Script.
  • Les lecteurs et les éditeurs du rapport ne peuvent pas réinitialiser ce cache spécifique.

Solution

Implémenter un compte de service

  1. Créez un compte de service dans votre projet Google Cloud.
  2. Assurez-vous que ce compte de service dispose d'un accès BigQuery dans le projet Cloud.
    • Rôles IAM (Identity and Access Management) requis: Firebase Admin
  3. Téléchargez le fichier JSON pour obtenir les clés de comptes de service. Stockez le fichier dans les propriétés de script de votre projet de connecteur. Après avoir ajouté doivent ressembler à ceci dans l'interface utilisateur d'Apps Script:
    Enregistrer des clés de compte de service dans les propriétés de script
  4. Incluez la bibliothèque OAuth2 for Apps Script dans votre projet Apps Script.
  5. Implémentez le code OAuth2 requis pour le compte de service:
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'
   
]);
}

Implémenter du code pour lire et écrire des données depuis Firebase

Vous utiliserez l'API REST Firebase Database pour lire et écrire des données dans un projet Firebase Realtime Database. Le code suivant implémente les méthodes nécessaires pour qui accèdent à cette API.

Implémenter getData()

La structure de votre code getData() existant sans mise en cache doit se présenter comme suit : comme ceci:

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

Pour utiliser la mise en cache dans votre code getData(), procédez comme suit:

  1. Déterminer le fragment ou 'unité' de données à mettre en cache.
  2. Créez une clé unique pour stocker l'unité minimale de données dans le cache.
    Pour l'exemple d'implémentation, zipcode de configparams est utilisé. comme clé.
    Facultatif: Pour le cache par utilisateur, créez une clé composite avec la clé de base et identité de l'utilisateur. Exemple d'implémentation:
    js var baseKey = getBaseKey(request); var userEmail = Session.getEffectiveUser().getEmail(); var hasheduserEmail = getHashedValue(userEmail); var compositeKey = baseKey + hasheduserEmail;

  3. Si des données mises en cache existent, vérifiez si le cache est à jour.
    Dans cet exemple, les données mises en cache d'un code postal spécifique sont enregistrées à l'aide de l'attribut la date actuelle. Lorsque les données sont extraites du cache, la date du cache est par rapport à la date du jour.

    var cacheForZipcode = {
      data
    : <data being cached>,
      ymd
    : <current date in YYYYMMDD format>
    }
  4. Si les données mises en cache n'existent pas ou si les données mises en cache ne sont pas récentes, récupérez les données depuis la source et les stocker dans le cache.

Dans l'exemple suivant, main.js inclut du code getData() avec mise en cache. mise en œuvre.

Exemple de code

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

Ressources supplémentaires

Le connecteur d'expérience utilisateur Chrome permet d'obtenir un tableau de bord basé sur un volume BigQuery d'environ 20 Go à des milliers d'utilisateurs. Ce connecteur utilise Firebase Realtime Database en plus du service de cache Apps Script pour une mise en cache à deux couches. Voir code pour obtenir plus d'informations sur l'implémentation.