Salesforce と Dynamics CRM 間で取引先データを同期する方法 (サンプルアプリケーション)

Salesforce の取引先データを Dynamics CRM に同期するサンプルアプリケーションの開発手順を見ていきます。

## ソースコード
このサンプルアプリケーションのソースコードは[こちら](https://github.com/kasoorimethi/Salesforce2DynamicsCRM)からどうぞ。

## CData製品のインストール
まずは CData Software をインストールします。今回必要になるのは、次の2つです。

* [Salesforce ADO.NET Provider](http://www.cdata.com/jp/drivers/salesforce/download/ado/ "Salesforce ADO.NET Provider")
* [Dynamics CRM ADO.NET Provider](http://www.cdata.com/jp/drivers/dynamicscrm/download/ado/ "Dynamics CRM ADO.NET Provider")

## プロジェクト作成
Visual Studio を起動し、新しいWindowsフォームアプリケーションのプロジェクトを作成します。

![プロジェクト作成.png](https://qiita-image-store.s3.amazonaws.com/0/141193/b6717772-8e61-6688-cd08-13d88b63bd4a.png)

## 参照設定
インストールした CData Software の2製品には、いくつかのADO.NETアセンブリが含まれています。
今回必要になるのは次の2つのDLLですので、これらをプロジェクトの参照設定で追加します。

* System.Data.CData.Salesforce.dll
* System.Data.CData.DynamicsCRM.dll

![参照設定.png](https://qiita-image-store.s3.amazonaws.com/0/141193/ce22f375-b801-efc9-d7fe-4b4cb68d0193.png)

## クラス構造
このサンプルアプリケーションに登場するクラスの構造は下図の通りです。

![クラス図.png](https://qiita-image-store.s3.amazonaws.com/0/141193/a7fec70a-7c9b-07b6-9375-485d9d006471.png)

## コーディング
まずは、CData が提供するクラス群を生成するファクトリクラスを設けます。

```csharp
abstract class CDataFactory
{
    public abstract IDbConnection CreateConnection(string connectionString);

    public abstract IDbCommand CreateCommand(string commandText, IDbConnection conn);

    public abstract DbDataAdapter CreateDataAdapter(IDbCommand command);

    public abstract DbCommandBuilder CreateCommandBuilder(DbDataAdapter adapter);
```

上記をベースとして、Salesforce 用と DynamicsCRM 用の2つを設けます。

```csharp
private class CDataFactorySalesforce : CDataFactory
{
    public override IDbConnection CreateConnection(string connectionString)
    {
        return new SalesforceConnection(connectionString);
    }

    public override IDbCommand CreateCommand(string commandText, IDbConnection conn)
    {
        return new SalesforceCommand(commandText, (SalesforceConnection)conn);
    }

    public override DbDataAdapter CreateDataAdapter(IDbCommand command)
    {
        return new SalesforceDataAdapter((SalesforceCommand)command);
    }

    public override DbCommandBuilder CreateCommandBuilder(DbDataAdapter adapter)
    {
        return new SalesforceCommandBuilder((SalesforceDataAdapter)adapter);
    }
}
```

```csharp
private class CDataFactoryDynamicsCRM : CDataFactory
{
    public override IDbConnection CreateConnection(string connectionString)
    {
        return new DynamicsCRMConnection(connectionString);
    }

    public override IDbCommand CreateCommand(string commandText, IDbConnection conn)
    {
        return new DynamicsCRMCommand(commandText, (DynamicsCRMConnection)conn);
    }

    public override DbDataAdapter CreateDataAdapter(IDbCommand command)
    {
        return new DynamicsCRMDataAdapter((DynamicsCRMCommand)command);
    }

    public override DbCommandBuilder CreateCommandBuilder(DbDataAdapter adapter)
    {
        return new DynamicsCRMCommandBuilder((DynamicsCRMDataAdapter)adapter);
    }
}
```

次に、ADO.NET へのアクセス部分をラップするクラスを設けます。
このクラスは、上で作成したファクトリクラスおよび ADO.NET のクラス/インターフェイスを使用することにより、
接続先が Salesforce であるか Dynamics CRM であるかを意識することなく、それぞれのシステムへの接続、および DataTable の生成処理と更新処理を行います。

```csharp
class DbWrapper : IDisposable
{
    private CDataFactory factory;

    private IDbConnection conn = null;


    public DbWrapper(CDataFactory factory, string connectionString)
    {
        this.factory = factory;
        this.conn = factory.CreateConnection(connectionString);
    }

    public void Dispose()
    {
        this.conn.Dispose();
    }

    public DataTable CreateDataTable(string tableName)
    {
        using (var command = this.factory.CreateCommand("SELECT * FROM " + tableName, conn))
        using (var adaptor = this.factory.CreateDataAdapter(command))
        {
            var table = new DataTable(tableName);
            adaptor.Fill(table);

            return table;
        }
    }

    public void Update(DataTable table)
    {
        using (var command = this.factory.CreateCommand("SELECT * FROM " + table.TableName, conn))
        using (var adaptor = this.factory.CreateDataAdapter(command))
        using (var commandBuilder = this.factory.CreateCommandBuilder(adaptor))
        {
            adaptor.InsertCommand = commandBuilder.GetInsertCommand();
            adaptor.UpdateCommand = commandBuilder.GetUpdateCommand();

            adaptor.Update(table);
        }
    }
}
```

当然のことながら、Salesforce の取引先オブジェクトと Dynamics CRM の取引先企業エンティティでは、データ項目が異なります。
そこで、そのマッピングを行うクラスを設けます。  
Salesforce のデータも Dynamics CRM のデータもデータベースのテーブルデータとして扱えるので、
このクラスが行うことは、Salesforce 側の項目名を Dynamics CRM 側の項目名に翻訳して、項目の値をセットするだけです。

```csharp
class AccountMapper : ColumnMapper
{
    private static Dictionary<string, string> NameMapping = new Dictionary<string, string>()
    {
        { "AccountNumber", "AccountNumber" },
        { "AnnualRevenue", "Revenue" },
        // ...省略...
    };

    public override void Map(DataRow source, DataRow destination)
    {
        foreach (var entry in NameMapping)
        {
            destination[entry.Value] = source[entry.Key];
        }
    }
}
```

上記のクラスのオブジェクトを "Account" などの文字列から取得できるようにするためのクラスも設けておきます。

```csharp
static class ColumnMapperRegistry
{
    private static Dictionary<string, ColumnMapper> columnMapperMap = new Dictionary<string, ColumnMapper>();


    static ColumnMapperRegistry()
    {
        columnMapperMap["Account"] = new AccountMapper();
    }

    public static ColumnMapper GetMapper(string name)
    {
        return columnMapperMap[name];
    }
}
```

ここまでで、Salesforce および Dynamics CRM にアクセスする部分が出来ました。次に、フォームにとりかかります。  

取引先データの読み込みやインポートには時間がかかるかもしれないので、処理状況をメッセージ表示するフォームを追加します。  
このフォームは、時間のかかる処理の実行中に自分自身を表示して、状況をメッセージ表示したり、処理が完了したら閉じられるようにします。

![状況表示画面_フォームデザイナ.png](https://qiita-image-store.s3.amazonaws.com/0/141193/a53856a6-1bc2-51b3-cc93-2de1b998a2b0.png)

次にメインのフォームを追加します。  
このフォームには、次のコンポーネントを追加します。

* インポート対象を選択するためのコンボボックス
* データ読み込みを行うボタン
* データを表示するためのデータグリッドビュー×2 (左がSalesforce、右がDynamicsCRM と対応する)
* インポートを実行するボタン

![メイン画面_フォームデザイナ.png](https://qiita-image-store.s3.amazonaws.com/0/141193/c423837f-2245-a7f0-ed4f-feddb4e076fa.png)

インポート対象の種類を選ぶコンボボックスは、今回は単に "Account" という文字列を選択できるものにします。

```csharp
private void MainForm_Load(object sender, EventArgs e)
{
    this.cbObject.DataSource = new string[] { "", "Account" };
}
```

データ読み込みの処理では、先に作成した DbWrapper を使用して DataTable を生成し、
それをデータグリッドビューにバインドします。

アプリケーションの設定に記載された Salesforce および Dynamics CRM の接続情報を使用して、それぞれのシステムへ接続します。

```csharp
using (var salesforce = new DbWrapper(CDataFactory.Salesforce, Properties.Settings.Default.SalesforceConnectionString))
using (var dynamicsCRM = new DbWrapper(CDataFactory.DynamicsCRM, Properties.Settings.Default.DynamicsCRMConnectionString))
{
```

DataTable を生成します。

```csharp
var salesforceDataTable = salesforce.CreateDataTable(tableName);
var dynamicsCRMDataTable = dynamicsCRM.CreateDataTable(tableName);
```

生成した DataTable をデータグリッドビューへバインドします。

```csharp
this.dgvSource.DataSource = salesforceDataTable;
this.dgvDestination.DataSource = dynamicsCRMDataTable;
```

これで、2つのデータグリッドビューに Salesforce および Dynamics CRM それぞれの取引先データが表示されるはずです。

最後に、インポート処理を記述します。

選択されている行を取得して、それを DataRow のリストに変換します。

```csharp
var importTargetRows = this.dgvSource.Rows.Cast<DataGridViewRow>()
    .Where(o => o.Selected)
    .Select(o => ((DataRowView)o.DataBoundItem).Row)
    .Where(o => !string.IsNullOrWhiteSpace(o["AccountNumber"].ToString()))
    .ToList();
```

ここではLINQを使用して、
* 行選択されているもののみ抽出 ` .Where(o => o.Selected) `
* DataRowView に変換 `.Select(o => (DataRowView)o.DataBoundItem)`
* 取引先番号(AccountNumber)が設定されているもののみ抽出 `.Where(o => !string.IsNullOrWhiteSpace(o["AccountNumber"].ToString()))`

ということを行っています。  

同じように、インポート先のデータも DataRowView として取得しますが、取引先番号で引けるように、Dictionaryに変換します。

```csharp
var destinationRowMap = this.dgvDestination.Rows.Cast<DataGridViewRow>()
    .Select(o => ((DataRowView)o.DataBoundItem).Row)
    .Where(o => !string.IsNullOrWhiteSpace(o["AccountNumber"].ToString()))
    .ToDictionary(o => o["AccountNumber"].ToString(), o => o); // AccountNumberをキー、DataRowViewを値とするDictionaryに変換
```

選択されている行の DataRow をループして、Salesforce 側の取引先番号と対応する DataRow が Dynamics CRM 側にあればそれを取得し、なければ新しい DataRow を生成します。
そして上で作成した AccountMapper を使用して、Salesforce 側のデータを Dynamics CRM 側にセットします。

```csharp
foreach (var importTargetRow in importTargetRows)
{
    DataRow dataRow = null;

    if (destinationRowMap.ContainsKey(importTargetRow["AccountNumber"].ToString()))
    {
        dataRow = destinationRowMap[importTargetRow["AccountNumber"].ToString()];
        importUpdate++;
    }
    else
    {
        dataRow = ((DataTable)this.dgvDestination.DataSource).NewRow();
        ((DataTable)this.dgvDestination.DataSource).Rows.Add(dataRow);
        importNew++;
    }

    ColumnMapperRegistry.GetMapper(tableName).Map(importTargetRow, dataRow);
}
```

仕上げに、Dynamics CRM 側の DataTable を Update します。
これにより、DataRow に加えられた変更から、それに対応する UPDATE/INSERT 文が生成され、Dynamics CRM へのデータ更新が実行されます。

```csharp
using (var dynamicsCRM = new CDataWrapper(CDataFactory.DynamicsCRM, Properties.Settings.Default.DynamicsCRMConnectionString))
{
    dynamicsCRM.Update((DataTable)this.dgvDestination.DataSource);
```

## 実行

### 接続情報の設定

接続情報をアプリケーションの設定に記述します。  
プロジェクトを右クリック → プロパティ からプロジェクトのプロパティを開き、「設定」で
"SalesforceConnectionString" と "DynamicsCRMConnectionString" の2つを追加します。

![接続情報の設定.png](https://qiita-image-store.s3.amazonaws.com/0/141193/2a33ef8a-61ec-ebd8-d822-62ed992054d7.png)

Salesforce の接続文字列は、以下の形式になります。  
User=<ユーザ名>;Password=<パスワード>;Security Token=<セキュリティトークン>;

Dynamics CRM の接続文字列は、以下の形式になります。  
User=<アカウント名>;Password=<パスワード>;URL=<サーバー組織ルート>;CRM Version=CRM Online;

### データ読み込み＆同期

左上のコンボボックスで「Account」を選び、選択ボタンをクリックすると、Salesforce および Dynamics CRM のデータが
それぞれに対応するデータグリッドビューに読み込まれます。

![メイン画面_未ロード.png](https://qiita-image-store.s3.amazonaws.com/0/141193/9496a2fd-30e9-2b76-fd4a-c8e318f7e859.png)

![メイン画面.png](https://qiita-image-store.s3.amazonaws.com/0/141193/4573ea7c-065c-abb6-79de-20a883373ed3.png)

データが読み込まれた状態で、左側のデータグリッドビューでインポートしたいデータを選択し、インポートボタンをクリックすると、選択したデータが Dynamics CRM に反映されます。