Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

概要

目的

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 (Salesforce API のラッパー)
  • 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が「コンシューマ鍵」、クライアントシークレットが「コンシューマの秘密」といった名前で公開されていて少し探しにくかったです。

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 を保存する属性(列)を追加します。

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 は databasedotcomRestforce の2つがあり、ググってヒットする情報も記事執筆時点で二分されておりますが databasedotcom の README に

DEPRECATED: Please use restforce instead.

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

Restforce のインストール

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

gem 'restforce'

インストールします。

$ bundle

ラッパーのラッパー

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

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

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! メソッドは属性の更新がなければ何もしない実装になっているので、アプリケーションレイヤーで属性に更新がかかったかを判定する必要はありません。

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 で背負うのであれば、こんな感じでやればレコード量と構造次第ですが早くなるかもしれません。

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 に追加して

gem 'whenever', require: false

インストールします。

$ bundle

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

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 なり環境変数なりに登録後、初回の同期を手動で行います。

$ bundle exec rails create_accounts_from_salesforce RAILS_ENV=production

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

$ bundle exec whenever --update-crontab
OgiharaRyo
ソフトウェアエンジニア / 個人事業主 / Ruby on Rails / dvorak配列 / Ergodox EZ / リモートワーカー / ヘビーゲーマー
https://ogihara-ryo.github.io
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away