Community

Google Sheetsを使ってデータディクショナリーを生成する

本記事は Generating a Data Dictionary in Google Sheets :us: の翻訳記事です

なぜGoogle Sheetsを使ったデータディクショナリーが必要なのですか?

Lookerを使うことで、より多くのユーザーがデータにアクセスしたり探索したりすることができるようになる一方で、多くのユーザーにとって、定義されたフィールドの意味や元になっているデータソースについてはわからなくなってしまうケースもあり、混乱して間違ったクエリーを発行してしまうことがあります。こうした混乱を防ぐ一つの方法として、Looker APIを使ってデータディクショナリーを定義し、Google Sheet上から簡単にアクセスできるフィールド情報を生成する方法があります。本記事ではその方法を記載します。以下のソリューションはview上で description パラメータをフィールドに定義している場合に特に便利です。

スクリプト

このスクリプトは Apps Script と Javascript の組み合わせで記述されており、こちらのリポジトリーにリストアップされている関数を使っています。

本スクリプトを使用する際の注意事項は以下の通りです:

  • model_nameexplore_name は大文字小文字を区別する必要があります
  • このスクリプトは、それぞれのViewの情報を独立したGoogle Sheetに作成します。それぞれのGoogle Sheetの名称はViewの名称となります(全ての情報を一つのシートに作成する場合は、以下の注を参照してその動作を強制する方法をご覧ください)。

このスクリプトでは、Exploreの各ビューへのGoogleシートマッピングを使用します。各Google Sheetには対応するビュー名が付けられ、そのビューに関連するデータのみがGoogle Sheetに表示されます。

また、このスクリプトはシートを開いた6時間後にAPIをコールしてアウトプットを出力します。

// ベースドメインをご自身のインスタンス名で置き換えてください
var BASE_URL = 'https://instance_name.looker.com:19999/api/3.1';
// API認証情報を置き換えてください
var CLIENT_ID = 'your_api_ID';
var CLIENT_SECRET = 'your_api_key';

function LOOKER_GET_DATA_DICTIONARY(model_name) {
  // 前回のキャッシュをチェックします
  var cache = CacheService.getScriptCache();
  var cached = cache.get("api_results");
  if (cached != null) {
    for (elem in cached) {
      results.push(elem)
      return results;
    }
  }
  // キャッシュがなければ呼び出しを実行します
  try {

    var options = {
      'method': 'get',
      'headers': {
        'Authorization': 'token ' + login()
      }
    };

    // エンドポイント /lookml_models/{lookml_model_name} に対するAPIコール
    var response = UrlFetchApp.fetch(BASE_URL + "/lookml_models/" + model_name, options);
    var explores = JSON.parse(response.getContentText()).explores;
    var result = [];

    // フィールド検索を定義
    result.push(["View Name", "Field Type", "Name", "Label", "Type", "Description", "Hidden"]);
                 // 必要に応じて追加の詳細情報を追記:
                 //, "SQL", "Source"]);

    for (var i = 0; len = explores.length, i < len; i++) {
      Logger.log(explores);

      var explore = explores[i].name;
      var explore_results = UrlFetchApp.fetch(BASE_URL + "/lookml_models/" + model_name + "/explores/" + explore, options);

      var connection = JSON.parse(explore_results.getContentText()).connection_name;
      var dimensions = JSON.parse(explore_results.getContentText()).fields.dimensions;
      var measures = JSON.parse(explore_results.getContentText()).fields.measures;
      var current_sheet = SpreadsheetApp.getActiveSheet().getName();

      // 特定のExploreに関連するデータのみusing this test to retrieve only data relevant to a specific explore
      // change explore_name with your explore
      if (explore == "explore_name") {

        // ディメンションのデータを追加する
        for (var j = 0; j < dimensions.length; j++) {
          // シートに表示されている名称と一致するビューから取得したフィールドのみかどうかをチェックする
          if (dimensions[j].view.replace("_", " ") == current_sheet.toLowerCase()|| dimensions[j].view_label.replace("_", " ").toLowerCase() == current_sheet.toLowerCase()) {
            result.push([dimensions[j].view,
              "Dimension",
              (dimensions[j].name.substring((dimensions[j].name.indexOf(".")+1), dimensions[j].name.length)).replace(/_/g, " "),
              (dimensions[j].label != null ? dimensions[j].label : (dimensions[j].name.substring((dimensions[j].name.indexOf(".")+1), dimensions[j].name.length).replace(/_/g, " "))),
              (dimensions[j].type != null ? (dimensions[j].type).replace("_", " ") : "String"),
              dimensions[j].description,
              dimensions[j].hidden, dimensions[j].view_label
              //, (dimensions[j].sql != null ? dimensions[j].sql : ""),
              //dimensions[j].source_file
            ]);
          }
        }

        // メジャーのデータを追加する
        for (var k = 0; k < measures.length; k++) {
          // シートに表示されている名称と一致するビューから取得したフィールドのみかどうかをチェックする
          if (measures[k].view.replace("_", " ") == current_sheet.toLowerCase() || measures[k].view_label.replace("_", " ").toLowerCase() == current_sheet.toLowerCase()) {
            result.push([measures[k].view,
              "Measure",
              (measures[k].name.substring((measures[k].name.indexOf(".")+1), measures[k].name.length).replace(/_/g, " ")),
              (measures[k].label != null ? measures[k].label : (measures[k].name.substring((measures[k].name.indexOf(".")+1), measures[k].name.length)).replace(/_/g, " ")),
              (measures[k].type != null ? (measures[k].type).replace("_", " ") : "String"),
              measures[k].description,
              measures[k].hidden
              //, (measures[k].sql != null ? measures[k].sql : ""),
              //measures[k].source_file
            ]);
          }
        }
      }
    }
    cache.put("api_results", result, 21600); // cache for 6 hours
    // キャッシュ最大値は6時間(21,600秒)
    // デフォルトは10分 (600秒)
    return result
  } catch(err) {
    return "Something went wrong. " + err
  }
}

