メインコンテンツへスキップ
見出し画像
Photo bym316jp2

【第463回】 データエクステンションを Google スプレッドシートへ連携する

    今回の記事では、Marketing Cloud Engagement のデータエクステンションに格納されているレコードを、自動的に Google スプレッドシートへ連携する方法について解説します。

    Google スプレッドシートには Apps Script(Google Apps Script) という仕組みがあり、Automation Studio の「スクリプトアクティビティ」に近い形で処理を実装できます。

    この Apps Script から Marketing Cloud Engagement の REST API にアクセスすることで、データエクステンションのデータを取得し、スプレッドシートへ書き込むことが可能になります。


    事前準備

    まず、Marketing Cloud Engagement 側で以下の準備が必要です。

    ① インストール済みパッケージの作成

    • インストール済みパッケージ を作成

    • API Integration(Server-to-Server)を設定

    • クライアント ID / クライアントシークレット を取得

    ② 必要なスコープ(権限)

    今回の用途では、データエクステンションのレコードを取得するため、

    • Data > Data Extensions:Read

    の権限があれば問題ありません。


    その他に必要な情報

    Apps Script 実装時に必要となる情報は以下の通りです。

    • データエクステンションの外部キー(External Key)

    画像
    • Google スプレッドシートのシート名

    画像

    設定方法

    本記事では、インストール済みパッケージの作成から、
    Apps Script を使って、Marketing Cloud Engagement の API を呼び出し、
    スプレッドシートへ自動連携するまで、詳細な手順を解説していきます。

    Marketing Cloud Engagement の設定

    1. まずは、Marketing Cloud セットアップの左メニューから、プラットフォーム > アプリ の中にある「インストール済みパッケージ」を選択します。

    画像

    2. 続いて、右上にある「新規」ボタンをクリックします。

    画像

    3. パッケージ名を決めて保存します。このパッケージ名は、API 連携のインターフェース設定の定義名になります。今回は Apps Script としました。

    画像

    4. 次に「コンポーネントの追加」をクリックします。

    画像

    5. 「API 連携」を選択して、次へをクリックします。

    画像

    6. 「サーバー間」を選択して、次へをクリックします。

    画像

    7. 最後に、スコープ(API で行える権限)を設定します。
    今回は、以下にチェックを入れて保存します。

    • Data > Data Extensions:Read

    画像

    8. 保存が完了すると、ポップアップ上に一度だけ「クライアントシークレット」が表示されますので、必ずメモしてください。

    画像

    9. 次の画面では、「クライアント ID」と「サブドメイン」を取得できますので、これらもコピーします。

    画像

    10. これで、以下の 3 つの情報がメモできたかと思います。これらの情報は、後に Apps Script 内で使用するので、保存しておいてください。

    ① クライアント ID
    ② クライアントシークレット
    ③ サブドメイン(REST Base URI のサブドメイン)


    スプレッドシートの設定

    1. スプレッドシートを開き、シートの名前を変更します。

    画像

    2. 今回、以下の 2 つのシートを用意してあります。データエクステンション名と一致している必要はありません。

    • 「OrderDetail_1K」(1,000 レコードを格納)

    • 「OrderDetail_100K」(100,000 レコードを格納)

    画像

    3. 続いて、メニューバーから「Extensions(拡張機能)」タブを開いて「Apps Script」 をクリックします。

    画像

    4. すると「無題のプロジェクト」が開きますので、名前を付けます。このプロジェクトは、1 つのスプレッドシートにつき、1 つのプロジェクトが割り当てられます。※ スプレッドシート内のシートごとではありません。

    画像

    5. 次にコード入力の画面になりますので、もともと記述されているものを一旦削除して、以下のコードをコピペしてください。

    ※ 注意:パフォーマンス上の問題から、100,000 レコードを上限にしてあります。必要に応じて拡張してください。
    現在のコードの場合、上書き(全消し → 書き直し)に対応しています
    。

    // ===== Global settings (declare ONCE) =====
    const MC_SUBDOMAIN = "mcxkknz3m0zj3jqsxn4hm3dp3q-4";
    const MC_CLIENT_ID = "t1onsb1ax48kuv7sc2kvie6s";
    const MC_CLIENT_SECRET = "1HwUFfftSNa6SfTIjyWaq3ow";
    
    const PAGE_SIZE = 2500;
    const MAX_RECORDS_100K = 100000;       // 100,000 records target
    const WRITE_CHUNK_ROWS = 5000;         // write to sheet in chunks
    
    // ===== 5 patterns (edit here) =====
    const PATTERNS_100K = {
      P1: { deKey: "57EFBC00-1564-409F-8979-6A9F878169E2", sheetName: "OrderDetail_1K" },
      P2: { deKey: "800851CB-9CE8-44F4-856C-16587AACA5C0", sheetName: "OrderDetail_100K" },
      P3: { deKey: "YOUR_DE_KEY_3", sheetName: "Sheet_P3" },
      P4: { deKey: "YOUR_DE_KEY_4", sheetName: "Sheet_P4" },
      P5: { deKey: "YOUR_DE_KEY_5", sheetName: "Sheet_P5" }
    };
    
    // ===== Core functions =====
    function getAccessToken_() {
      const url = `https://${MC_SUBDOMAIN}.auth.marketingcloudapis.com/v2/token`;
      const payload = {
        grant_type: "client_credentials",
        client_id: MC_CLIENT_ID,
        client_secret: MC_CLIENT_SECRET
      };
    
      const response = UrlFetchApp.fetch(url, {
        method: "post",
        contentType: "application/json",
        payload: JSON.stringify(payload),
        muteHttpExceptions: true
      });
    
      const body = response.getContentText();
      if (response.getResponseCode() >= 300) {
        throw new Error(`Token request failed: ${response.getResponseCode()} / ${body}`);
      }
      return JSON.parse(body).access_token;
    }
    
    function fetchRowsetPage_(token, deKey, page) {
      const url =
        `https://${MC_SUBDOMAIN}.rest.marketingcloudapis.com/data/v1/customobjectdata/key/` +
        `${encodeURIComponent(deKey)}/rowset?$page=${page}&$pagesize=${PAGE_SIZE}`;
    
      const response = UrlFetchApp.fetch(url, {
        method: "get",
        headers: { Authorization: `Bearer ${token}` },
        muteHttpExceptions: true
      });
    
      const body = response.getContentText();
      if (response.getResponseCode() >= 300) {
        throw new Error(`DE retrieval failed (deKey=${deKey}, page=${page}): ${response.getResponseCode()} / ${body}`);
      }
    
      const json = JSON.parse(body);
      return json.items || [];
    }
    
    function writeRows_(sheet, headers, rowsObj, alreadyWritten) {
      if (!rowsObj || rowsObj.length === 0) return 0;
    
      const values = rowsObj.map(obj => headers.map(h => (obj[h] ?? "")));
      const startRow = 2 + alreadyWritten; // row 1 is header
      sheet.getRange(startRow, 1, values.length, headers.length).setValues(values);
      return values.length;
    }
    
    // patternKey: "P1" | "P2" | ...
    function exportPattern100K_(patternKey) {
      const lock = LockService.getScriptLock();
      // Wait up to 30 seconds for other executions to finish
      if (!lock.tryLock(30000)) {
        throw new Error("Another export is currently running. Try again later or stagger triggers.");
      }
    
      try {
        const pattern = PATTERNS_100K[patternKey];
        if (!pattern) throw new Error(`Unknown patternKey: ${patternKey}`);
    
        const token = getAccessToken_();
    
        const ss = SpreadsheetApp.getActiveSpreadsheet();
        const sheet = ss.getSheetByName(pattern.sheetName) || ss.insertSheet(pattern.sheetName);
        sheet.clearContents();
    
        const maxPages = Math.ceil(MAX_RECORDS_100K / PAGE_SIZE); // 100,000 => 40 pages
    
        let headers = null;
        let buffer = [];
        let writtenRows = 0;
    
        for (let page = 1; page <= maxPages; page++) {
          const items = fetchRowsetPage_(token, pattern.deKey, page);
          if (items.length === 0) break;
    
          for (const item of items) buffer.push(item.values || {});
    
          if (!headers && buffer.length > 0) {
            headers = Object.keys(buffer[0]);
            sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
          }
    
          if (buffer.length >= WRITE_CHUNK_ROWS) {
            writtenRows += writeRows_(sheet, headers, buffer, writtenRows);
            buffer = [];
          }
    
          if (writtenRows >= MAX_RECORDS_100K) {
            buffer = [];
            break;
          }
        }
    
        if (buffer.length > 0) {
          if (!headers) {
            headers = Object.keys(buffer[0]);
            sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
          }
          writtenRows += writeRows_(sheet, headers, buffer, writtenRows);
        }
    
        Logger.log(`Export completed: pattern=${patternKey}, rows=${writtenRows}, sheet=${pattern.sheetName}`);
      } finally {
        lock.releaseLock();
      }
    }
    
    // ===== Wrapper functions for each pattern (easy to set triggers) =====
    function export_P1_100K() { exportPattern100K_("P1"); }
    function export_P2_100K() { exportPattern100K_("P2"); }
    function export_P3_100K() { exportPattern100K_("P3"); }
    function export_P4_100K() { exportPattern100K_("P4"); }
    function export_P5_100K() { exportPattern100K_("P5"); }
    

    ここで、皆さんの情報に変更する必要があるのは、以下の情報なので書き換えてください。

    // ===== Global settings (declare ONCE) =====
    const MC_SUBDOMAIN = "mcxkknz3m0zj3jqsxn4hm3dp3q-4";
    const MC_CLIENT_ID = "t1onsb1ax48kuv7sc2kvie6s";
    const MC_CLIENT_SECRET = "1HwUFfftSNa6SfTIjyWaq3ow";
    • サブドメイン

    • クライアント ID

    • クライアントシークレット

    // ===== 5 patterns (edit here) =====
    const PATTERNS_100K = {
      P1: { deKey: "57EFBC00-1564-409F-8979-6A9F878169E2", sheetName: "OrderDetail_1K" },
      P2: { deKey: "800851CB-9CE8-44F4-856C-16587AACA5C0", sheetName: "OrderDetail_100K" },
      P3: { deKey: "YOUR_DE_KEY_3", sheetName: "Sheet_P3" },
      P4: { deKey: "YOUR_DE_KEY_4", sheetName: "Sheet_P4" },
      P5: { deKey: "YOUR_DE_KEY_5", sheetName: "Sheet_P5" }
    };

    次に、各シートごとに、対象となるデータエクステンションの 「外部キー」 と、書き込み先の スプレッドシートの「シート名」を設定してください。

    注意:このサンプルでは最大 5 シート分 までしか用意していません。
    もし 10 シート分 など、さらに増やしたい場合は、このサンプルコードをそのまま ChatGPT に渡して「10 シート分に拡張して」と依頼すれば、同じ形式で増やせると思います。

    6. ご自身の情報で入力できたら、一度「保存」します。

    画像

    7. すると、実行可能な「関数」が表示できるようになったと思います。

    画像

    8. まず、シート 1 を実行します。

    画像

    9. アクセス権を求められますので、許可します。

    画像

    10. すると、すぐに実行が完了します。

    画像

    11. シート 1 を確認すると、データが入っています。成功です。

    画像

    12. 続いて、Apps Script の画面に戻って、シート 2 も実行します。

    画像

    13. すると、先ほどよりは多少時間はかかりますが、完了を確認できます。

    画像

    注意:Apps Script は 6 分以内に完了する必要があります。

    14. シート 2 を確認すると、データが入っています。こちらも成功です。

    画像

    自動化トリガーの設定

    ここまでは、手動実行の例でしたが、これを Automation Studio のように自動化することができます。今回は「スケジュール実行」を試します。

    1. 左メニューから「トリガー」を選択します。

    画像

    2. 「Add Trigger(トリガーを追加)」を選択します。

    画像

    3. 自動化したい「関数」を選択します。

    画像

    4. スケジュール実行なので「Time-driven(時間主導型)」を選択します。

    画像

    5. 実行のタイミングを設定して、保存します。

    画像

    6. スケジュールでの実行が登録されました。これですべての作業は完了です。後ほどスケジュールした時間が来ましたら、シート内にデータが入っているか確認してください。※ シートごとにスケジュールしてください。

    画像

    いかがでしたでしょうか。

    これまでデータエクステンションに直接アクセスしなければ確認できなかった情報も、本仕組みを活用することで、スプレッドシートの閲覧権限を持つユーザーであればログイン不要で確認できるようになります。そのため、部門間共有や簡易レポート用途として非常に有効です。

    なお、今回は分かりやすさを優先し、クライアント ID / クライアントシークレット を ハードコーディング する形で説明しました。実運用においてセキュリティ要件がある場合は、PropertiesService などを利用してコード上に直接記載しない構成に変更することをおすすめします。

    今回は以上です。


    次の記事はこちら

    前回の記事はこちら

    私の note のトップページはこちら

     
     
    Salesforce Marketing Cloud、Agentforce、Data Cloud、Salesforce 認定資格に関する実践的な情報を発信しています。これらの記事が、皆さまの学習や日々の業務に少しでもお役に立てば幸いです。