GoogleDrive+GASを使ってマスターデータの環境ごとにデータを分けて保存・取得する

初めに

個人等でゲーム開発を行う場合、開発初期時のなどはなるべくお金をかけない開発環境を作ることを求めるケースが多々あります そこで今回は Google DriveとGAS(Google App Script)を使ったマスターデータの管理方法について実際に行ってみます

求める要件

  • マスターデータは Google Sheetで管理して、いくつかのシートに散らばっている
  • 各環境ごとにマスターデータをデプロイして区別をしたい
    • dev環境
    • qa環境
    • product環境 ...
  • 外部(イメージはUnityなど)から指定した環境のマスターデータを取得した

マスターデータの準備

まずは以下のようなマスターデータを作成します。これらは複数のシートがあるものとします。(一つでも問題はないです)

test1 test2   test3_1 test4   test3   test4   test3_2 test5   test5   test6   test3_3 test6   test7   test8   test3_4
test1   2   -0.45   0   test2   -1.483333333    -2.483333333    -3.483333333    test3   -4.483333333    -5.483333333    -6.483333333    test4   -7.483333333    -8.483333333
test2   2   -0.45   0   test3   -1.483333333    -2.483333333    -3.483333333    test4   -4.483333333    -5.483333333    -6.483333333    test5   -7.483333333    -8.483333333
test3   2   -0.45   0   test4   -1.483333333    -2.483333333    -3.483333333    test5   -4.483333333    -5.483333333    -6.483333333    test6   -7.483333333    -8.483333333

イメージは以下のような感じです

GoogleDriveに環境ごとにCSVとバイナリーで保存

以下のようにmenuから各環境ごとに保存できるようにします。

実行すると以下のように環境ごとのフォルダにbinとcsvが保存されます

以下がCSVとバイナリーに保存する処理のコードです

/**
 * スプレッドシートのデータをCSVおよびバイナリ形式で保存し、指定された環境フォルダに配置する
 * @param {string} environment - 保存する環境名(例: 'product', 'qa', 'dev1', 'dev2', 'dev3')
 */
