[Ruby on Rails] Salesforce のオブジェクトを Rails のデータベースに反映する

# 概要

## 目的

Salesforce のオブジェクトを Ruby on Rails アプリケーション上のデータベースに反映します。今回のサンプルコードは、 `Account` というオブジェクトの全レコードを Rails アプリケーションの `accounts` テーブル( `Account` モデル)に反映します。

## 同期スケジュール

サーバーが cron に対応していると仮定して、5分に1度 cron で rake タスクを実行して新規のレコードを反映します。既存レコードの属性を変更された場合の反映まで5分に1度行うと、サーバーやデータベースへの負荷が大きいので今回は毎日0時に反映することにします。つまり、新規レコードの反映は5分毎、更新レコードの反映は毎日0時に行います。

## 環境

- Ruby 2.5.1
- Rails 5.2.1

## 使用するGem

この記事のサンプルは、以下の2つの Gem を README に書いてある通りに使用して組み合わせることにより、定期的に Salesforce からデータを受け取る手順を書いただけの単純なものです。

- [Restforce](https://github.com/restforce/restforce) (Salesforce API のラッパー)
- [Whenever](https://github.com/javan/whenever) (アプリケーションレイヤーで cron のスケジュールを管理する)

## 用意するもの

### Salesforce API を利用するための情報

最低限、Salesforce API を利用するための情報を用意する必要があります。credentials や環境変数を参照するようにすると良いでしょう。今回のサンプルでは、とりあえず環境変数に記述されているものとして話を進めます。各情報を得る場所は頻繁に変更されるので salesforce の最新のドキュメントを参照するか、気合いで見つけて頂くようお願いします。(記事中最難関)

私は https://developer.salesforce.com/docs/atlas.ja-jp.api_rest.meta/api_rest/quickstart_oauth.htm を参考に取得しました。記事執筆時点ではクライアントIDが「コンシューマ鍵」、クライアントシークレットが「コンシューマの秘密」といった名前で公開されていて少し探しにくかったです。

```bash
SALESFORCE_HOST="login.salesforce.com"
SALESFORCE_CLIENT_ID="id"
SALESFORCE_CLIENT_SECRET="secret"
SALESFORCE_USER_NAME="user@example.com"
SALESFORCE_PASSWORD="password"
```

### Salesforce の ID を保存する属性

同期先のテーブル(今回のサンプルでは `accounts` テーブル)に Salesforce の ID を保存する属性(列)を追加します。

```rb:yyyymmddhhnnss_add_salesforce_id_to_accounts.rb
class AddSalesforceIdToAccounts < ActiveRecord::Migration[5.2]
  def change
    add_column :accounts, :salesforce_id, :string
  end
end
```

この属性を見て新規レコードなのか更新レコードなのかを判定します。作成日時や更新日時を見て判定しても良いのですが、どちらにせよ Salesforce の一意な ID は Rails アプリケーションに保存していた方が良いでしょう。

# Salesforce 通信の準備

Salesforce API のラッパーの Gem は [databasedotcom](https://github.com/heroku/databasedotcom) と [Restforce](https://github.com/restforce/restforce)　の2つがあり、ググってヒットする情報も記事執筆時点で二分されておりますが `databasedotcom` の README に

> DEPRECATED: Please use restforce instead.

と書かれている通り、現在は Restforce 一択となっています。

## Restforce のインストール

書くまでもありませんが、Gemfile に追加して

```rb
gem 'restforce'
```

インストールします。

```console
$ bundle
```

## ラッパーのラッパー

`Restforce::Data::Client` のインスタンスを返すクラスを作っておきます。API のラッパーに何を使うかと、Salesforce の各種情報の在り処(下記のサンプルでは環境変数)を隠蔽するためだけのクラスです。

```rb:app/lib/salesforce_api.rb
class SalesforceApi
  attr_reader :client

  def initialize
    @client = Restforce.new(
      host: ENV['SALESFORCE_HOST'],
      username: ENV['SALESFORCE_USER'],
      password: ENV['SALESFORCE_PASSWORD'],
      client_id: ENV['SALESFORCE_CLIENT_ID'],
      client_secret: ENV['SALESFORCE_CLIENT_SECRET']
    )
  end
end
```

# 新規レコード作成用の rake タスク

Salesforce の `Account` オブジェクトの `Id` と `Name` 属性を、アプリケーションの `accounts` テーブルの `salesforce_id` と `name` 属性に同期します。タスクの内容はシンプルで、アプリケーションが知らない `salesforce_id` のみをアプリケーションに追加するだけです。このサンプルは API の結果を全て取得してループをぶん回す大変富豪的な実装になっています。Salesforce の API を叩ける回数には限りがありますし、アプリケーションのスペックにも限りがあるのでバランスを取って最適な実装を選択する必要があります。

```rb:lib/tasks/create_accounts_from_salesforce.rb
desc 'Create accounts from salesforce'

task create_accounts_from_salesforce: :environment do
  Account.transaction do
    salesforce_ids = Account.pluck(:salesforce_id)

    SalesforceApi.new.client.query('select Id,Name from Account').each do |o|
      Account.create!(salesforce_id: o.Id, name: o.Name) if salesforce_ids.exclude?(o.Id)
    end
  end
end
```

# 既存レコードの属性更新用の rake タスク

アプリケーションが知っている `salesforce_id` の属性を `update!` に渡します。ActiveRecord の `update` ,  `update!` メソッドは属性の更新がなければ何もしない実装になっているので、アプリケーションレイヤーで属性に更新がかかったかを判定する必要はありません。

```rb:lib/tasks/update_accounts_from_salesforce.rb
desc 'Update accounts from salesforce'

task update_accounts_from_salesforce: :environment do
  Account.transaction do
    SalesforceApi.new.client.query('select Id,Name from Account').each do |o|
      account = Account.find_by(salesforce_id: o.Id)
      account.update!(name: o.Name) if account.present?
    end
  end
end
```

## 余談

上記もまた大変富豪的な実装で、Salesforce の全アカウントを取得してループをぶん回し、毎回 `find_by` でデータベースの照会をしています。API を叩く回数とアプリケーションのデータベースにアクセスする回数を減らして負荷を Ruby で背負うのであれば、こんな感じでやればレコード量と構造次第ですが早くなるかもしれません。

```rb
objects = client.query('select Id,Name from Account').group_by(&:Id)
salesforce_ids = objects.map(&:first)
Account.where(salesforce_id: salesforce_ids).find_each do |account|
  account.update!(name: objects['0012800001b6FF3AAM'].first.Name)
end
```

いろいろやってみてベンチマーク取ってみて、API の利用可能数やサーバースペックと相談しつつ実装していく形になります。


# 同期スケジュールの登録

とりあえず新規用と更新用の rake タスクを作ったので、次は5分に1度と毎日0時にこれらを実行する機構の実装を行います。

## Whenever のインストール

これも書くまでもありませんが、Gemfile に追加して

```rb
gem 'whenever', require: false
```

インストールします。

```console
$ bundle
```

## スケジュールファイルの作成

[Whenever](https://github.com/javan/whenever) の README に書いてある通りにスケジュールファイルに記述します。5分に1度 `create_accounts_from_salesforce` の rake ファイルを実行し、毎日0時に `update_accounts_from_salesforce` の rake ファイルを実行するように指定します。 `rake` メソッドはデフォルトで `RAILS_ENV=production` で crontab に書き込んでくれます。

```config/schedule.rb
every 5.minutes do
  rake 'create_accounts_from_salesforce'
end

every 1.day, at: '00:00 am' do
  rake 'update_accounts_from_salesforce'
end
```

これで `whenever --update-crontab` を実行すれば crontab へ反映してくれます。



# サーバー準備

ここまででアプリケーションのコードは書き終えたので、あとはサーバーにデプロイして Salesforce の各情報を credentials なり環境変数なりに登録後、初回の同期を手動で行います。

```console
$ bundle exec rails create_accounts_from_salesforce RAILS_ENV=production
```

そして、Whenever の crontab 書き込みコマンドを実行して終了です。お疲れ様でした。

```console
$ bundle exec whenever --update-crontab
```