GASでデータを保存する場所は、時折頭を悩ませます。
Webアプリを作成しようとすると、
GSSは、速度に難あり。
ScriptPropatiesサービスは、容量に難あり。
ということで、両方の欠点を補えるCacheサービスをラッピングしました。
CacheServiceの問題点
優れているとは言ったものの、致命的な問題もあります。
・MAX6時間までしか保存できない
GSSやScriptPropatiesは半永久的に保存しておいてくれるのに、CacheServiceは6時間が限界らしいです。おい。
・「全てのキーを取得」がない
じゃ、じゃあ、6時間毎に全てのデータを保存し直せばいいじゃないか(汗
と思ってCacheServiceの公式ドキュメントを読んでみると、全てのキー一覧・データを取得する関数はサポートされていません。ScriptPropatiesならあるのに。
なんでやねん。
ソースコード
上記の問題点を踏まえ、ラップ以外にも処理を追加しました。
const Cache = (function(){
const ScriptCache = CacheService.getScriptCache();
const parseJSON = str => {
try{
return JSON.parse(str);
}catch{
return str;
}
};
return {
/**
* @return {array} キャッシュに登録されている全てのキーを取得します。[key1,key2,key3,...]
*/
getKeys(){
return this.require("ALL_KEYS_OF_THIS");
},
/**
* キーからデータを取得。
* @param {string} 取得したいデータのキー。(省略可)
* @param {array} 取得したいデータのキー一覧。(省略可)
* @return {object} キーに対応するデータ。省略した場合は、キャッシュされている全てのデータ。
*/
require(key){
let all;
if(typeof key === "string"){
return parseJSON(ScriptCache.get(key));
}else if(Array.isArray(key)){
all = ScriptCache.getAll(key);
}else if(!key){
all = ScriptCache.getAll(this.getKeys());
}
const obj = {};
for(const key in all){
obj[key] = parseJSON(all[key]);
}
return obj;
},
/**
* データを追加。
*
* @param {string} key キー名
* @param {object} key {key,value}の形にすること。
* @param {} value 型はなんでも良い。(省略可)
*/
exports(key,value){
//""をキー名として許可すると、this.require()で区別がつかない。
if(!key) throw new Error("第一引数は必須です。");
if(key === "ALL_KEYS_OF_THIS") throw new Error("禁止ワードです。");
//日付をJSONに変換する時の処理。デフォルトだと扱いにくいので上書き。
Date.prototype.toJSON = function(){
return Utilities.formatDate(this,"JST","yyyy-MM-dd hh:mm:ss");
}
if(typeof key === "string"){
const keys = this.require("ALL_KEYS_OF_THIS") || [];
ScriptCache.put(key,JSON.stringify(value),21600);
keys.push(key);
ScriptCache.put("ALL_KEYS_OF_THIS",JSON.stringify(keys),21600);
}else if(typeof key === "object"){
const keys = this.require("ALL_KEYS_OF_THIS") || [];
for (let i in key){
key[i] = JSON.stringify(key[i]);
keys.push(i);
}
ScriptCache.put("ALL_KEYS_OF_THIS",JSON.stringify(keys),21600)
ScriptCache.putAll(key,21600);
}
return this;
},
/**
* データの削除。
* @param {string} key 削除したいデータのキー名(省略可)
* @param {array} key 削除したいデータの全てのキー名(省略可)
* @param {} key 省略した場合、全てのデータを削除。
*/
remove(key){
if(typeof key==="string"){
ScriptCache.remove(key);
const keys = this.require("ALL_KEYS_OF_THIS") || [];
this.exports("ALL_KEYS_OF_THIS",keys.filter(k => k!==key));
}else if(Array.isArray(key)){
ScriptCache.removeAll(key);
const keys = this.require("ALL_KEYS_OF_THIS") || [];
this.exports("ALL_KEYS_OF_THIS",keys.filter(k => !keys.includes(key)));
}else if(!key){
ScriptCache.removeAll(this.getKeys());
this.exports("ALL_KEYS_OF_THIS",[]);
}
return this;
}
}
})();
/**
* キャッシュを持続するための関数。
* 4時間に1度トリガーを回すことを推奨します。
*/
function conserveData(){
const data = Cache.require();
Cache.exports(data);
const TRIGGER_NAME = "conserveData";
const triggers = ScriptApp.getScriptTriggers();
if(triggers.some(v => v.getHandlerFunction() === TRIGGER_NAME)) return ;
ScriptApp.newTrigger(TRIGGER_NAME)
.timeBased()
.everyHours(4)
.create();
}
解説
①日付の処理
Date.prototype.toJSON = function(){
return Utilities.formatDate(this,"JST","yyyy-MM-dd hh:mm:ss");
}
プロトタイプを汚染しています。よくないです。でもGASなので許してください。
というのも、Date型の標準のtoJSON関数は、かなり扱いづらいんですよね。
const now = new Date();
const now_json = JSON.stringify(now);
console.log(now_json); //"2022-12-27T13:52:56.140Z"
console.log(new Date(now_json)); //Invalid Date
このように、JSONにはできるけど元に戻せない...ので、やむを得ず上書きしました。
②キー一覧の作成
おそらく最も重要です。
Cacheに追加するたびに、内部でキー一覧を勝手に追加しています。
const keys = this.require("ALL_KEY_OF_THIS") || [];
ScriptCache.put(key,JSON.stringify(value),21600);
keys.push(key);
ScriptCache.put("ALL_KEY_OF_THIS",JSON.stringify(keys),21600);
ALL_KEY_OF_THISプロパティを用意することで、キー一覧を管理しています。ちなみに、
Cache.getKeys();
で、キー一覧を取得できます。また、全ての値を取得するときには、
Cache.require(Cache.getKeys());
Cache.require(); //引数を省略すると、全て取得。
このどちらかで行なってください。推奨は下です。
③データの保持
CacheServieは、最大で6時間しかデータを保持してくれません。 そのため、数時間おきに保存し直すことで、データの保持を持続させます。
/**
* キャッシュを持続するための関数。
* 4時間に1度トリガーを回すことを推奨します。
*/
function conserveData(){
const data = Cache.require();
Cache.exports(data);
const TRIGGER_NAME = "conserveData";
const triggers = ScriptApp.getScriptTriggers();
if(triggers.some(v => v.getHandlerFunction() === TRIGGER_NAME)) return ;
ScriptApp.newTrigger(TRIGGER_NAME)
.timeBased()
.everyHours(4)
.create();
}
これを実行すると、4時間毎にキャッシュを保存し直します。
最初は6時間だとギリギリ遅いので5時間にしようと思っていたのですが、GUI上では6時間の次は4時間でしか設定できかったので、ソースコードもそれに合わせました。
conserveData関数を一度実行すれば、トリガーを勝手に作成してデータを保持します。