5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【GAS】CacheServiceをもっと簡単に使おう

Last updated at Posted at 2022-12-27

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関数を一度実行すれば、トリガーを勝手に作成してデータを保持します。

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?