function login() {
  try {
    var post = {
      'method': 'post'
    };
    var response = UrlFetchApp.fetch(BASE_URL + "/login?client_id=" + CLIENT_ID + "&client_secret=" + CLIENT_SECRET, post);
    return JSON.parse(response.getContentText()).access_token;
  } catch(err) {
    Logger.log(err);
    return "Could not login to Looker. Check your credentials.";
  }
}

一つのシートに全ての出力をまとめる方法

上記の通り、このスクリプトはビューごとに別のGoogleシートに出力します。もし一つのシートにまとめて出力したい場合は、上記のスクリプトから以下のセクションを削除してください。

if (dimensions[j].view.replace("_", " ") == current_sheet.toLowerCase()|| dimensions[j].view_label.replace("_", " ").toLowerCase() == current_sheet.toLowerCase())

if (measures[k].view.replace("_", " ") == current_sheet.toLowerCase() || measures[k].view_label.replace("_", " ").toLowerCase() == current_sheet.toLowerCase())

追記

追記 2020/01/28

sql_table_name も合わせて取得する際は、LOOKER_GET_DATA_DICTIONARY 関数を以下のように変更してください。

function LOOKER_GET_DATA_DICTIONARY(model_name) {
    try {
        var options = {
            "method": "get",
            "headers": {
                "Authorization": "token " + login()
            }
        };
        var response = UrlFetchApp.fetch(BASE_URL + "/lookml_models/" + model_name, options);
        var explores = JSON.parse(response.getContentText()).explores;
        var result = [];

        // push header row first
        result.push(["Connection", "SQL Table Name", "Explore Name", "View Name", "Field Type", "Name", "Label", "Type",
            "Description", "Hidden", "SQL", "Source"
        ]);
      
        // for explore in explores
        for (var i = 0; len = explores.length, i < len; i++) {
            var explore = explores[i].name;
            // get the explore
            var explore_results = UrlFetchApp.fetch(BASE_URL + "/lookml_models/" + model_name + "/explores/" +
                explore, options);
          
            // get sql_table_name
            var sql_table_name = JSON.parse(explore_results.getContentText()).sql_table_name;

            // get connection, dimensions, measures on the explore
            var connection = JSON.parse(explore_results.getContentText()).connection_name;
            var dimensions = JSON.parse(explore_results.getContentText()).fields.dimensions;
            var measures = JSON.parse(explore_results.getContentText()).fields.measures;

            // for dimension in explore, add dimension to results
            for (var j = 0; j < dimensions.length; j++) {
                result.push([connection, sql_table_name, explore, dimensions[j].view, "Dimension",
                    dimensions[j].name, dimensions[j].label, dimensions[j].type,
                    dimensions[j].description, "hidden: " + dimensions[j].hidden, (dimensions[j].sql != null ?
                        dimensions[j].sql : ""), dimensions[j].source_file
                ]);
            }

            // for measure in explore, add measure to results
            for (var k = 0; k < measures.length; k++) {
                result.push([connection, sql_table_name, explore, measures[k].view, "Measure", measures[k].name,
                    measures[k].label, measures[k].type, measures[k].description, "hidden: " + measures[k].hidden,
                    (measures[k].sql != null ? measures[k].sql : ""),
                    measures[k].source_file
                ]);
            }
        }
        return result
    } catch (err) {
        return "Uh oh! Something went wrong. Check your API credentials and if you're passing the correct parameters and that your model exists!"
    }
}