PCAクラウドの顧客データを他のSaaSに同期するC#アプリケーションを作成する

#はじめに
本記事では、CData ODBCドライバを使って、PCA商魂・商管DXの顧客データを、Salesforce、kintoneと同期するWindowsフォームのC#アプリケーションを作成します。PCA商魂・商管DXは、ピー・シー・エー株式会社が提供するSaaS型の基幹業務ソフトです。

#使用するCData製品
- [CData ODBC Driver for PCA Sales](https://www.cdata.com/jp/drivers/pcasales/odbc/)
- [CData ODBC Driver for Salesforce](https://www.cdata.com/jp/drivers/salesforce/odbc/)
- [CData ODBC Driver for kintone](https://www.cdata.com/jp/drivers/kintone/odbc/)

※こちらのリンクから30日の評価版、またはベータ版のインストーラを(.exe)をダウンロードできますので、インストールしてください。

#前提
- SalesforceのAPIを利用できる権限のアカウントを保有していること
- kintoneのアカウントを保有しており、アプリテンプレートの「営業支援パック」でアプリを作成済みである
- PCA商魂・商管DXのアカウントを保有していること
- 上記の3つのODBCドライバがインストール済みであること
- Visual Studio がインストール済みであること　※今回はVisual Studio Community 2017を使用しました

#ODBCドライバのDSN設定
各ODBCドライバのインストール後、またはインストール先フォルダにある「ConfigureODBC」を起動すると、下記のような「DSN構成」画面が立ち上がりますので、それぞれ接続情報を入力します。
<img width="325" alt="ConfigureODBC.png" src="https://qiita-image-store.s3.amazonaws.com/0/288655/4647ac7d-53e6-e905-f899-9a477f2e3b7b.png">

各SaaSの接続情報は下記のとおりです。

| PCA Salesの接続情報 | 設定項目 |
|:-----------------|:---------------|
| クライアントID | OAuth Client Id |
| クライアントシークレット | OAuth Client Secret |
| コールバックURL | Callback URL |
| デフォルトデータ領域 | Default Data Area |
| プロダクトコード | Product Code |
| Web APIサーバのURL | Url |
※コールバックURL、クライアントID、クライアントシークレットについては、[PCA Web API 利用準備](https://qiita.com/sugimomoto/items/8837226105891c92d574#pca-web-api-利用準備)をご覧ください。

| Salesforceの接続情報 | 設定項目 |
|:-----------------|:---------------|
| ユーザID | User |
| パスワード | Password |
| セキュリティートークン | Security Token |

| kintoneの接続情報 | 設定項目 |
|:-----------------|------------------|
| ユーザID | User |
| パスワード |Password|
| kintoneのサブドメイン含むサイトURL |Url|
※Urlは、https://***.cybozu.com/k/の「/k」は不要です

#プロジェクト作成
Visual Studioで「Windowsフォームアプリケーション (.Net Framework)」プロジェクトを新規作成し、コンテナとコントロールを配置していきます。
ここで解説するプログラムは、[こちら](https://github.com/kankiry/PCA2Others)からダウンロードいただけます。

##1．TableLayoutPanelコンテナ
Formのデザイナー画面で、Formを適当な大きさに拡大し、その中に「1行×3列」の「TableLayoutPanel」コンテナを配置します。サイズをFormいっぱいにするため「Dock」プロパティを「Fill」に設定してください。

##2．TabControlコンテナ
作成したTableLayoutPanelコンテナの両端の列の中に「TabControl」コンテナを1つずつ配置します。この「TabControl」はどちらもサイズを最大にするため「Dock」プロパティを「Fill」に設定してください。また、左側のTabControlのタブページ数は1、右側のタブページ数は2に設定します。各タブページの見出し（Textプロパティ）は下記のように設定します。

- フォーム左側タブページ1 : PCASales
- フォーム右側タブページ1 : Saleforce
- フォーム右側タブページ2 : kintone

##3．DataGridViewの配置とデータソースの設定
これまでの作業でタブページが左側に1つ、右側に2つあります。各タブページの中に「DataGridView」を配置します。サイズは最大にするため「Dock」プロパティを「Fill」に設定してください。

この時点では、Formは下記のように表示されます。
![InitialDesign.PNG](https://qiita-image-store.s3.amazonaws.com/0/288655/71e086be-91b8-2de5-a2ee-5de876106e8e.png)

次に、3つの「DataGridView」にデータソースを設定します。下記のように「DataGridView」を選択した際に表示される「▶」をクリックして、「データソースの選択」の「（なし）」右の下矢印をクリックして、「プロジェクトデータソースの追加」リンクをクリックします。
![DataGridViewDataSource_arrow.png](https://qiita-image-store.s3.amazonaws.com/0/288655/04b6b769-dbbe-7aac-bce8-f9088b80669d.png)

「データソース構成ウィザード」が立ち上がりますので「データベース」―「データセット」を選択して進みます。
<img width="324" alt="DataSourceWizard.png" src="https://qiita-image-store.s3.amazonaws.com/0/288655/1c25eb9f-9cc3-005c-af6d-774e449aad14.png">
<img width="409" alt="DataSourceWizard2.png" src="https://qiita-image-store.s3.amazonaws.com/0/288655/6c8234fa-9a3c-730d-ef3d-7fa119139d60.png">

次の「データ接続の選択」ウィンドウでは「新しい接続」をクリックして、さらに「データソースの選択」ウィンドウから「Microsoft ODBCデータソース」を選択して「OK」ボタンをクリックします。
<img width="551" alt="ODBCDataSource_arrow.png" src="https://qiita-image-store.s3.amazonaws.com/0/288655/1accb732-f768-61cd-e0a2-c721f8cf5f34.png">

続いて「接続の追加」ウィンドウでは「ユーザーデータソース名またはシステムデータソース名を使用」のドロップダウンリストから作成済みの今回使用するデータソースを選択して「テスト接続」をクリックします。テスト接続に成功したら「OK」ボタンをクリックします。3つのDataGridViewには、それぞれ下記のデータソースを指定します。

- フォーム左側タブページ1 : CData PCASales Source
- フォーム右側タブページ1 : CData Saleforce Source
- フォーム右側タブページ2 : CData kintone Source

<img width="209" alt="ODBCDataSource2_alpha3.png" src="https://qiita-image-store.s3.amazonaws.com/0/288655/0b0bd18b-23aa-e3f9-3145-bb320c05a4de.png">

ここでは何もせず「次へ」をクリックします。
<img width="328" alt="DataSource4.png" src="https://qiita-image-store.s3.amazonaws.com/0/288655/3b621b88-db73-c0ab-67f6-e894505c2376.png">

プロジェクト内のアプリケーション構成ファイルに保存する接続文字列に適当な名前をつけて、「次へ」を進みます。
<img width="408" alt="DataSourceWizard4.png" src="https://qiita-image-store.s3.amazonaws.com/0/288655/f27bbd1f-abc4-c246-4930-625ebc82e90b.png">

選択したデータソースが持つテーブルのリストが表示されるので適当なテーブルをチェックし、データセット名を入力して「完了」ボタンをクリックします。
<img width="408" alt="DataSourceWizard5.png" src="https://qiita-image-store.s3.amazonaws.com/0/288655/208b80d0-5fec-8d52-da01-61de7eba8d8b.png">

3つの各データソースで使用するテーブルと、そのデータセット名は下記のとおりです。このデータセット名は、c#プログラムで参照するため、正しく設定してください。

- PCA Sales
  - テーブル名 : MasterTMS
  - データセット名 : DataSetPCASales
  
- Salesforce
  - テーブル名 : Account
  - データセット名 : DataSetSalesforce

- kintone
  - テーブル名 : __顧客管理_営業支援パック_
  - データセット名 : DataSetKintone

データソース設定が完了すると、下記のように、そのDataGridViewにヘッダー行が表示されます。
<img width="559" alt="FormDesign2.png" src="https://qiita-image-store.s3.amazonaws.com/0/288655/edb28471-546a-73f0-2703-edc8d1391242.png">

このままでは不要な項目が多く見づらいため、各DataGridViewの不要なカラムを非表示にします。DataGridViewを選択した際に表示される「▶」をクリックして、そこで表示されるメニューから「列の編集」をクリックします。「列の編集」ウィンドウが開きますので、下記のように不要なフィールド名を選択後、「削除」ボタンをクリックします。
![FormDesign4_arrow.png](https://qiita-image-store.s3.amazonaws.com/0/288655/8bb6d0ee-3ca6-1659-79c6-c48cc91c2415.png)

各DataGridViewに表示するフィールドは下記のとおりです。

- PCA Sales（フォーム左側）
  - TokuisakiCode
  - TokuisakiMei1
  - TokuisakiMei2
  - YubinBango
  - Jyusho1
  - Jyusho2
  - AitesakiTelNo
  - AitesakiFaxNo

- Salesforce（フォーム右側のタブページ1）
  - Id
  - Name
  - BillingStreet
  - BillingPostalCode
  - Phone
  - Fax

- kintone（フォーム右側のタブページ2）
  - RecordId
  - Revision
  - 郵便番号
  - 顧客名
  - 住所
  - TEL
  - FAX

以上の設定作業を、3つのDataGridViewに行ってください。完了後は「▶開始」ボタンをクリックして、アプリケーションを起動してみます。下記のように、各DataGridViewにデータソースがバインドされ、レコードの指定した項目のみが表示されます。
<img width="494" alt="TestRun.png" src="https://qiita-image-store.s3.amazonaws.com/0/288655/3a6a1231-883e-63a8-2599-b9a6ea1eee87.png">

#ボタンの配置と同期処理の実装
Formのデザイナー画面で、真ん中の列に「Button」を配置します。そのボタンが中央に来るように「Anchor」プロパティを「None」に設定してください。また「Text」プロパティに“>>”と入力します。最後に、配置したボタンをダブルクリックし、クリックイベントハンドラを追加します。この時、エディターで「Form1.cs」が開かれますので、下記のコードをコピー＆ペーストしてください。

```c#
        private void button1_Click(object sender, EventArgs e)
        {
            button1.Enabled = false;

            if (tabControl2.SelectedTab.Text == "Salesforce")
            {
                PCAtoSalesforce();
            }
            else
            {
                PCAtoKintone();
            }

            button1.Enabled = true;

        }

        private void PCAtoSalesforce()
        {
            dataGridView1.Rows.Cast<DataGridViewRow>()
                 .Where(o => o.Selected)
                 .Select((o, i) => new { Row = (DataSetPCASales.MasterTmsRow)((DataRowView)o.DataBoundItem).Row, Index = i })
                 .ToList()
                 .ForEach(selected =>
                 {
                     var name = selected.Row.TokuisakiMei1 + (selected.Row.IsTokuisakiMei2Null() ? "" : selected.Row.TokuisakiMei2);
                     var row = dataSetSalesforce.Account.Rows.Cast<DataSetSalesforce.AccountRow>()
                     .Where(o => o.Name == name)
                     .FirstOrDefault();

                     if (row is null)
                     {
                         row = dataSetSalesforce.Account.NewAccountRow();
                         //row.Id = "";
                         row.Id = selected.Index.ToString();
                     }

                     row.Name = name;
                     row.BillingPostalCode = selected.Row.IsYubinBangoNull() ? "" : selected.Row.YubinBango;
                     row.BillingStreet = (selected.Row.IsJyusyo1Null() ? "" : selected.Row.Jyusyo1) + (selected.Row.IsJyusyo2Null() ? "" : selected.Row.Jyusyo2);
                     row.Phone = selected.Row.IsAitesakiTelNoNull() ? "" : selected.Row.AitesakiTelNo;
                     row.Fax = selected.Row.IsAitesakiFaxNoNull() ? "" : selected.Row.AitesakiFaxNo;

                     if (row.Id == selected.Index.ToString())
                     {
                         dataSetSalesforce.Account.AddAccountRow(row);
                     }

                     accountTableAdapter.Update(dataSetSalesforce);
                 });

            accountTableAdapter.Fill(dataSetSalesforce.Account);
        }

        private void PCAtoKintone()
        {
            dataGridView1.Rows.Cast<DataGridViewRow>()
                .Where(o => o.Selected)
                .Select((o, i) => new { Row = (DataSetPCASales.MasterTmsRow)((DataRowView)o.DataBoundItem).Row, Index = i })
                .ToList()
                .ForEach(selected =>
                {
                    var name = selected.Row.TokuisakiMei1 + (selected.Row.IsTokuisakiMei2Null() ? "" : selected.Row.TokuisakiMei2);
                    var row = dataSetKintone._顧客管理_営業支援パック_.Rows.Cast<DataSetKintone._顧客管理_営業支援パック_Row>()
                    .Where(o => o.顧客名 == name)
                    .FirstOrDefault();

                    if (row is null)
                    {
                        row = dataSetKintone._顧客管理_営業支援パック_.New_顧客管理_営業支援パック_Row();
                        row.RecordId = selected.Index
                        + (dataSetKintone._顧客管理_営業支援パック_.Rows.Cast<DataSetKintone._顧客管理_営業支援パック_Row>().Max(o => (int?)o.RecordId) ?? 0)
                        + 1;
                        row.Revision = 0;
                    }

                    row.顧客名 = name;
                    row.郵便番号 = selected.Row.IsYubinBangoNull() ? "" : selected.Row.YubinBango;
                    row.住所 = (selected.Row.IsJyusyo1Null() ? "" : selected.Row.Jyusyo1) + (selected.Row.IsJyusyo2Null() ? "" : selected.Row.Jyusyo2);
                    row.TEL = selected.Row.IsAitesakiTelNoNull() ? "" : selected.Row.AitesakiTelNo;
                    row.FAX = selected.Row.IsAitesakiFaxNoNull() ? "" : selected.Row.AitesakiFaxNo;

                    if (row.Revision == 0)
                    {
                        dataSetKintone._顧客管理_営業支援パック_.Rows.Add(row);
                    }

                    顧客管理_営業支援パック_TableAdapter.Update(dataSetKintone);
                });

            顧客管理_営業支援パック_TableAdapter.Fill(dataSetKintone._顧客管理_営業支援パック_);
        }
```

プロジェクトをリビルド後、「▶開始」ボタンをクリックし、アプリケーションを起動します。PCASalesのDataGridViewで適当な行を選択した状態で、“>>”ボタンをクリックしてください。右側の表示されているタブのDataGridViewにそのレコードが追加されれば成功です。今回のプログラムは、右側に表示中のDataGridViewにバインドされているデータソースに対して、データの追加・更新を行いますので、タブを切り替えて「Salesforce」と「kintone」双方で試してみてください。
![SyncTest_arrow.png](https://qiita-image-store.s3.amazonaws.com/0/288655/e8e6a9c2-7e59-ed57-ae9c-676e3294bbf6.png)

「kintone」タブでは成功しますが、「Salesforce」タブでは、「列'IsDeleted'にnullsを使用することはできません」というエラーが発生し失敗します。
![SalesforceError1.PNG](https://qiita-image-store.s3.amazonaws.com/0/288655/86cfdf9e-8628-ab5a-6d23-053159a0af77.png)

このエラーは、SalesforceタブのDataGridViewにバインドされたデータソース「DataSetSalesforce」のNull値を許容しないフィールドがNullのまま、レコードが追加されたことが原因です。今回扱うデータは会社名、郵便番号、住所、TEL、FAXのみで、その他のフィールドは初期化もしないため、原因となっている「DataSetSalesforce」のフィールドのプロパティを変更します。

Visual Studioの「ソリューションエクスプローラー」から「DataSetSalesforce.xsd」を右クリックし、表示されるメニューから「デザイナーの表示」を選択します。
![xsdEditor.PNG](https://qiita-image-store.s3.amazonaws.com/0/288655/4b5f42dd-6732-04a8-5784-a7231faeef4a.png)

下記のようなペインが表示されます。ここに表示されているフィールド名をクリックするとプロパティが表示されます。これを見ると、「IsDeleted」フィールドの「AllowDBNull」プロパティはFalseなので、これをTrueに変更します。この他に「OwnerId」、「CreatedDate」、「CreatedById」、「LastModifiedDate」、「LastModifiedById」、「SystemModstamp」、これらのフィールドについても、同様に「AllowDBNull」プロパティをTrueに変更します。
![xsdEdit2.png](https://qiita-image-store.s3.amazonaws.com/0/288655/a2e96376-4395-ae43-6ecd-a40466243b42.png)

プロジェクトをリビルド後、アプリケーションを起動し、「Salesforce」タブのDataGridViewにレコードが追加できることを確認してください。

次に「kintone」タブを選択し、追加済みのレコードを選択し、“>>”ボタンをクリックしてみてください。
![UpdateError2.PNG](https://qiita-image-store.s3.amazonaws.com/0/288655/47a9ae8c-7072-ce46-a76c-21857a0eeca0.png)

「From1.cs」内の下記のUpdateするところで「[RecordId]列は読み取り専用です。この列の値を更新出来ません。」というエラーが発生します。これは、kintone側のレコード毎に保有のキー項目であるRecordIdは更新出来ないため発生します。そこで、必要な項目のみ更新するように変更します。

先ほどと同様に、Visual Studioの「ソリューションエクスプローラー」から「DataSetKintone.xsd」を右クリックし、表示されるメニューから「デザイナーの表示」を選択します。その後、下記のように、「顧客管理（営業支援パック）　Table Adapter」のアイコンをクリックして、右下の「プロパティ」が「顧客管理（営業支援パック）　Table Adapter」になった事を確認して、「UpdateCommand」のプラスボタンを開き、「CommandText」を右側の「...」をクリックします。　
![xsdEdit3_arrow.png](https://qiita-image-store.s3.amazonaws.com/0/288655/17c641f6-3797-0a1a-1836-4ec783517096.png)

クエリビルダーが開きますので、下記のようにクエリを変更します。

```
UPDATE                    Kintone."顧客管理（営業支援パック）"
SET                       郵便番号 = ?, 住所 = ?, TEL = ?, FAX = ?
WHERE                     (RecordId = ?)
```

![QueryBuilder.PNG](https://qiita-image-store.s3.amazonaws.com/0/288655/bd1bbf62-bdac-fd20-21ab-cdc83a680461.png)

プロジェクトをリビルド後、アプリケーションを起動し、もう1度、「kintone」タブを選択し、追加済みのレコードを選択し、“>>”ボタンをクリックしてみてください。「kintone」タブのDataGridViewの当該レコードの「Revision」が2になれば成功です。
![UpdateSucceed.PNG](https://qiita-image-store.s3.amazonaws.com/0/288655/5e825a06-1ee2-4d49-f536-a2a5886a6ca2.png)

kintoneにログインし、テーブルを見るとデータが追加されていることが分かります。
![kintoneCloud.PNG](https://qiita-image-store.s3.amazonaws.com/0/288655/049845e4-b89c-df3b-7cab-ba89fde3f1d1.png)

実は、この「kintone」タブで起こったエラーは「Salesforce」でも発生します。そのため、同様の手順でクエリビルダーを起動し、下記のようにクエリを編集してください。

```
UPDATE                    Salesforce.Account
SET                       BillingStreet = ?, BillingPostalCode = ?, Phone = ?, Fax = ?
WHERE                     (Id = ?)
```

最後に「PCASales」タブから「Salesforce」タブに、追加済みのレコードも含め、全てのレコードをコピーしてみます。
![SyncSucceed.PNG](https://qiita-image-store.s3.amazonaws.com/0/288655/6d7ec6b7-7739-aa65-f64c-434c6890dd85.png)

実行が完了したら、Salesforceにログインしデータを確認します。Salesforce側で追加したデータを確認できたら完成です。
![SalesforceCloud.PNG](https://qiita-image-store.s3.amazonaws.com/0/288655/b27a1127-086b-d6b6-12ae-302f13b6791a.png)

#まとめ
本記事では、PCA商魂・商管DXの顧客データを、Salesforceとkintoneに同期するWindowsフォームのC#アプリケーションを作成する手順について解説しました。このように、CData ODBCドライバを使うと、最小限のプログラミングでSaaS間のでデータを連携させることができます。プログラムは、[こちら](https://github.com/kankiry/PCA2Others)からダウンロードいただけます。