複合ユニークキーとは、複数の項目をつないで1つの値にし、その値が重複しないことを保証するキーです。当社が確認した時点では、Zoho CRMに「複数の項目の組み合わせで一意にする」設定が見当たらないため、つないだ値を入れる項目を1つ作り、その項目に「重複を許可しない」を設定して実現します。

この記事では、西日本の食品卸(中小企業)の案件をもとに、基幹システムのCSV取込に提携先の別会社データを加えたときの設計を解説します。得意先コードが2社で衝突する問題を、「会社区分+得意先コード」の複合ユニークキーで解きました。途中で、届いたCSVが当初の想定と違っていたことも含めて書きます。

得意先コード単独のキーのままだと、何が壊れる?

この案件では、基幹システムが得意先マスタと販売実績をCSVで出力し、Zoho DataPrep(データの整形・取込ツール。基本は「ZohoDataPrepでデータをクレンジング」で紹介しています)で整形してZoho CRMへ日次で取り込んでいました。一意のキーは得意先コード単独です。取引先モジュールの得意先コード項目に「重複を許可しない」を設定し、DataPrepもこのコードで既存レコードを探して更新していました。

ここに提携先の別会社の得意先を同じCRMへ載せることになりました。2社は別々に得意先コードを付番しているため、同じ番号が両社に存在しえます。コード単独キーのままだと、次の3か所が壊れます。

場所 現状 別会社データを足すと
CRMの重複禁止 得意先コードが重複禁止 同じコードの2社目を登録できない
DataPrepの照合キー 得意先コードで既存レコードを探して更新 別会社の得意先で上書きしてしまう
Delugeの紐付け関数 販売実績の得意先コードで取引先を検索し、ルックアップを設定 検索結果の先頭1件を使うため、どちらの会社に紐付くか保証されない

3つ目は見落とされがちです。販売実績を作ると関数が動き、得意先コードで取引先を検索して紐付けます。検索結果が2件返ってきても、コードは先頭の1件を取るだけでした。エラーにならず、誤った取引先に売上が付くので、気付くのが遅れます。

Zoho CRMで複合ユニークキーを作るには?区分+コードを1つのテキスト項目にする

会社を表す区分と得意先コードをつなげて一つのテキスト項目にし、重複を禁止する照合キーとして使う設計を示した図。

方針は単純です。「どちらの会社の得意先か」を表す区分を足し、区分とコードをつないだ値を新しいキーにします。

追加する項目

取引先と販売実績の両方に、区分と複合キーを追加します(項目名は一般的な名前に置き換えています)。

モジュール 項目 型 重複を許可しない
取引先 会社区分 Company_Category 選択リスト なし
取引先 複合キー Account_Composite_Key 一行テキスト あり(新規)
取引先 得意先コード Account_Code(既存) 既存のまま あり → 外す
販売実績 会社区分 Sales_Company_Category 選択リスト なし
販売実績 複合キー Sales_Composite_Key 一行テキスト あり

複合キーの形は 区分コード_得意先コード です。自社を 0、提携先を 1 として、0_01000 と 1_01000 は別の値になります。販売実績の一意キーも同じ考え方で、区分を先頭に付けて 区分コード_伝票番号_行番号 にしました。

既存の得意先コードは消さずに残します。基幹システムと突き合わせるときの元の値だからです。別会社用のコード項目を新たに作ることもしません。コードは同じ項目に入れ、区分との組み合わせで見分けます。

先頭ゼロに注意する

この案件では、取込時に得意先コードを数値型に変換していました。数値にすると 01000 は 1000 になり、先頭のゼロが落ちます。複合キーは数値に変換する前の文字列から作るか、桁数を決めてゼロで埋めてからつなぎます。キーの作り方が取込経路ごとにずれると、同じ得意先が別のキーになり、重複レコードが生まれます。

実CSVで前提が変わった点

設計した時点では、基幹システム側で2社のCSVを別ファイルで出し、それぞれに会社区分の列を付けてもらう前提でした。ところが、受け取った出力仕様とサンプルCSVを確かめると、次のようになっていました。

  1. 得意先マスタも販売実績も、2社分が1ファイルに混在して出力される
  2. 会社区分の列は追加されない
  3. 提携先の得意先は、コードの先頭に固定の英字1文字、名称の先頭に会社名の括弧書きが付く
  4. 使用停止に設定された提携先の得意先は、マスタにも販売実績にも出力されない

区分の列がない以上、CSVから区分を受け取ることはできません。そこで、コードの接頭辞から区分を作る形に読み替えました。

判定 会社区分
得意先コードが固定の英字で始まる 提携先(1)
それ以外 自社(0)

サンプルでは、英字で始まるコードと、名称に括弧書きがある行は全件一致していました。販売実績に出てくる得意先コードは、すべて得意先マスタに存在しました。それでも判定にはコードの接頭辞だけを使い、名称は使いません。販売実績側の得意先名がマスタより短く切れている行があったためです。表示や検索に使う名称は、取引先マスタ側を正とします。