function convertSheetToCSVAndBinary(environment) {
  // アクティブなスプレッドシートとシートを取得
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = spreadsheet.getActiveSheet();

  // シート内のデータ範囲を取得
  var dataRange = sheet.getDataRange();

  // データ範囲内の全データを取得
  var data = dataRange.getValues();

  // データをCSV形式の文字列に変換
  var csv = data.map(function(row) {
    return row.map(function(cell) {
      // セル内のダブルクォーテーションをエスケープ
      return '"' + String(cell).replace(/"/g, '""') + '"';
    }).join(',');
  }).join('\n');

  // CSV文字列をBlobに変換
  var csvBlob = Utilities.newBlob(csv, MimeType.CSV);

  // データをJSON文字列に変換
  var jsonString = JSON.stringify(data);
  // JSON文字列をBlobに変換
  var binaryBlob = Utilities.newBlob(jsonString, MimeType.PLAIN_TEXT);

  // フォルダIDを指定
  var rootFolderId = 'your google drive id';
  var rootFolder = DriveApp.getFolderById(rootFolderId);

  // "masterdata" フォルダを取得または作成
  var masterdataFolder = getOrCreateFolder(rootFolder, 'masterdata');

  // 環境フォルダを取得または作成
  var environmentFolder = getOrCreateFolder(masterdataFolder, environment);

  // ファイル名を "masterdata-環境名" に設定
  var baseName = 'masterdata-' + environment;

  // CSVファイルの処理
  csvBlob.setName(baseName + '.csv');
  var existingCsvFile = getFileByName(environmentFolder, baseName + '.csv');
  if (existingCsvFile) {
    existingCsvFile.setTrashed(true); // 既存のファイルを削除
  }
  environmentFolder.createFile(csvBlob);

  // バイナリファイルの処理
  binaryBlob.setName(baseName + '.bin');
  var existingBinFile = getFileByName(environmentFolder, baseName + '.bin');
  if (existingBinFile) {
    existingBinFile.setTrashed(true); // 既存のファイルを削除
  }
  environmentFolder.createFile(binaryBlob);

  // 完了メッセージを表示
  SpreadsheetApp.getUi().alert('スプレッドシートのデータを ' + environment + ' 環境にデプロイしました。');
}

/**
 * 指定したフォルダ内で、指定した名前のサブフォルダを取得する。存在しない場合は作成する。
 * @param {Folder} parentFolder - 親フォルダ
 * @param {string} subFolderName - サブフォルダ名
 * @return {Folder} - 取得または作成したサブフォルダ
 */
function getOrCreateFolder(parentFolder, subFolderName) {
  var subFolders = parentFolder.getFoldersByName(subFolderName);
  if (subFolders.hasNext()) {
    return subFolders.next();
  } else {
    return parentFolder.createFolder(subFolderName);
  }
}

/**
 * 指定したフォルダ内で、指定した名前のファイルを取得する
 * @param {Folder} folder - 検索対象のフォルダ
 * @param {string} fileName - 検索するファイル名
 * @return {File|null} - 見つかったファイル、またはnull
 */
function getFileByName(folder, fileName) {
  var files = folder.getFilesByName(fileName);
  return files.hasNext() ? files.next() : null;
}

また以下がGoogle Sheetの上部にメニュとして表示する機能です

/**
 * スプレッドシートを開いたときにカスタムメニューを追加する
 */
function onOpen() {
  var ui = SpreadsheetApp.getUi();
  ui.createMenu('マスターデータをデプロイ')
    .addItem('product 環境にデプロイ', 'deployToProduct')
    .addItem('qa 環境にデプロイ', 'deployToQA')
    .addItem('dev1 環境にデプロイ', 'deployToDev1')
    .addItem('dev2 環境にデプロイ', 'deployToDev2')
    .addItem('dev3 環境にデプロイ', 'deployToDev3')
    .addToUi();
}

/**
 * product 環境にデプロイ
 */
function deployToProduct() {
  convertSheetToCSVAndBinary('product');
}

/**
 * qa 環境にデプロイ
 */
function deployToQA() {
  convertSheetToCSVAndBinary('qa');
}

/**
 * dev1 環境にデプロイ
 */
function deployToDev1() {
  convertSheetToCSVAndBinary('dev1');
}

/**
 * dev2 環境にデプロイ
 */
function deployToDev2() {
  convertSheetToCSVAndBinary('dev2');
}

/**
 * dev3 環境にデプロイ
 */
function deployToDev3() {
  convertSheetToCSVAndBinary('dev3');
}

GAS経由ででデータを取得するAPIを作成

外部から環境を指定したマスターデータを取得するために以下のようなget関数を定義します。

doGet 関数 API ドキュメント

doGet 関数は、指定された環境とデータ形式に対応するファイルを取得し、その内容をレスポンスとして返します。

エンドポイント

GET https://script.google.com/macros/s/あなたのスクリプトID/exec

パラメータ

パラメータ名 必須 種類 説明
environment はい クエリ 取得する環境名。productqadev1dev2dev3 のいずれかを指定します。
dataType はい クエリ 取得するデータの形式。bin(バイナリデータ)または csvCSVデータ)を指定します。

リクエスト例

GET https://script.google.com/macros/s/あなたのスクリプトID/exec?environment=dev1&dataType=bin

レスポンス

  • 成功時 (200 OK):

    • 指定された環境とデータ形式に対応するファイルの内容を返します。
    • MIMEタイプは、dataTypebin の場合は text/plaincsv の場合は text/csv となります。
  • エラー時:

    • 不足しているパラメータや無効な dataType が指定された場合、エラーメッセージを含むテキストを返します。
    • 例:
      • Missing parameter: environment or dataType.
      • Invalid dataType parameter. Use "bin" or "csv".
      • File not found.

注意事項

  • environment パラメータには、事前に設定された環境名を正確に指定してください。
  • dataType パラメータは、bin または csv のいずれかを指定してください。大文字・小文字を区別します。
  • エンドポイントURLの あなたのスクリプトID 部分は、GASプロジェクトのデプロイ時に発行されるスクリプトIDに置き換えてください。

GET関数

実際にget関数は以下になります。

/**
 * HTTP GETリクエストを処理し、指定された環境とデータ形式に対応するファイルを返す
 * @param {Object} e - HTTPリクエストオブジェクト
 * @return {ContentService.Output} - ファイルデータのレスポンス
 */
