Edited at
HerokuDay 1

Heroku Connectの注意点

More than 3 years have passed since last update.


Heroku Connectとは

簡単に言うとSalesforceのDBとHeroku Postgresを双方向に同期してくれるアドオンです。なのでSalesforce Advent Calendarの方が向いていたのかも。

2010年にHerokuがSalesforceに買収されて以来、特に何が変わるでもない状況が続いていたのですが、2013年末ごろに発表され、2015年5月にGAした公式の連携機能です。当初はアドオンマーケットプレイスにいなかったんですが、今年に入ってから(多分このころ)誰でも利用できるようになりました。ただしDemoエディション。有料プランはHeroku Enterprise前提のようです。

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


Demoエディションの主な制限

Demoエディションの主な制限は同期できるレコード数と同期間隔です。あとExternal Objectという機能も使えないのですが、これは有料プランでも上位プランになるらしいので使ったことがありません。


  • 同期できるレコード数が最大1万

  • Salesforce→Heroku方向の同期間隔が最短10分(有料プランは最短2分)

  • Salesforceでのレコード更新を検出して短時間で同期するEvented Modeが利用できない(と書いてあるがなぜか利用できる)

  • Dev Centerには書いてないが、Heroku→Salesforce方向の同期間隔もだいぶ遅い(10秒のはずだが1分前後かかる)

基本的には、同じことを自力で開発しようと思ったらできる範囲のことしかやってくれません。非公開のAPIを使っているとかはないようです。ただし、SalesforceのAPIコール数制限にはほとんどカウントされません。自力で開発するとAPIコール制限がかなり厳しいので助かります。


試してみる

Dev CenterのGetting Started on Heroku with Heroku Connectに添って進めていけば、SalesforceのテーブルがPostgresのテーブルと同期するところまで試せます。サンプルアプリはsinatraとsinatra-activerecordを使っています。

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

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


APIとCLI

Heroku Schedulerと違ってAPIとCLIが用意されています。heroku plugins:install heroku-connect-pluginでプラグインをインストールすることでCLIのサブコマンドを利用できるようになります。

よく使うのはheroku connect:importheroku 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でレコードを新規作成する場合はsfidnullにします。Heroku→Salesforce同期でSalesforceにinsertされ、その後のSalesforce→Heroku同期の際にsfidが同期されます。

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

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して参照関係を結ぶことができます。

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_logstateカラムで確認できるので、スケジューラなどで検出できるようにしておいたほうが良いでしょう。

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

なお、イベントログテーブルとWebhookで、Salesforce→Herokuの同期失敗を検出できたのですが、deprecatedになってしまいました。


監査項目とRailsのtimestamp

Winter'16の新機能で、レコード作成時にCreatedDateなどを設定できる権限が追加されたのですが、まだ対応していません。もともと監査項目はHeroku側で設定していても同期されないのですが、その処理によって無視されているようです。

ただし、監査項目だけを更新しても「空update」されます。あまり必要になることはないですが、おそらくこれが唯一の「空update」する方法ですSQLでupdateすれば「空update」できました

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

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

module SalesforceTimestamp

def timestamp_attributes_for_update
super + [:systemmodstamp]
end

def timestamp_attributes_for_create
super + [:createddate]
end
end


削除の検出

イベントログテーブルとWebhookがあるのですが、deprecatedになってしまいました。まだ使えますが。将来的にも利用できる、簡単に削除を検出する方法はまだありません。

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

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


テーブル名、カラム名が気に入らない

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

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



  • alias_attributeを使う: 無駄にソースが長くなるし、SQLを使うときに混乱します

  • Postgresの更新可能ビューを使う: ビューの作成は自動化できるので悪くないのですが検索性が問題でした。また、viewをdropしないと同期設定を変更できないのも面倒です。(というか変更できてしまうのにテーブルやカラムが残ったままになるのが困る。そのままだと容量無駄遣いになるし、同期解除したテーブルやカラムをもう一度同期しようとした時に初めてエラーになる)


バージョン管理とCI

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

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

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

jsonとSchemafileをこんな感じでソートしてます。

https://gist.github.com/tarot/7f1358ac3f05eb2544d1