読み替えで、もう1つ影響が出ました。英字付きのコードは、数値型の得意先コード項目にそのまま入りません。英字付きのコードを保持するテキスト型の項目か、英字付きのコードを含む複合キー項目が必要になります。設計時の前提は、実データが届いた時点で必ず照合し直す。この案件で一番の学びはここでした。

使用停止の得意先が出力されない点も、取込の設計に関わります。CSVに出てこない得意先を「削除された」と扱って消す処理は入れません。CRM側のレコードはそのまま残ります。

付け替えの手順

作業の順番を誤ると、一意制約がない時間ができたり、取込が止まったりします。本番の前に、サンドボックス(本番とは別の検証用の環境)で同じ順番を一通り流します。

  1. 取引先と販売実績に、会社区分と複合キーの項目を追加する
  2. サンドボックスで、紐付け関数を複合キー検索に改修する(次の節のコード)
  3. サンドボックスで、DataPrepのパイプラインに「区分を作る列」と「複合キーを作る列」を加え、照合キーを複合キーに変える
  4. 既存の自社データに、区分 0 と複合キーを後付けする。取引先は件数が少なければ、CRMから書き出して表計算で値を付け、レコードIDをキーに更新インポートします。販売実績は件数が多いため、APIで分けて更新します。作業中はDataPrepの日次実行を止めるか、時刻をずらします
  5. サンドボックスで、提携先の過去データを全量流す。実行時間、重複の発生、関数の呼び出し回数、エラーの傾向を確かめます
  6. 本番で、複合キーに「重複を許可しない」を設定してから、得意先コードの設定を外す
  7. 本番で、改修した関数とDataPrepのパイプラインを適用する
  8. 本番で、既存データへの後付けを行う
  9. 本番で、提携先の過去データを投入する

6番の順番には理由があります。DataPrepからCRMへ書き出すとき、既存レコードを探す照合用の項目には、CRMで必須かつ「重複を許可しない」に設定された項目を選ぶ必要があります(Zoho DataPrepのヘルプ)。先に旧キーの設定を外すと、新キーが有効になるまでの間、どちらの項目でも一意性が守られません。新キーを先に有効にしてから旧キーを外します。

コードが空の取引先(名刺管理ツールから来たものや手入力のもの)は、複合キーを機械的に付けられません。一括の後付けからは外し、従来どおり人の手で統合する運用を残しました。

紐付け関数のコード(Deluge)

販売実績が作られたときに動き、複合キーで取引先を探してルックアップを設定する関数です。変更前は得意先コード単独で検索し、先頭の1件を使っていました。変更後は、一致が1件のときだけ紐付けます。

void automation.linkSalesToAccountByCompositeKey(Int sales_id)
{
	// 販売実績 → 取引先 のルックアップを、複合キー(区分コード_得意先コード)で設定する
	// モジュール名・項目名は自社の環境に合わせて置き換えてください
	api_base = "https://www.zohoapis.com/crm/v8/";
	// データセンターが日本の場合は zohoapis.jp
	sales = zoho.crm.getRecordById("Sales_Results", sales_id);
	if(sales == null || sales.get("id") == null)
	{
		info "販売実績が見つかりません: " + sales_id;
		return;
	}
	// 既に紐付いていれば何もしない(更新時の再実行による無限ループ防止)
	if(sales.get("Account") != null)
	{
		return;
	}
	account_code = ifnull(sales.get("Sales_Account_Code"),"").toString().trim();
	if(account_code == "")
	{
		info "得意先コードが空のためスキップ: " + sales_id;
		return;
	}
	// 区分コードは接頭辞から作る(固定の英字で始まれば提携先=1、それ以外は自社=0)
	category_code = "0";
	if(account_code.startsWith("X"))
	{
		category_code = "1";
	}
	composite_key = category_code + "_" + account_code;
	// COQLに文字列を埋め込むので、想定外の文字が入っていたら止める
	if(!composite_key.matches("^[0-9A-Za-z_]+$"))
	{
		info "想定外の文字を含むキーのためスキップ: " + composite_key;
		return;
	}
	query = "select id, Account_Name from Accounts where Account_Composite_Key = '" + composite_key + "' limit 2";
	coql_params = Map();
	coql_params.put("select_query",query);
	response = invokeurl
	[
		url :api_base + "coql"
		type :POST
		parameters:coql_params.toString()
		connection:"crm_connection"
	];
	// 該当0件のときは本文のない応答が返ることがある
	if(response == null || response.toString() == "" || response.get("data") == null)
	{
		info "該当する取引先がありません: " + composite_key;
		return;
	}
	matches = response.get("data");
	if(matches.size() != 1)
	{
		info "取引先が一意に決まりません(" + matches.size() + "件): " + composite_key;
		return;
	}
	update_row = Map();
	update_row.put("id",sales_id);
	update_row.put("Account",{"id":matches.get(0).get("id")});
	update_row.put("Sales_Company_Category",category_code);
	rows = List();
	rows.add(update_row);
	update_body = Map();
	update_body.put("data",rows);
	// trigger に空の配列を渡し、この更新で他のワークフローを動かさない
	update_body.put("trigger",List());
	update_response = invokeurl
	[
		url :api_base + "Sales_Results"
		type :PUT
		parameters:update_body.toString()
		connection:"crm_connection"
	];
	info "紐付け結果: " + update_response;
}