function doGet(e) {
  // リクエストパラメータから環境名とデータ形式を取得
  var environment = e.parameter.environment;
  var dataType = e.parameter.dataType;

  // パラメータのバリデーション
  if (!environment || !dataType) {
    return ContentService.createTextOutput('Missing parameter: environment or dataType.')
      .setMimeType(ContentService.MimeType.TEXT);
  }

  // データ形式のバリデーション
  if (dataType !== 'bin' && dataType !== 'csv') {
    return ContentService.createTextOutput('Invalid dataType parameter. Use "bin" or "csv".')
      .setMimeType(ContentService.MimeType.TEXT);
  }

  // ルートフォルダIDを指定
  var rootFolderId = 'your google folder id';
  var rootFolder = DriveApp.getFolderById(rootFolderId);

  // "masterdata" フォルダを取得
  var masterdataFolder = getSubFolderByName(rootFolder, 'masterdata');
  if (!masterdataFolder) {
    return ContentService.createTextOutput('Masterdata folder not found.')
      .setMimeType(ContentService.MimeType.TEXT);
  }

  // 環境フォルダを取得
  var environmentFolder = getSubFolderByName(masterdataFolder, environment);
  if (!environmentFolder) {
    return ContentService.createTextOutput('Environment folder not found.')
      .setMimeType(ContentService.MimeType.TEXT);
  }

  // ファイル名を設定
  var fileName = 'masterdata-' + environment + '.' + dataType;
  var file = getFileByName(environmentFolder, fileName);
  if (!file) {
    return ContentService.createTextOutput('File not found.')
      .setMimeType(ContentService.MimeType.TEXT);
  }

  // ファイルデータをレスポンスとして返す
  var mimeType = dataType === 'bin' ? ContentService.MimeType.TEXT : ContentService.MimeType.CSV;
  return ContentService.createTextOutput(file.getBlob().getDataAsString())
    .setMimeType(mimeType);
}

/**
 * 指定した親フォルダ内で、指定した名前のサブフォルダを取得する
 * @param {Folder} parentFolder - 親フォルダ
 * @param {string} subFolderName - サブフォルダ名
 * @return {Folder|null} - 見つかったサブフォルダ、またはnull
 */
function getSubFolderByName(parentFolder, subFolderName) {
  var subFolders = parentFolder.getFoldersByName(subFolderName);
  return subFolders.hasNext() ? subFolders.next() : null;
}

/**
 * 指定したフォルダ内で、指定した名前のファイルを取得する
 * @param {Folder} folder - 検索対象のフォルダ
 * @param {string} fileName - 検索するファイル名
 * @return {File|null} - 見つかったファイル、またはnull
 */
function getFileByName(folder, fileName) {
  var files = folder.getFilesByName(fileName);
  return files.hasNext() ? files.next() : null;
}

テストコード

実際に動くのかをテストコードで試します(GAS上のテストコードです)

/**
 * doGet関数のテストを実行する
 */
function testDoGet() {
  // テストする環境とデータタイプの組み合わせ
  var testCases = [
    { environment: 'dev1', dataType: 'bin' },
    { environment: 'dev1', dataType: 'csv' }
  ];

  // 各テストケースを実行
  testCases.forEach(function(testCase) {
    // モックのリクエストオブジェクトを作成
    var e = {
      parameter: {
        environment: testCase.environment,
        dataType: testCase.dataType
      }
    };

    // doGet関数を呼び出し
    var result = doGet(e);

    // 結果をログに出力
    Logger.log('Environment: ' + testCase.environment + ', DataType: ' + testCase.dataType);
    Logger.log('Response Content: ' + result.getContent());
    Logger.log('Response MimeType: ' + result.getMimeType());
  });
}

これを実行すると以下のようなログが出ています。ログ上は問題なさそうです

GASの制限

以下のドキュメントに記載の通り一定の制限はあります。規模が大きくなったり開発工数が取れる場合は,GCPAWSに移行しましょう

機能 制限
スクリプトの実行時間 6 分 / 実行
カスタム関数の実行時間 30 秒 / 実行
トリガーの合計実行時間 90 分 / 日
同時実行数 30 / ユーザー
メールの添付ファイルの総サイズ 25 MB / メッセージ
メール添付ファイル数 250 件 / メッセージ
メール本文のサイズ 200 KB / メッセージ
1 通あたりのメール受信者数 50 人 / メッセージ
1 日あたりのメール受信者数 100 人 / 日
プロパティ値のサイズ 9 KB / 値
プロパティの合計保存容量 500 KB / プロパティ ストア
トリガー数 20 / ユーザー / スクリプト
URL Fetch 呼び出し数 20,000 / 日
URL Fetch レスポンスサイズ 50 MB / 呼び出し
URL Fetch ヘッダー数 100 / 呼び出し
URL Fetch ヘッダーサイズ 8 KB / 呼び出し
URL Fetch POST サイズ 50 MB / 呼び出し
URL Fetch URL の長さ 2 KB / 呼び出し
作成可能なプロジェクト数 50 / 日

developers.google.com