PaaSとしてのSalesforce
セールスフォースの出自はCRMのSaaSですが、PaaSとしてこれを見ると以下のような特徴があると考えることができます。
- バックエンドのリレーショナルに近いデータベース1
- 1レコード/1ページのUIやレコードを一覧するリストビュー、集計のためのレポートなどを標準装備
- LightningフローのようなNoCodeプログラミングツール2
これらがポイント&クリックで行えることにより、ITの専門知識がない方にもアプリケーションが作りやすいというのが大きな特徴だと思われます。
ただ、IT専門でない方はデータ設計などには疎く、しばしば奇妙なオブジェクトの構造が見受けられます。後から修正することは可能ですが、データの移行、レポートタイプや各種自動化プロセスなどの作り直しなどが必要になる可能性があるため、やはりデータ設計は最初の段階できちんと行っておくことが好ましいです。
正規化
リレーショナルデータベースには正規化という概念があります。一般的にはリレーショナルデータベースのデータ設計を行う場合は以下の3ステップを実施します。(IT専門でないなら用語を覚えておく必要は個人的にはないように思います。)
例を見ながら見てみましょう。
元々の伝票
非正規形(UNF)
Excelにこれをまとめようとすると、多くの方が以下のように記述すると思います。
これが非正規形(正規化が行われていない状態)です。
第一正規形(1NF)
同じ項目が繰り返される状態を無くす作業が第一正規化(その結果が第一正規形)です。
塗りつぶされた項目(伝票番号と商品コード)でその行を特定することに使えます。つまり複数の項目の組み合わせがこの表の主キーを構成しています(複合主キー)。
第二正規形(2NF)
複合主キーの一部で値が特定できる部分を分けて別の表にしていくのが第二正規化です。
ここでは、伝票番号が決まれば日付や顧客コードなどが決まり、商品コードが決まれば商品名と単価は決まります。
第三正規形(3NF)
主キー以外の部分で値が特定できる部分を分けます。
ここでは、主キー(伝票番号) → 顧客コード → 顧客名と値が決定できます。
また導出項目(計算すれば値を算出できる項目、ここでは小計・合計)は削除します。3
正規化の目的
要するに正規化というのは同じ値を複数の場所に持たないようにすることです。なぜ同じ値を重複して保持しないようにするのかというと、一箇所変えれば全てを整合性を持って変更できるからです。4
同じ情報を複数箇所に持っておくと、片方を更新し忘れた場合などに、何が正しいのか分からなくなってしまいます。特定の情報は特定の一箇所に存在するようにしておくということが、データの整合性を保つためには非常に重要です。
親子関係
レコードが1:Nの関係になる場合、それらを親子関係と呼びます。上記の例では、以下の親子関係があると見做すことができます。
- 伝票:伝票明細
- 顧客マスタ:伝票
- 商品マスタ:伝票明細
「XXXマスタ」はだいたいは親になり、「XXX明細」といったら子になる表だと考えられます。
Salesforce固有の条件と実装
上記の各テーブルがSalesforceのオブジェクト(sObject)に相当します。オブジェクトにはあらかじめ作成されているもの(標準オブジェクト)とユーザが作成するもの(カスタムオブジェクト)が存在します。Salesforceのオブジェクトには通常のリレーショナルデータベースとは異なる特徴があるため、これをご説明します。
SalesforceId
Salesforceは各レコードの主キーを15桁もしくは18桁の文字列として勝手につけてくれます。5 6
システムで使用されることを前提としているので、視認性の高いフォーマットではありませんが、「Salesforceのレコードには必ず主キーが設定されている」と考えることができます。
主従関係/参照関係項目
主従関係/参照関係項目は子オブジェクトに設定する親レコードを指定する項目です。実際には親レコードのSalesforceIdが格納されています。オブジェクトの結合(JOIN)はこれらの項目を使用して行われるため、WHERE tab1.colA = tab2.colB
のように任意の項目で結合することができません。
別の言い方をすると、親子関係は常に主従関係/参照関係項目によって表現されているということです。
-
主従関係/参照関係の違いと選び方
下の表のような違いがあります。大雑把にいうと主従関係の方が親子の関係の縛りが強い反面、親レコードで子レコードの項目を集計する積上集計項目を使用することができるというメリットがあります。
主従関係 参照関係 親レコードに対する参照は必須 親レコードへの参照は任意 親レコードが削除されると子レコードも削除 親レコード削除で参照項目がクリアされるか親レコードの削除を許可しないかを選択 子レコードの所有者と共有が親レコードによって決定 親オブジェクトとは独立した共有設定が可能 親レコードで積上集計項目を使用することができる 積上集計項目使用不可
実際にSalesforceIdが項目に格納されていることはSOQLで確認ができます。
伝票明細(SalesSlipDetail__c)を検索すると以下の結果が得られます。
SELECT Id, Name, Quantity__c, Food__c, Food__r.Name, SalesSlip__c,
SalesSlip__r.Name, SalesSlip__r.Date__c,
SalesSlip__r.Customer__c, SalesSlip__r.Customer__r.Name
FROM SalesSlipDetail__c
WHERE Name = 'D-001'
Food__cは商品マスタに対する参照項目、__c を __r に置き換えたものがリレーション名です。参照先レコードの項目を得るには、リレーション名に . (ドット)でつなげて項目名を記述します。
親レコードから子レコードを参照することもできます。親レコードから子レコードを参照するためのリレーション名は子オブジェクトに定義する主従/参照項目の下記の場所に定義されています。
今度は伝票(SalesSlip__c)を検索して紐づく子レコードの伝票明細をみてみます。
SELECT Id, Name, Date__c, Customer__c, Customer__r.Name,
(SELECT Id, Name, Quantity__c, Food__c, Food__r.Name FROM SalesSlipDetails__r)
FROM SalesSlip__c
WHERE Name='001'
Salesforceでのオブジェクトの結合は、このようにリレーション名を使って実現されます。
数式項目と積上集計項目
導出項目は数式項目と積上集計項目が使えないかをまず考えます。
-
数式項目では親レコードの項目を参照する数式を使うことができます。(クロスオブジェクト数式と呼びます。10階層まで親を辿ることができます。)
-
積上集計項目では子レコードの件数あるいは子レコードの特定の項目の合計・最大・最小の値を算出し親レコードの項目値として設定します。
- 子レコードの数式項目の積上集計を行うことは可能ですが、数式がクロスオブジェクト数式であったりNOW()やTODAY()といったその場で計算が行われる数式が使われている場合、積上集計ができません。親レコードを参照する数式項目を別途作成し、その項目を参照する数式項目を作って積み上げるということもできません。
-
導出項目に数式項目/積上集計項目が使えない場合は、Lightningフロー等で計算を行った結果を項目に書き込むという手段をとります。但しどのタイミングで計算が行われレコードが更新されるかについては注意が必要です。(下記サンプルの実装例を参考にしてください。)
カスタムオブジェクトのName項目
カスタムオブジェクトではName項目を必ず持つ必要があります。この項目はレコードを代表するような項目値を保持するのに使用します。上の例では顧客マスタにおける顧客名などです。
先述のように主従関係/参照関係項目には実際には参照先レコードのSalesforceIdが格納されていますが、標準UIのレコードページではName項目の値が表示されます。またルックアップの検索レイアウトはカスタマイズ可能ですが、Name項目を省くことはできません。
Name項目のデータ型としてはテキストと自動採番が選択できます。上の例における伝票明細のようにレコードを代表するような項目値が存在しない場合、自動採番を採用します。自動採番項目はリードオンリーの項目で、任意の値を設定することはできません。なお自動採番を採用する際は以下の点にご留意ください。
-
桁溢れ
A-{00} と指定していた場合、A-99 の次は A-100 となります。指定した桁数におさまることを保証していません。 -
欠番
連続した番号が振られることは保証されていません。A-98 の次が A-100 かもしれません。 -
番号のリセット
一度テキスト型に変更し再度自動採番型に変更することによって、開始番号をリセット(任意の番号から採番を再開)することができます。言い方を変えると、自動採番型であること自体は一意性を保証していません。
なお、テキスト型のName項目もユニーク項目として設定することはできません。7
入力規則
文字数やフォーマット、使える文字などについて制限をかけたい場合、入力規則を設定します。データの品質を保ちたければ適切な入力規則を設定すべきです。
規則はエラーになる時TRUEと評価される数式で表現します。数式では親レコードを参照できます。また入力規則でのみ例外的にVLOOKUP()関数で主従/参照関係がないオブジェクトも参照できます。
選択リスト
リレーショナルデータベースの考え方に従うなら、選択リストも別テーブルとすべきものです。しかしながら、以下の三点を満たす場合は、わざわざオブジェクトを分けず、選択リストを採用すべきでしょう。
- データが固定的であり、レコードが増えたり減ったりということが(ほとんど)ない
- 必要なのは1項目(名前)のみ
- レコード数が多くなくプルダウンリストからの選択が可能
例としては、性別、国、都道府県、などが挙げられます。増えたり減ったりすることが滅多にないものです。但し、例えば都道府県について、都道府県の名称だけではなく、人口・県庁所在地・首長の名前、といった情報も管理したければ別オブジェクトにすべきでしょう。
特定のオブジェクトでだけ使用するなら、そのオブジェクトの項目に選択リストの値を定義すれば良いでしょう。都道府県のようなリストは複数のオブジェクトで使い回したいかもしれません。その場合は、グローバル選択リストとして定義することで使い回しが可能になります。(リレーショナルデータベースの正規化の考え方に則ったマスターテーブルと考えることができます。)
なお、選択リストは簡便なUIを提供することを第一の目的としています。リストされている以外の値の入力を許可したくない場合は「値セットで定義された値に選択リストを制限します」という設定を必ずチェックしてください。チェックされていない場合、データローダなどAPI経由ででリストされている以外の値を設定することが可能です。
連動選択リスト
ある項目の値によって別の選択リスト項目で入力できる値が制限されるような場合は、項目の連動関係を設定します。プルダウンリストに表示される値が絞られ、不要な値はリストされなくなります。
例えば日本の市区町村は2019年2月15日現在1741あるそうです。選択リスト項目に市区町村名を羅列してもプルダウンリストから選ぶのは困難です。市区町村以外に都道府県という選択リストがあり、都道府県を選べば該当する市区町村のみが選べるのであれば、より使いやすいユーザインターフェースとなります。
- 連動選択リストの値を絞る項目を制御項目、連動選択リストの項目を連動項目と呼ぶことがあります。
- 見方によっては入力規則と似ていますが、絞られるのはあくまでUI上に表示される選択肢です。API経由で連動関係で指定していない値に更新することは可能なので、データの整合性を厳密に保持したい場合は入力規則と併用するのが良いでしょう。
レコードタイプ
オブジェクトにレコードタイプを設定することができます。レコードタイプというのはオブジェクトのサブカテゴリと考えることができます。例えば商品に 野菜/果物/肉 といったカテゴリがあるときに使用を検討する価値があります。
単にカテゴリの値を持っておきたいのであれば選択リストで十分です。レコードタイプを使うことのメリットはレコードタイプによって以下を制御できるようになることです。
- ページレイアウト/Lightningページ
- 選択リストの値(レコードタイプが選択リストの制御項目になっていると見做すことができます。)
RecordTypeという標準オブジェクトがレコードタイプのマスターとして存在し、ここに対象オブジェクト名とレコードタイプ名が保持されています。オブジェクトの各レコードは、RecordTypeオブジェクトに対する参照を持つ(RecordTypeId項目にRecordTypeレコードのSalesforceIdの値を持つ)ことになります。
ユーザインターフェース
一般的なシステム開発であれば、データ設計とUI設計は独立に考えますが、Salesforceでは標準装備のUI機能が存在するので、データ設計時からこれを活用できるように考慮しておいた方が良いでしょう。
-
レコードページ
1レコードを1ページで表示する基本的なUIです。ただし、関連リストで子レコードの情報も表形式で表示することができます。
-
ページレイアウトと関連リスト
ページに表示する項目、表示する子オブジェクトの種類と項目を定義することができます。孫オブジェクト以下は表示できません。
-
Lightningページ
Lightning Experienceでのみ使用され、ページレイアウトをラップするUIの定義です。関連リストの表示場所やページ内のタブ設定、各コンポーネントの表示条件などを柔軟に定義できます。
また、関連リストについては親レコードの関連リストを表示することもでき、関連レコードコンポーネントを使用すれば、数式項目を使わなくても親レコードの項目も表示できます。
関連レコードコンポーネントは自レコードに対しても設定できるので、条件によって表示項目を変えるといった設定も可能です。8
-
-
リストビュー
一種類のオブジェクトの複数レコードを表示するUIです。
条件に合致するレコードを表形式で一覧することができ、その状態で更新することも可能です。
Lightning Experienceでは表形式ではなくKanban形式で表示したり、集計結果をグラフで表示することもできます。 -
レポートタイプ
複数のオブジェクトを関連づけて一覧、あるいは、集計したレポートを作成することができます。レポートを作成する際にはオブジェクトの関係や使用する項目を定義したレポートタイプを作成しておく必要があります。(別の言い方をすると、オブジェクトの構造は最終的に出力したいレポートを実現するためのレポートタイプが作成できる形になっていることが望ましいということになります。9)- 最上位の親オブジェクトを選択後、4階層まで子オブジェクトを関連づけることができます。各結合は左外部結合もしくは内部結合です。
- 結合は 親→子→孫→曾孫 と直列になります。
- 親と子1/子2を結合したレポートタイプはできません。その場合2つの考え方があります。
- 子1/子2をひとつのオブジェクトにまとめてしまい、レコードタイプで分ける
- 親→子1/親→子2 の二つのレポートタイプを作成し、結合レポートでそれぞれのレポートタイプで作成したレポートを結合
- 親と子1/子2を結合したレポートタイプはできません。その場合2つの考え方があります。
- 親・子・孫・曾孫からレポートタイプで結合していない親オブジェクト(参照先)の項目を参照することができます(4階層まで)。レコードページにも表示したければ数式項目を作りますが、レポートでだけ必要であればレポートタイプで対応可能です。
よくあるダメな例
定期的に項目を増やしている
Salesforceでは項目を一つ追加するのは簡単ですが、簡単だからカジュアルにやっても良いということにはなりません。正規化の考えに基づいて設計すれば、その時点で必要な項目は決まるはずです。ビジネス要件が変更した場合(顧客のこれこれの情報も管理しよう、など)は項目の追加が発生するでしょう。しかしながらもし時間の経過で項目の追加が行われているとしたら適切な正規化が行われていません。「2020年7月」「2020年8月」という項目があり9月になったら「2020年9月」というような項目を作るという運用は不適切です。
同じような名前の項目が多数ある
例えば「担当1」「担当2」「担当3」といった項目を見ることがあります。「部長」「課長」「係長」であるなら各項目の意味合いが違うので問題ありません。意味合いが同じ項目が複数存在しその数が不定という場合(「担当1」「担当2」・・・「担当N」)、正規化の考えに則って子オブジェクトを作成すべきです。
ひとつのテキストエリアなどに複数の値を記述している
例えば「品質/価格」という項目を作り、ユーザには「良い/普通」などと記述してもらうことを期待しているようなケースです。「品質は良いけど価格はイマイチ」などと記述するユーザも出てくるかもしれません。集計するのが著しく困難になりますので、異なる意味合いの値は項目は分け、必要に応じて入力規則あるいは選択リストを使用しましょう。
カスタムオブジェクトのAPI名、項目のAPI名が CustomObjectX__c, FieldX__c などとなっている
オブジェクトあるいは項目の表示ラベルを日本語で作成するとAPI名としてデフォルト値が上記のようになってしまいます。このまま作成を続けると数式、Lightningフロー、Apex、Einstein Analyticsのデータフロー等々API名でオブジェクト/項目を指定しなければならないときに非常に分かりにくいことになります。面倒でもアルファベットで意味が推測できるAPI名を_最初の段階で_指定しましょう。API名を後から変更すると、これを参照している前述の仕組みが一気に動作しなくなります。
親レコードの値をワークフロールール等で子レコードの項目に(無駄に)コピーしている
一つの値はどこか一箇所に存在し、その値を変更するときに複数箇所を変更しないで済むようにするのが正規化の目的でした。親レコードの現在の値を参照したいだけなら、親レコードの当該項目を参照する数式項目を使用してください。
ある特定時点で親レコードが持っていた値を取得しておきたいなら子レコードにコピーするということはあるでしょう。現在の値なのかある特定時点の値なのか、ということでデータの意味は異なってきます。
項目値をフローやApexなどの自動化の仕組みの中にハードコードする
項目値は変わる可能性があるので、ハードコードは極力避けるべきです。
アプリケーションのデータというよりアプリケーションが動作するための設定値のような位置付けのものであれば、カスタムオブジェクトではなくカスタムメタデータ型に保存しておくのも一考に値します。
全く違う内容を同じオブジェクトに格納しレコードタイプで分ける
上述のようにレコードタイプはカテゴリと考えることができるので、例えば案件オブジェクトに新規案件と継続案件という二つのレコードタイプがあるというのは自然です。
しかし例えば、計画と実績、だとどうでしょうか。データの意味合いが大きく異なり、多くの場合1レコードの単位も異なります(実績はひとつひとつの案件ベースで、計画は月ごと、など)。常識的なデータ設計ではこれらは別のオブジェクトに分かれるでしょう。しかし、レポートタイプの仕組みから、別オブジェクトにすると計画と実績を比較するレポートがうまく作成できないということがおこるかもしれません。個人的にはオブジェクトを分けて作成した後、目的のレポートが作成できるか確認するPOCなどを実施するのが良いと思います。
サンプルの実装例
基本の考え方と課題
上の例の伝票をSalesforceで実装する場合、「1レコードページ」=「1伝票」と考えるのが自然でしょう。オブジェクトも4つに分け、以下のように考えてみます。
- 伝票明細は関連リストに表示し、伝票明細は伝票に対する主従関係項目を持つ
- 伝票明細は商品マスタに対する参照項目を持つ
- 伝票明細は数式項目で小計を計算する
- 伝票は顧客マスタに対する参照項目を持つ
- 伝票は伝票明細の小計を合計する積上集計項目を持つ
残念ながらこの通りでは伝票ページに合計金額を表示させることができません。クロスオブジェクト数式を積上集計することができないためです。
伝票ページに合計金額を表示させることを諦めレポートで表現する
いきなり諦めるところから入るのはイマイチだとは思いますが、Salesforceにはレポートという集計機能があるので、レコードページでの表示が必須ではないならばひとつの考え方ではあります。
Lightningフローを使用し動的に合計金額を集計する
伝票明細の小計がクロスオブジェクト数式であるため伝票で積上集計できないことが問題でした。これを解決するには2つの方法が考えられます。
- クロスオブジェクト数式項目で単価を得るのではなく、伝票明細に数値項目で単価を持たせ、積上集計を利用する
- 積上集計の機能は使用せず、伝票に数値項目で合計金額を持たせる
どちらのケースでも伝票明細が更新された場合/商品マスタが更新された場合の両方について考える必要があります。(現実的なユースケースでは、商品マスタでの単価が変わったタイミングで過去の伝票の合計金額は変えたくないことが多いと思います。その場合、下記1-2, 2-2のフローは不要です。)
1.伝票明細に数値項目で単価を持たせる場合
1-1. 伝票明細の作成/更新時に単価を設定
数値項目の単価を伝票明細の作成/更新時に設定するフローです。更新するのはフローの起点になったレコードのみのため、保存前実行フローが使用できます。これは保存後実行フローより高速です。
1-2. 商品マスタの変更時に伝票明細の単価を更新
商品マスタが変更された時、子レコードの伝票明細を検索し、新しい単価を設定した上で一括更新しています。数式項目により小計が自動計算され、積上集計の合計値も自動計算されます。
2.伝票に数値項目で合計金額を持たせる場合
2-1.伝票明細の作成/更新時に伝票の合計金額を更新
伝票明細が作成・更新されたタイミングで親の伝票に紐づく伝票明細の小計を集計して伝票の合計項目を更新する以下のようなフローを作成することができます。(なお、伝票明細が削除された場合は考慮されていません。10)
2-2.商品マスタが変更されたときに紐づく伝票明細を更新することで2-1のフローを呼び出し伝票の合計金額を更新
商品マスタの金額が変更された際、その商品マスタを参照する伝票明細から伝票をリストし更新することも可能ではありますが、ひとつのトランザクションが大きくなり制限に抵触するリスクが大きくなります。ここでは伝票明細自体を更新することで2-1のフローを呼び出しています。(同じ伝票レコードを2回更新する必要はないので、ループ内で同じ伝票に紐づいている伝票明細を省いています。)
上記のようにどちらのパターンでも伝票に合計金額を持たせることは可能ですが、伝票と伝票明細に主従関係があり積上集計を使えるのであれば、これを利用した方がカスタマイズは簡便になります。データ設計時から留意すべきポイントのひとつです。
本稿では一般的なデータ設計の考え方から入ってSalesforce固有の事情を考えていくという順序で記述しました。Salesforceの機能から考えていくという方向で記述されているものとしては、下記の記事がまとまっていると思います。合わせて参考にしてください。
https://www.terrasky.co.jp/blog/2013/130821_001303.php
-
リレーショナルデータベースと呼ぶべきかどうかという議論はここではおいておきます。 ↩
-
NoCodeかLowCodeかという議論もここではしません。 ↩
-
導出項目が削除されているかどうかは1NF, 2NF, 3NFの定義には入っていないようです。どの段階で行っても構いませんがどこかでは行います。 ↩
-
SQLでは主キーを使ってテーブルを自由に結合して検索するので同じ値を重複して保持する必要はありません。但し、いちいち結合操作をしなければならないので、大量データの検索においてはこの点はデメリットになります。検索向けに作成されているNoSQLデータベース(非リレーショナルデータベース)では正規化されているデータを検索に適した形に非正規化して格納するといったことが行われており、Einstein Analyticsのデータセットもそのような形で格納されています。 ↩
-
15桁のIDも18桁のIDも本質的には同じものですので変換することが可能です。2種類のIDが振られているわけではありません。大文字小文字を区別する場合は15桁で済みますが、区別しない場合は18桁となり、最後の3桁は15桁のうちどの文字が大文字かを示しています。 ↩
-
通常のデータ設計では主キーはテーブル内でユニークであることを要件としますが、Salesforceでは組織内の全オブジェクト内でユニークな値になっています。一部の標準オブジェクトで実装されている複数種類のオブジェクトに対する参照(ToDoの関連先(WhatId)など)が可能なのはこのためです。しかし、カスタム項目ではこうした多態的(Polymorphic)な参照を作成することはできません。 ↩
-
まだGAになっていませんが、Summer'20にてDynamic Formsという機能が案内されました。これがGAになれば、表示項目の指定はページレイアウトの定義から完全に独立することになります。 ↩
-
Einstein Analyticsは SalesforceId 以外でレコードを結合でき、その際の階層数にも制限がないため、こういった考慮をする必要はありません。オブジェクト構造が複雑でレポートタイプの4階層に収まらない場合は、正規化の考え方とは外れますが、プロセスビルダーやフローでレポートタイプで指定されているオブジェクトにカスタム項目を作成し値を更新するという仕組みを作ることもあります。 ↩
-
レコードが削除されたタイミングで自動化プロセスを動かしたい場合、現状ではApexトリガーをプログラミングする必要がありますが、Winter'21ではレコード削除のタイミングでもフローを動作させることができるようになる模様です。 ↩