初めに
qiita投稿2度目ですが、またラッピング系統の記事です。
シートを増やすたびにgetSheetByName()を書いたり、変数を増やしたりするのが面倒だったので作成しました。
また、後述しますが、関数が複数乱立すると「同じシートを2度読み込まない」が達成しづらく、全てグローバル変数に入れると「(主にトリガー発火時)不要なシートを読み込まない」が達成できないので、その辺も解決できるようにしました。
結論
このコードをコピペしとけばとりあえず動きます。
ソースコード
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheets = (function(ss){
const handler = {
get(target , prop){
if(target[prop]) return target[prop];
const sheet = ss.getSheetByName(prop);
target[prop] = sheet;
return sheet;
}
}
return new Proxy({},handler);
})(ss);
使用方法
//通常の取得方法
const mondaySheet = ss.getSheetByName("monday");
//今回の方法
const mondaySheet = sheets.monday;
//これでもOK①
const mondaySheet = sheets["monday"];
//これでもOK②
const { monday } = sheets;
アピールポイント
① 記述量減るよ!!!
今までgetSheetByName()といちいち書いていたのを、単なるプロパティアクセスで終わらせられます。
タイピングの量だけでなく、コードが横に短くなるので、若干見やすくなります。やったね!
② 変数名減るよ!!!
シート名が日本語の場合、変数名をいちいち考えるのが面倒くさかったです。(私怨)
頑張って英訳する必要ないですし、変数名を減らせるので、後から読み返しやすくなります。
ここからのポイントは、すぐ下のコードのように、単にオブジェクトにしただけとは違うよという話です。
const sheets = {
sheet1(){
return ss.getSheetByName("sheet1");
},
sheet2(){
return ss.getSheetByName("sheet2");
}
}
③ 重複して読み込むことはないよ!!!
ss.getSheetByName("sheet1").crearContent();
//色々処理して
ss.getSheetByName("sheet1").appendRow([1]);
このように書いたら、getSheetByName()が2度も呼び出されていて無駄ですよね。
単にオブジェクトにするだけでも、これと同じような動作をします。
そういう挙動をしないよってことです。
//2度呼び出してるけど、getSheetByName()の処理は一度のみ
sheets.sheet1.clearContent(); //ここでgetSheetByNameを呼び出す => 保存
sheets.sheet1.appendRow([1]); //ここではgetSheetByNameを呼び出さない。
④ コピペだけでOK!
一番最後に持ってきましたが、正直これが一番大きいです。
オブジェクトで作ろうとすると、シートが多くなるたびにメソッドを追加しなきゃいけないので...
const sheets = {
sheet1(){
return ss.getSheetByName("sheet1");
},
sheet2(){
return ss.getSheetByName("sheet2");
}
//sheet3を追加したら、ここにもメソッドを入れなきゃいけない...
}
そんなことしなくても、勝手に持ってきてくれるのでご安心を。
経緯
経緯というか、作成までの過程です。
1.スコープ管理の難しさ
【要点】
・重複して読み込まないように、できるだけグローバルスコープで管理したいよね
・でもグローバルだと、使わない変数も一括して読み込んじゃうよね。
・じゃあ、関数スコープで管理しよう!
・でもそれだと、重複して読みこまないように気を使う必要があり、超めんどくさくない?
【内容】
ふつう、シートを取得するときは、グローバルスコープに入れます。
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName("sheetname");
function main(){}
なぜなら、複数の関数で同じシートを使用する場合に、重複して読み込むことを防ぐためです。
const ss = SpreadsheetApp.getActiveSpreadsheet();
function main(){
const sheet = ss.getSheetByName("sheetname");
sub();
//色々作業
}
function sub(){
const sheet = ss.getSheetByName("sheetname");
//作業
}
関数を複数跨ぐ場合に、シートの取得を二度手間にしないように、グローバルに入れておく必要があるわけですね。(引数に渡すのも一案ですが、引数が長くなったり、デバッグを面倒にしたりするので割愛します。)
兎にも角にも、このように宣言することが一般的であることは納得していただけると思います。
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName("sheetname");
function main(){}
もちろんこれでもいいのですが、シートが大量にあったらどうしましょうか。
const idSheet = ss.getSheetByName("id");
const dataSheet = ss.getSheetByName("data");
//グローバル変数多すぎ問題
シート毎に変数を作っていては、グローバル変数が増えてしまいます。
ただしGASでは、グローバル変数が増えるということよりも、シートの読み込み回数が増えることの方が深刻でしょう。
例えば、次のようなケースではいかがでしょうか。
const mondaySheet = ss.getSheetByName("monday");
const tuesdaySheet = ss.getSheetByName("tuesday");
function onMonday(){
const values = mondaySheet.getDataRange().getValues();
//tuesdaySheetは使用しない
}
function onTuesday(){
const values = tuesdaySheet.getDataRange().getValues();
//mondaySheetは使用しない
}
//...続く...
当然ながら、使用しないシートを読み込む必要はありませんし、時間のロスとなるため、むしろ避けるべきです。
よって、変数をグローバルから関数スコープに制限する必要があります。
function onMonday(){
const mondaySheet = ss.getSheetByName("monday");
const values = mondaySheet.getDataRange().getValues();
//tuesdaySheetは使用しない
}
function onTuesday(){
const tuesdaySheet = ss.getSheetByName("tuesday");
const values = tuesdaySheet.getDataRange().getValues();
//mondaySheetは使用しない
}
これで速くなったね!めでたしめでたし...とはなりません。
実際に使用する際はこんなに単純なコードになることは珍しく、実際には色々な関数を行ったり来たりしますから、超面倒臭いです。
(そのため、やはりグローバルに入れておくことが有用であることは、先述の通りです。)
結局、グローバルスコープも関数スコープも一長一短で、どっちを取っても問題は残り続けます。
2.オブジェクトの限界
【要点】
・結局、重複して読み込んだり、必要以上に読み込んだりしなければいいんだよね
・ということは、必要に応じて読み込めるように、関数にすればいいよね
・でもそれだと、重複して読み込んじゃうかもよ?
・最初の呼び出しで取得 & 保存、2度目以降は取得せずに、保存したものを返せばいいんじゃないかな?
・なるほどっっ!!!!!!!...いや、シート毎に関数作るのめんど...
【内容】
関数しておけば不要なシートまで読み込む心配はないので、一番サクッとしています。
function getSheet1(){
return ss.getSheetByName("sheet1");
}
function main(){
const sheet = getSheet1();
}
functoin sub(){//呼び出さなければ、読み込まれない}
しかし、立ちはだかるのは重複の壁。
function getSheet1(){
return ss.getSheetByName("sheet1");
}
function main(){
const sheet = getSheet1();
sub();
}
functoin sub(){
const sheet = getSheet1(); //2度目の呼び出し
}
そこで、「既に取得済みなら、そのデータを返す」にすればいいのでは??ということで、次のように変更します。
let sheet1;
function getSheet1(){
if(sheet1) return sheet1;
sheet1 = ss.getSheetByName("sheet1");
return sheet1;
}
function main(){
const sheet = getSheet1();
sub();
}
functoin sub(){
const sheet = getSheet1();
}
これで一応は、重複して呼び出されることも、必要以上に呼び出されることも無くなりました。
...しかし、グローバル変数や関数を定義しすぎていて、流石に見づらい。
というか、ちょい扱いづらいので、いい感じにするためにオブジェクトに直します。
const sheets = {
getSheet1(){
if(this.sheet1) return this.sheet1;
this.sheet1 = ss.getSheetByName("sheet1");
return this.sheet1;
}
}
const sheet1 = sheet.getSheet1();
単純にスコープを狭められたのと、オブジェクト指向っぽい設計になったのでカッコ良くなりました。
ただし、これは関数時代からあった話ですが、
「「「いちいち追加するのが面倒くさい」」」
機能的には十分だけど、シートが増えるたびにメソッドを増やすのはやだな...誰も使わんだろこれ...
3.Proxyの力を借りよう!
Proxyってなんぞ??って方は、MDNのページをご覧ください。
要は、「プロパティを定義していなくても、それっぽく動いてくれる」ということです。
は?って感じだと思うので、軽く説明します。
A. getter
オブジェクトのプロパティを読み込むときに働く関数を、ゲッターと言います。
プロパティを読み込むというのは、下でいうとobj[a]やobj.aのことです。
const obj = { a: 40 };
console.log(obj[a]);
console.log(obj.a);
で、普通のオブジェクトだと対応する値を返して終わりなのですが、Proxyはその処理をいじれます。
const target = {a : 40};
const handler {
get(target,prop){
console.log("target : ",target);
console.log("prop : ",prop);
return "Hello World";
}
}
const proxy = new Proxy(target,handler);
console.log(proxy.hogehoge);
// target : {a : 40}
// prop : hogehoge
// Hello World
こんな感じで、ゲッターをいじった結果、proxy.hogehogeが定義されていないのに、それっぽい(?)挙動をしてくれます。これを利用して、各シートごとのメソッドを定義しなくても、勝手にシートを取得してくれるようにしよう!という話です。
Proxyは初見殺し性能が高いので、そう易々と使わない方がいいです。