Salesforceの数式の「答え」を、ApexからTableauへ届けてみた
マエスです。Salesforceでは「大口」と表示されているのに、Tableauにつなぐと、その数式項目が出てこない。Tableauでも同じ計算をつくると、今度は式を変えるたびに両方を直すことになります。
そこで試したのが、Salesforceで計算した「答え」を、ApexからTableauへ直接届ける方法です。ApexはSalesforce上で動くプログラム。今回、データの取得からファイルの梱包、認証、送信までをSalesforce内で実行できました。転送処理を動かす外部サーバーは用意していません。
取引先3件で初回公開し、売上を変えて同じTableauデータソースを更新。公開先から読み戻した3件・4項目は、初回も更新後もSalesforceの取得値と一致しました。まず、その結果から見てみます。
今回はApexを手動で実行し、全3件をまとめて送り直した検証です。接続定義の事前準備と公開後の照合は別途行いました。自分の環境で試すためのコードと手順の検証キット(ZIP)も用意しています。
売上を変えたら、「答え」もTableauへ届いた
使ったのは、年間売上が1,000万円以上なら「大口」、それ未満なら「通常」、空欄なら「未設定」を返す数式です。初回送信の後、同じ取引先3件の売上を次のように変えました。表のA・B・Cは、実際に作った試験用レコードの呼び名です。
| 年間売上の変更(万円) | 数式結果の変化 |
|---|---|
| A:500 → 1,500 | 通常 → 大口 |
| B:1,200 → 空欄 | 大口 → 未設定 |
| C:空欄 → 0 | 未設定 → 通常 |
Salesforceで売上を変えただけの時点では、Tableauには古い値が残っていました。変更を確定し、別の実行でApexから数式の結果を取り直して再送すると、Tableauからも表の変更後の値を読み取れました。空欄と0円も区別されています。
照合したのは、レコードID・取引先名・年間売上・数式結果の4項目。Apexとは別のクライアントから、TableauのVizQL Data Serviceで問い合わせています。公開APIの成功応答に加えて、届いた中身まで確かめた結果です。初回公開も更新もHTTP 201で成功し、REST APIが返すデータソースID(LUID)は同じでした。
ここが今回の面白いところです。「大口かどうか」を決める式をSalesforceで管理したまま、その判定結果をTableauで使えました。
Salesforceにある数式が、標準の接続では出てこない
Tableauの公式ナレッジでは、標準のSalesforceコネクターは計算項目の抽出に非対応とされ、Tableau側で計算を再作成する方法が案内されています。公式資料:Salesforce.com Formula Fields do not Import to Tableau Desktop
一方、SalesforceのSOQLでは、数式項目を指定して計算結果を取得できます。SOQLは、オブジェクトと項目を指定してデータを取り出す問い合わせ言語です。数式の値は参照・クエリ時に計算されるので、その時点の答えを受け渡します。SOQLのSELECT構文/数式・派生項目の計算の仕組み
この経路なら、数式の変更はSalesforce側で行い、次の取得時に新しい結果を渡せます。Tableau側で同じ判定を保守する手間を減らしたい場面の、連携方法の一つになります。
Apexの仕事は「取得 → 梱包 → 送信」
今回の流れは、SOQLで結果を取得し、CSVへ書き出し、接続定義と一緒にTableauへ送る、というものです。接続定義を含むパッケージをTDSXと呼びます。

