0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

スプレッドシートを簡易DBにするGASパターン集

0
Posted at

結論から先に: 複数のシートをDB代わりに使うなら、実装をAIに任せる前に「IDは必ずA列」「関数名を統一する」という共通インターフェースを自分で決めておくべきです。先に型を固定してから実装させると、シートが増えても同じ関数セットを使い回せます。

課題

個人開発の規模だと、わざわざデータベースを立てるほどでもないが、単純な配列やJSONファイルだけで管理するのも心もとない、というデータ量のタスクがよくある。スプレッドシートは手軽だが、CRUD処理を毎回その場で書いていると、書き方が微妙に違うシートがどんどん増えていった。

最初のうちは「今回だけの一時的な処理だから」と、その場しのぎでシートを触るコードを書いていた。ところがシートの数が増えるにつれ、似たようなコードを毎回微妙に違う形で書き直す羽目になり、どのシートにどの書き方をしたかを覚えていられなくなった。

完成形

今は、シートをテーブルに見立てたCRUD関数の型を1つ決めて、新しいシートを作るたびにその型をコピーして使っている。列の1行目をヘッダーとして扱い、IDで行を検索・更新・削除できる最小限の関数セットだ。

AIとの作り方

最初にAIへ「スプレッドシートをDB的に使う関数を書いて」と頼んだところ、確かに動くコードは出てきたが、シートごとに微妙に実装が違うものが量産された。あるシートはID列が1列目、別のシートは2列目、関数名もバラバラ、という具合だ。

そこで、先に「共通インターフェース」を自分で決めてからAIに実装させる順番に変えた。具体的には「IDは必ずA列」「関数名はfindById/insertRow/updateById/deleteByIdに統一する」というルールを先に書き、それに沿って実装だけをAIに任せた。ルールを先に固定したことで、複数のシートに同じ関数セットをそのまま使い回せるようになった。

この「先にルールを決める、実装は後」という順番は、1週目で決めた役割分担の型そのものだった。改めて振り返ると、GASのコード設計にもこの型がそのまま応用できることに、作りながら気づいた。

サンプルコード

function findById(sheetName, id) {
  const sheet = SpreadsheetApp.getActive().getSheetByName(sheetName);
  const data = sheet.getDataRange().getValues();
  const rowIndex = data.findIndex(row => row[0] === id);
  return rowIndex === -1 ? null : data[rowIndex];
}

function insertRow(sheetName, rowValues) {
  const sheet = SpreadsheetApp.getActive().getSheetByName(sheetName);
  sheet.appendRow(rowValues);
}

function updateById(sheetName, id, newValues) {
  const sheet = SpreadsheetApp.getActive().getSheetByName(sheetName);
  const data = sheet.getDataRange().getValues();
  const rowIndex = data.findIndex(row => row[0] === id);
  if (rowIndex === -1) return false;
  sheet.getRange(rowIndex + 1, 1, 1, newValues.length).setValues([newValues]);
  return true;
}

function deleteById(sheetName, id) {
  const sheet = SpreadsheetApp.getActive().getSheetByName(sheetName);
  const data = sheet.getDataRange().getValues();
  const rowIndex = data.findIndex(row => row[0] === id);
  if (rowIndex === -1) return false;
  sheet.deleteRow(rowIndex + 1);
  return true;
}

複数シートで使い回すことを見越して、シート名を引数で渡す設計にしている。呼び出し側は次のように、対象シート名を指定するだけでよい。

const user = findById('users', 'u001');
insertRow('logs', ['l001', new Date(), 'ログイン']);

シート名と行構造さえ決めれば、この4関数をそのままコピーして使える。サンプルでは全行を毎回読み込んでいるため、行数が数千を超える規模になったらBigQuery移行(後日扱う予定)を検討した方がよい。

ハマりどころ

getDataRange().getValues()を関数ごとに毎回呼んでいたため、1回の処理で何度もシート全体を読み込んでしまい、実行が遅くなったことがあった。複数の関数をまとめて呼ぶ場合は、データを1回だけ読み込んで使い回すように直す必要がある。

特に、1つの処理の中でfindByIdを複数回呼ぶようなケースで顕著だった。呼ぶたびにシート全体を読み直していたので、行数が増えるにつれて体感できるほど遅くなった。データを1回読み込んでから、その場で必要な行を探す形に直したところ、体感速度がはっきり改善した。

もう1つ、IDの型が「数値」と「文字列」で混在してしまい、===比較が一致しないバグに何度か遭遇した。スプレッドシート側で数値として入力された値と、GAS側で文字列として生成したIDが一致しないのが原因だった。IDは必ず文字列に統一する、というルールを追加してから解消した。

function findById(sheetName, id) {
  const sheet = SpreadsheetApp.getActive().getSheetByName(sheetName);
  const data = sheet.getDataRange().getValues();
  const targetId = String(id); // 比較前に必ず文字列化する
  const rowIndex = data.findIndex(row => String(row[0]) === targetId);
  return rowIndex === -1 ? null : data[rowIndex];
}

比較の直前で両辺をString()で揃えるだけの単純な修正だが、この1行を入れ忘れたシートだけ検索がヒットしない、という不具合に何度か悩まされた。共通関数として1箇所にまとめておく価値を、身をもって実感した部分だ。

まとめ

シートをDB代わりに使うこと自体はよくある手法だが、「型を先に決めてからAIに実装させる」という順番を守らないと、シートの数だけ微妙に違う実装が増えていく。地味だが、複数シートを扱う段階になって初めて効いてくる工程だった。

前回は「GAS×Gemini APIで毎朝ニュース要約メールを届ける」について、次回は「GASトリガー設計と指数バックオフ再試行」について書く予定です。

note(作った経緯・所感はこちら): https://note.com/kar8

参考・関連リンク

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?