Heroku Connectの注意点

Heroku Connectとは
====
簡単に言うとSalesforceのDBとHeroku Postgresを双方向に同期してくれるアドオンです。なのでSalesforce Advent Calendarの方が向いていたのかも。
2010年にHerokuがSalesforceに買収されて以来、特に何が変わるでもない状況が続いていたのですが、2013年末ごろに発表され、[2015年5月にGAした](https://blog.heroku.com/archives/2014/5/13/introducing_heroku_connect)公式の連携機能です。当初はアドオンマーケットプレイスにいなかったんですが、今年に入ってから(多分[このころ](https://blog.heroku.com/archives/2015/2/24/Heroku%20Connect%20Demo%20Edition%20Now%20Available%20via%20Heroku%20Button))誰でも利用できるようになりました。ただしDemoエディション。有料プランは[Heroku Enterprise](https://www.heroku.com/enterprise)前提のようです。

しごとでHeroku Connectを使うことが何度かあったので、苦労したところなどを雑多に書いてみたいと思います。

Demoエディションの主な制限
----
Demoエディションの主な制限は同期できるレコード数と同期間隔です。あと[External Object](https://www.pasonatquila.com/about/blog/technical/data-warehousing-heroku-postgres-and-salesforce1-lightning-connect.html)という機能も使えないのですが、これは有料プランでも上位プランになるらしいので使ったことがありません。

- 同期できるレコード数が最大1万
- Salesforce→Heroku方向の同期間隔が最短10分(有料プランは最短2分)
- Salesforceでのレコード更新を検出して短時間で同期するEvented Modeが利用できない(と書いてあるがなぜか利用できる)
- Dev Centerには書いてないが、Heroku→Salesforce方向の同期間隔もだいぶ遅い(10秒のはずだが1分前後かかる)

基本的には、同じことを自力で開発しようと思ったらできる範囲のことしかやってくれません。非公開のAPIを使っているとかはないようです。ただし、[SalesforceのAPIコール数制限にはほとんどカウントされません](https://blog.heroku.com/archives/2015/7/14/heroku_connect_now_with_free_salesforce_api_calls)。自力で開発するとAPIコール制限がかなり厳しいので助かります。

試してみる
====
Dev Centerの[Getting Started on Heroku with Heroku Connect](https://devcenter.heroku.com/articles/getting-started-with-heroku-and-connect-without-local-dev)に添って進めていけば、SalesforceのテーブルがPostgresのテーブルと同期するところまで試せます。サンプルアプリはsinatraとsinatra-activerecordを使っています。

アドオンダッシュボードのGUIで同期設定すると、Heroku Connectが使うPostgresのスキーマにテーブルができるのでマイグレーションする必要はありません。むしろ勝手にテーブルを作るとアドオンGUIでエラーになります。

英語読むのが面倒な人用: http://qiita.com/tarot/items/902f4793d8b5c75e35e8

APIとCLI
====
Heroku Schedulerと違って[APIとCLI](https://devcenter.heroku.com/articles/herokuconnect-api)が用意されています。`heroku plugins:install heroku-connect-plugin`でプラグインをインストールすることでCLIのサブコマンドを利用できるようになります。

よく使うのは`heroku connect:import`と`heroku connect:export`です。アドオンGUIからもインポート・エクスポートできますが、CLIで利用できるのはやはり便利です。

一般的な注意
====
- テーブル単位ですべてのレコードを同期します。「条件を満たすレコードのみ」などはできません。
- 同期するカラムを選ぶことはできますが、「Postgresにだけ存在するカラム」は作成できません。
    - 一応できますがいつエラーになるかわかりません。
    - ただし、Heroku Connectが内部利用するシステムカラムはPostgresにだけ存在してSalesforceには同期されません。
- 数式項目はSalesforce→Postgres方向に同期されるまで更新されません。
    - Evented Modeを利用していれば、Postgres→Salesforceに同期したあと数秒で逆方向に同期されますが、それでも必ずタイムラグがあります
- 親レコードを参照する数式を同期している場合でも、親レコードを更新しただけでは子レコードは同期されません。
    - 親レコードを参照する数式は注意深く利用する必要があります。例えば親レコードの当該項目は絶対に変更しないとか。
    - `RecordType.DeveloperName`くらいなら使っても良いでしょう
- Salesforceでインデックスがついていればインデックスになりますが、インデックスを追加することはできません。
- 数値型は基本的に`double`です。小数点以下0桁でも`double`です。「.0」が表示されてイラッとします。
    - 標準項目がたまにintになります。ズルイ。

Association
====
Salesforceのレコードは変更不可の自動採番IDを持っています。一方、Postgres→Salesforce方向の同期もあるので、PostgresでinsertしたレコードはSalesforceのIDではないIDが必要になります。そこでHeroku Connectで同期するテーブルには、Rails的なauto incrementな`id`と、Salesforce ID用の`sfid`項目が必ず作成されます。

Herokuでレコードを新規作成する場合は`sfid`を`null`にします。Heroku→Salesforce同期でSalesforceにinsertされ、その後のSalesforce→Heroku同期の際に`sfid`が同期されます。

Salesforceのテーブル同士のAssociationには`sfid`を使います。Railsだとこのようになります。

```ruby
class Account < ActiveRecord::Base
  self.table_name = 'salesforce.account'
  has_many :contacts, primary_key: 'sfid', foreign_key: 'accountid'
end

class Contact < ActiveRecord::Base
  self.table_name = 'salesforce.contact'
  belongs_to :account, primary_key: 'sfid', foreign_key: 'accountid'
end
```

同期しているテーブルが親、Herokuのみのテーブルが子になるAssociationは`id`を使うと良いでしょう。同期しているテーブルにHerokuでinsertした後、同期を待たずにHerokuのみのテーブルにinsertして参照関係を結ぶことができます。

```ruby
class Contact < ActiveRecord::Base
  self.table_name = 'salesforce.contact'
  belongs_to :account, primary_key: 'sfid', foreign_key: 'accountid'
  has_many :posts
end

class Post < ActiveRecord::Base
  belongs_to :contact
end
```

親子関係のあるレコードを同時にinsertする方法
====
親オブジェクトが外部IDを持っていれば、子オブジェクトの項目一覧にその外部IDが現れます。例えば`Account.ExternalId__c`という外部IDがあった場合、`Contact`の項目一覧で`Account__ExternalId__c`を選べるようになります。
なおこの項目名の`Account`はオブジェクト名ではなく参照名です。同じオブジェクトを参照する複数の参照関係項目を作成できるためこのようになっています。例えば`Contact`のカスタム項目で、`Account`を参照する`SecondaryAccount__c`を作った場合、`SecondaryAccount__r__ExternalId__c`を同期できるようになります。

この項目はちょっとややこしくて、親オブジェクトの同期設定ページを開いて項目一覧をロードするところまでやると、(同期しなくても)子オブジェクトの同期設定ページに出現するようになります。

`ExternalId__c`を設定した`Account`と、`Account__ExternalId__c`を設定した`Contact`をinsertすれば、これらのレコードは`Contact.AccountId`で関連付けられます。

ただしこれらの項目をAssociationに利用するには注意が必要です。親レコードの外部ID(上の例では`Account.ExternalId__c`)が変更された場合でも、その子レコードの`Contact`は同期されず、昔の`ExternalId__c`のままになるからです。この機能は新規作成時のみ利用すべきです(updateでの利用も少々危険です)。更新時に利用するなら、外部ID項目値を変更しないなどのルールが必要です。

なおこのような外部ID項目は`Event.WhatId`などの"Polymorphic"項目にも出現しますが、利用することはできません。

Salesforce側のオブジェクト定義変更
====
Salesforce側でオブジェクト定義を変更しても自動的には反映されません。項目を削除したなら同期設定からも削除しなければ、その項目を更新した時にエラーになります。このエラーは通知され、エラーを解決すると同期されますが、同期のタイミングが遅れるので注意しましょう。

テキスト項目の長さ変更などが面倒で、一旦同期解除してからもう一度同期するしかありません。スキーマのインポート(`heroku connect:import`コマンド)では対応できないのでマニュアル対応になりがちです。

コンフリクト
====
Heroku Connectはコンフリクトの解決をしません。後勝ち戦略です。ただし、

- Heroku→Salesforceの同期は変更した項目のみ同期します
    - Herokuで更新→同期用のテーブルに変更した項目値をコピー→10秒程度でSalesforceにupdate
    - となるので、Herokuで更新した項目は必ずSalesforceに同期されます
- Salesforce→Herokuは同期対象の全項目を同期します
    - Heroku→Salesforceの同期はSalesforceのレコードを更新するので、必ず次のSalesforce→Heroku同期のタイミングで取り込まれます

このようになっているので大体うまくマージされます。問題はエラーです。

エラーに注意
====
Heroku→Salesforceの同期で、ネットワーク以外のエラーが発生したら放ったらかしにされます。

Heroku→Salesforceの同期はSalesforceの入力規則や暗黙の入力規則(Email項目など)、トリガのエラーなどで失敗します。この時、Postgresのレコードは更新されたままになっているので、~~同じ値でもう一度更新することができません~~→__ActiveRecordのせいでした。SQLのupdateで更新されたカラムを同期するのですが、ActiveRecordは更新のないカラムをupdateしません__。更新していなかった項目を更新した時や、Salesforce側で更新があった時、突然エラーになっていた項目が元に戻るような挙動を示します。

また、Salesforceに更新できないレコード(後から入力規則を追加した時、古いレコードが入力規則に適合しないなど)があると同期失敗が頻発します。特に面倒なのは、主従関係で積み上げ集計を使っている時に、主レコードが更新できないケースです。Herokuからは従レコードしか更新していないのに、主レコードが更新できなくてエラーになって、アプリケーションからはどうしようもなくなることがあります。

Heroku→Salesforceの同期エラーは、Heroku Connectの管理カラム`_hc_lastop`や管理テーブル`_trigger_log`の`state`カラムで確認できるので、スケジューラなどで検出できるようにしておいたほうが良いでしょう。

Salesforce→Herokuの同期に失敗するのを見たことはありません。ただし、validationの不一致によってRailsでinvalidになるレコードが同期されることはよくあります。validationの不一致が不安なら、RecordInvalidもバグトラッカー(Airbrakeなど)に通知するようにしたほうが良いかもしれません。

なお、[イベントログテーブルとWebhook](https://devcenter.heroku.com/articles/herokuconnect#salesforce-event-log-and-webhooks)で、Salesforce→Herokuの同期失敗を検出できたのですが、deprecatedになってしまいました。

監査項目とRailsのtimestamp
====
Winter'16の新機能で、レコード作成時に`CreatedDate`などを設定できる権限が追加されたのですが、まだ対応していません。もともと監査項目はHeroku側で設定していても同期されないのですが、その処理によって無視されているようです。
ただし、監査項目だけを更新しても「空update」されます。あまり必要になることはないですが、~~おそらくこれが唯一の「空update」する方法です~~__SQLでupdateすれば「空update」できました__。

Railsの`timestamp`項目に`CreatedDate`と`LastModifiedDate`または`SystemModstamp`を設定しても問題ないと思います。Salesforce→Heroku同期されたときにSalesforce側の値で上書きされることに注意しましょう。

Railsで規約に従わない`timestamp`項目を利用するには`timestamp_attributes_for_update`と`timestamp_attributes_for_create`を使います。このようなモジュールを`include`してやればよいでしょう。

```ruby
module SalesforceTimestamp
  def timestamp_attributes_for_update
    super + [:systemmodstamp]
  end

  def timestamp_attributes_for_create
    super + [:createddate]
  end
end
```

削除の検出
====
[イベントログテーブルとWebhookがあるのですが](https://devcenter.heroku.com/articles/herokuconnect#salesforce-event-log-and-webhooks)、deprecatedになってしまいました。まだ使えますが。将来的にも利用できる、簡単に削除を検出する方法はまだありません。

これが問題になるのは、Herokuでだけ使っているテーブルと同期しているテーブルに参照関係がある場合や、アドオンなどの外部サービスと連携している場合です。

Herokuで参照関係を作っておいて、スケジュール処理で参照先がないレコードを探して検出したり、Salesforceで削除トリガを作って削除検知テーブルにinsertするなどの対応が必要です。

テーブル名、カラム名が気に入らない
====
特にRailsだと気になると思いますが、Salesforceはキャメルケース文化なので、Postgresに持ってくると単語区切りがわからなくなります。また、カスタムオブジェクト・カスタム項目には必ず`__c`が付くという気持ち悪いルールがあります。

これを回避する方法をいろいろと模索したのですが、結局のところ「我慢する」ことを強く推奨します。SalesforceとHerokuの両方を触っていると、どっちでどんな名前にしていたか分からなくなります。キャメルケース→スネークケース変換と`__c`の除去だけなら覚えられるかと思いましたが、検索性が悪くなるのでお勧めしません。

- `alias_attribute`を使う: 無駄にソースが長くなるし、SQLを使うときに混乱します
- Postgresの[更新可能ビュー](http://www.postgresql.org/docs/9.3/static/sql-createview.html#SQL-CREATEVIEW-UPDATABLE-VIEWS)を使う: ビューの作成は自動化できるので悪くないのですが検索性が問題でした。また、viewをdropしないと同期設定を変更できないのも面倒です。(というか変更できてしまうのにテーブルやカラムが残ったままになるのが困る。そのままだと容量無駄遣いになるし、同期解除したテーブルやカラムをもう一度同期しようとした時に初めてエラーになる)

バージョン管理とCI
====
マッピングのバージョン管理は`heroku connect:export`でエクスポートしたJSONを使えばよいです。ただこいつはマッピングを変更するたびに順番が変わるので、差分が出すぎないようにソートしたほうが良いです。

CIやローカル開発用のDBには[ridgepole](https://github.com/winebarrel/ridgepole)がおすすめです。`schema_search_path`を指定してやればHeroku Connectのテーブルだけエクスポートすることができます。管理テーブル、管理カラムはすべて`_`から始まるので無視してやってもいいのですが、Heroku Postgresから`heroku pg:backups capture`で抜いてきたテーブルをローカルにインポートするときに面倒なので無視しないほうがいいと思います。

ridgepoleでもカラムの順番がしょっちゅう変わるのでソートしたほうが良いです。

jsonとSchemafileをこんな感じでソートしてます。
https://gist.github.com/tarot/7f1358ac3f05eb2544d1