使う前に、次の点を自社の環境に合わせてください。

  • crm_connection は、Zoho CRMのスコープ(ZohoCRM.modules.ALL と ZohoCRM.coql.READ)を付けたコネクションの名前です
  • 会社区分を選択リストにしている場合は、Sales_Company_Category に入れる値を選択肢の表示値に合わせます
  • 接頭辞の英字(例では X)は、基幹システムの出力仕様に合わせます。ここを関数の外に出したい場合は、組織変数に持たせると変更しやすくなります

いくつか補足します。

  • COQLでテキストの値を比べるときは、値をシングルクォートで囲みます。変更前は数値のコードで比べていたので、クォートがありませんでした。キーをテキストにしたら、クォートの付け忘れに注意してください
  • limit 2 にしているのは、「1件か、それ以上か」が分かれば十分だからです。2件返ってきたら、データの側に重複があるという合図です
  • 更新リクエストの trigger に空の配列を渡すと、その更新ではワークフローなどの自動処理が動きません(Zoho CRM API:Insert Records)。1回の更新APIで扱えるのは100件までです(Update Records)

ワークフローは、販売実績の「作成時」に、この関数へレコードIDを渡す設定にします。

同じ会社が両社にいる場合

提携先と自社の両方で、同じ会社が得意先になっていることがあります。複合キーでは、この2件は別レコードとして共存します。統合は機械的に行わず、人が判断する方針にしました。取引条件や担当が違う得意先を、名前が同じというだけで1件にすると、売上の集計先が変わってしまうためです。重複の候補を見つけても自動で統合しない設計は「CRMの重複チェックは「自動マージしない」設計にする」でも解説しています。

名称の先頭に会社名の括弧書きを付けた点にも注意が要ります。この案件では、名刺管理ツールが会社名の一致で取引先に紐付ける仕組みを使っていました。名称を変えると、この一致が外れる可能性があります。今回は提携先側でそのツールを使っていなかったため問題にしませんでしたが、将来使う場合は改めて確認が必要です。正式名称は変えずに、区分項目で見分けるほうが安全です。

なお、この案件では後にCSVの取込方法を見直しました。ここで解説したのは、キー設計と付け替えの考え方です。

まとめ

  • 別会社のデータを同じCRMに載せるときは、まず「コード単独で一意か」を疑います(移行時に重複削除のキーを誤る例は「商品マスタ移行で「商品名+仕入先名」の重複削除をしてはいけない理由」)
  • 区分とコードをつないだテキストの複合キーを新設し、既存のコードは残します
  • 付け替えるのは、CRMの重複禁止・DataPrepの照合キー・Delugeの検索キーの3か所です。重複禁止は「新キーを有効にしてから旧キーを外す」順に進めます
  • 設計の前提は、実データのサンプルが届いた時点で照合し直します。この案件でも、区分列が届く前提から、接頭辞で区分を作る形に変わりました

株式会社etikaのCRMサポートセンターでは、基幹システムとZoho CRMの連携や、データ取込の設計・改修を支援しています。取込データの重複や紐付けの誤りでお困りの際は、販売管理システムとCRMの統合構築サービスからお気軽にご相談ください。

よくある質問

コードの先頭に英字が付いて区別できるなら、複合キーは要らないのでは?

接頭辞は基幹システム側の出力ルールなので、将来変わる可能性があります。CRM側で区分を独立した項目として持ち、キーの組み立て方を1か所で決めておくと、ルールが変わっても直す場所が限られます。区分項目は一覧の絞り込みやレポートにも使えます。

既存の得意先コード項目はどうすればよいですか?

元のコードとしてそのまま残し、重複禁止だけを外します。ただし英字付きのコードは数値型の項目には入らないため、テキスト型でコードを保持する項目が別に必要になることがあります。

同じ会社が両社の得意先として登録されている場合はどうなりますか?

複合キーでは別レコードとして共存します。統合するかどうかは機械的に決めず、人が判断する方針にしました。自動で統合すると、取引条件や担当が違う得意先を誤ってまとめる恐れがあるためです。

取込を止めずに作業できますか?

既存データにキーを後付けしている間に日次の取込が走ると、値が競合します。後付けの時間帯は取込のスケジュールを止めるか、実行時刻と重ならないように作業してください。