検証環境はSalesforce Developer Edition(API 68.0)とTableau Cloudの検証用Sandboxです。以下は仕組みが見える部分を抜き出したコードで、完全な実装と設定手順は検証キットに収録しています。
1. 数式の結果を、そのまま取得する
取引先に、テキストを返す数式項目Tableau_Test_RevenueBand__cを作りました。式は次のとおりです。
IF(
ISBLANK(AnnualRevenue),
"未設定",
IF(AnnualRevenue >= 10000000, "大口", "通常")
)
最初に引っかかったのは、空欄の扱いでした。ISBLANKを書いても、数式の設定が「空白をゼロとして処理」だと、空欄が「通常」になったのです。「空白項目を空白として処理」へそろえると、「未設定」を取得できました。メタデータではformulaTreatBlanksAs=BlankAsBlankに当たります。公式資料:ISBLANK
式と空白処理を確認できたら、試験レコードだけをSOQLで取得します。次のAccountNumberは説明用なので、自分が作成した試験レコードの番号へ合わせます。
Set<String> targetNumbers = new Set<String>{
'DEMO-001', 'DEMO-002', 'DEMO-003'
};
List<Account> rows = [
SELECT Id, Name, AnnualRevenue, Tableau_Test_RevenueBand__c
FROM Account
WHERE AccountNumber IN :targetNumbers
WITH USER_MODE
];
WITH USER_MODEでオブジェクト・項目の権限などを適用し、取得するレコードと列を限定します。なお、Salesforceの共有設定や項目権限はTableauへ複製されません。送信する範囲に加え、Tableau側の閲覧権限も設定します。公式資料:ユーザーモードでのSOQL
2. CSVに接続定義を添え、ApexでZIPにする
公開APIへ渡すために、CSVとTDSを一つのTDSXへまとめます。TDSは接続先・列・データ型などの定義、TDSXはその定義とローカルデータを梱包したZIPです。Tableauのファイル形式/REST APIによる公開
Account.tdsx
├─ Account.tds CSVへの接続と項目の定義
└─ Data/Account.csv Salesforceから取得した値
TDSは、Tableauで作成した既存のCSV接続を雛形に、今回の4列へ調整しました。検証キット内のAccount.tdsとStatic Resource登録手順を使えます。CSV名、相対パス、列名、データ型をそろえ、以降はSalesforce内で雛形を使います。今回調整したTDSとCSVの組み合わせで動作を確認した、実験的な構成です。
年間売上のnullは、CSVでは空のフィールドにします。文字列の「null」や数値の「0」は書きません。初回送信を基に、IDと名前を説明用に置き換えると次の形です。
Id,Name,AnnualRevenue,Tableau_Test_RevenueBand__c
demo-A,検証A,5000000,通常
demo-B,"検証,サンプルB",12000000,大口
demo-C,検証C,,未設定
Cのデータ行では、,,の間が年間売上の空欄です。実送信では実際のレコードIDを使い、カンマ・二重引用符・改行はCSVの規則でエスケープします。日本語・カンマ・二重引用符を含む名前もTableauで一致しました。LF改行はSalesforce保存後の取得時点でスペースになっていたため、改行の往復成功とは扱っていません。
梱包にはApex標準のCompression.ZipWriterを使います。tdsTemplateはTDSのBlob、csvDataはCSVのBlobです。公式紹介:ApexのネイティブZIP対応
Compression.ZipWriter zip = new Compression.ZipWriter();
zip.addEntry('Account.tds', tdsTemplate);
zip.addEntry('Data/Account.csv', csvData);
Blob tdsx = zip.getArchive();
このTDSXの生成から公開までをApexで実行しました。
3. 認証して、Tableauへ公開する
認証には、Tableauのパーソナルアクセストークン(PAT)を使います。秘密値はApexのソースへ埋め込まず、External Credentialで保管し、Named Credential経由で参照しました。公式資料:Named Credentials
| 設定するもの | 今回の設定 |
|---|---|
| External Credential | 認証プロトコルはCustom。Named Principalを作り、認証パラメーターPatNameとPatSecretへPAT名と秘密値を保存。 |
| 実行者のアクセス | 権限セットのExternal Credential Principal Accessでプリンシパルを許可し、Apex実行者へ割り当てる。 |
| サインイン用Named Credential | 接続先はTableau CloudのpodのHTTPSベースURL。Allow Formulas in HTTP BodyはON、Generate Authorization HeaderはOFF。 |
| 公開API用Named Credential | 同じHTTPSベースURLとExternal Credentialを参照。Allow Formulas in HTTP BodyはOFF、Generate Authorization HeaderはOFF。 |
| 実行ログ | Apex CodeはINFO、CalloutはNONE。本文・トークン・秘密値を独自のdebug出力へ書かない。 |
サインイン用の本文だけで{!$Credential.TableauAuth.PatName}のような参照式を展開し、ZIPを送る本文では式展開を止めます。TableauAuthは説明用のExternal Credential名です。公開要求はmultipart/mixed形式で組み、TDSXのバイナリを保持して送ります。HTTP本文の式展開設定/公開要求の形式
もう一つ引っかかったのが、認証の参照式でした。今回のNamed CredentialではHTMLENCODEを使う式がエラーとなり、直接参照へ修正しました。現在の認証情報でサインインできた範囲の実装で、特殊文字を含む任意の値をXMLへ安全に展開する汎用処理までは備えていません。数式関数の対応範囲も確認します。公式資料:カスタムヘッダーで使える数式関数
Tableauへは、PATでサインイン → 返されたセッショントークンをX-Tableau-Authへ設定 → 公開 → サインアウトの順に呼び出します。公開要求に付けるのは、サインインで返されたトークンです。処理終了時のサインアウトで無効にします。公式資料:サインイン・サインアウト
検証キットで、作成・送信・変更・再送を試す
検証キット(ZIP)をダウンロードすると、送信クラス、梱包クラス、テスト、4列のTDS、READMEと4つの実行例が入っています。実機で使った送信クラスと、接続先などを自分の環境へ差し替える実行例です。
Developer組織などの試験環境で、項目参照・Apex実行権限、Tableauの専用プロジェクトへの公開権限、認証情報を準備します。READMEに沿って設定とコードを登録したら、次の順に一つずつ、別々に実行します。
- 01:試験データを作成。専用の取引先3件を用意し、数式が「通常」「大口」「未設定」を返すことを確認する。
- 02:初回送信。Apexから公開し、返されたデータソースIDを保存。Tableauで元の値と照合する。
- 03:売上を変更。専用3件を1,500万円・空欄・0円へ変更し、処理の確定を待つ。
- 04:更新送信。初回と同じ名前・プロジェクトへ再送し、同じデータソースIDと変更後の値を照合する。
読み戻しは、公開処理とは別にサインインしたクライアントから、Tableau公式のVizQL Data Serviceの手順で行います。データソースのAPI Access権限を確認し、02で返されたdatasourceIdをdatasourceLuidへ指定。read-metadataで項目名を確認してから、query-datasourceでID・名前・年間売上・数式結果を取得し、レコードIDをキーにSOQLの値と照合します。初回・更新後とも、3件すべての4項目が一致すれば合格です。READMEの手順5・7にも照合条件を載せています。VDSの権限設定/データソースへの問い合わせ手順
データ作成・変更と送信を分けるのは、同一トランザクション内で未コミットの更新後にコールアウトすると、エラーになる場合があるためです。公式資料:同一トランザクション内の更新とコールアウト
更新時は、初回と同じ名前・プロジェクトの設定に、次の指定を加えます。
cfg.overwrite = true;
cfg.expectedDatasourceId = 'YOUR_EXISTING_TABLEAU_DATASOURCE_LUID';
overwrite=trueは同名のデータソースがあれば上書きし、なければ新規作成する指定です。今回のコードは、送信直前に既存ID・名前・プロジェクト・所有者を照合し、応答でも同じIDを確認します。確認と公開は原子的な一処理ではないため、検証中に対象の名前変更・移動・削除を並行して行わず、不明な応答が出たら再送前に送信先を調べます。公式資料:Publish Data Sourceのoverwrite
キットのテストは、ZIP内容などを確かめる3件と、初回公開・上書き・送信先の取り違えなどをモック応答で確かめる8件の、計11件が合格しています。冒頭の結果は、これに加えて実際のTableau Cloudで初回公開・同じデータソースの更新・値の読み戻しを行ったものです。
業務で使うなら、「いつの答えを届けるか」を決める
Tableauへ渡した値は、送信時点のスナップショットです。Salesforce側の値が変わったら、改めて取得して送る必要があります。TODAY()など日付に依存する数式は、レコードを編集しなくても答えが変わるので、値を取り直す時刻が意味を持ちます。公式資料:数式・派生項目と更新日時
今回確認したのは、手動で全3件を送り直す方法です。継続利用する際は、実行時刻、失敗時の確認・再実行、履歴の残し方を設計します。定期実行や差分同期は、今回の検証には含めていません。
また、ApexにはメモリとHTTPコールアウトの上限があります。CSV文字列、ZIP、送信本文の一時コピーもメモリを使うため、件数だけで判断できません。今回は3件の小さな検証で、大量データの容量・速度は未測定です。配布コードはTDSXが512KiBを超えると停止させています。これは検証用コードに設けた上限です。公式資料:Apexの制限
Salesforceで決めた「大口」という答えを、Tableauへ届ける。小さな3件の検証ですが、数式を管理する場所と、その結果を使う場所をつなぐ経路を確かめられました。自分の組織でも、まず一つの数式と数行のデータで、取得元と公開先の値を比べるところから試せます。
検証メモ:2026年10月25日、Salesforce Developer Edition(API 68.0)とTableau Cloudの検証用Sandbox(プレリリース環境)で実施。TDSXは初回1,712バイト、更新時1,711バイトでした。画面URLの末尾の数値は更新後に変わりましたが、REST APIのLUIDは同じでした。数値URLが変わった理由は判断していません。

