tamuです。
Heroku ConnectでSalesforceのレコードを参照できるようになったので、
Ruby on Railsからその情報を扱ってみたいと思います。
例によって開発はローカル環境で行いたいので、環境構築のお話をしていきます。
環境について
Heroku Connectを有効にしたHerokuはこのような環境になります。
(Ruby on Railsでいうproduction環境)
xxxxx
データベース(herokuが自動で命名する)の中に public
スキーマと、
Salesforce側のデータをいじくることができる salesforce
スキーマがあります。
これと同じような環境をローカルでも構築します。
また、salesforce
スキーマで定義するテーブルの情報(SQL)は、ローカルで開発するHeroku Connect を参考に取得します。
方針
なるべくRuby on Rails標準の手順で環境構築ができるようにしたいです。
1
2
3
4
| $ docker compose up
$ bundle exec rake db:craete
$ bundle exec rake db:migrate
$ bundle exec rails s
|
ローカルだとこんな感じで。
本番環境へのデプロイも docker compose up
が無いだけでほぼ同じ手順でやりたいと考えています。
「ここで psql
から以下のSQLを流します」という手順をあまり組み込みたくないな〜、と。
これと同じ状況にするには以下の準備が必要です。
- salesforceスキーマの作成
- salesforceスキーマ上のテーブルの作成
- search_pathの追加
salesforceスキーマの作成
いろいろと考えましたが、migrationでやるのが良いと判断しました。
まず、migrationファイルを作成します。
1
| $ bundle exec rails g migration CreateSalesforceschema
|
作成されたmigarationファイルを修正します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| class CreateSalesforceschema < ActiveRecord::Migration[6.1]
def up
if Rails.env.production?
return
end
ActiveRecord::Base.connection.execute <<~SQL
CREATE SCHEMA salesforce;
SQL
end
def down
if Rails.env.production?
return
end
ActiveRecord::Base.connection.execute <<~SQL
DROP SCHEMA salesforce CASCADE;
SQL
end
end
|
本番環境だとすでに Heroku Connect によって salesforce スキーマはつくられているので、
本番環境で動かないように Rails.env.production?
で判断してます。
直でSQLを実行するのはあまりRuby on Railsっぽくないのですがしょうがないです。
余談) salesforceスキーマの準備方法について
本番環境では実行しない、開発環境とテスト環境だけでスキーマを作りたいという要求だったのでmigrationでやりました。
他のやり方としては以下のやり方があります。
- docker で起動時にスキーマを準備する
- タスクでスキーマを作成する
- 環境構築準備に手オペを入れる
docker で起動時にスキーマを準備する
docker は起動時にスクリプトを実行できたりSQLを走らせたりすることができます。
その仕組みを使ってsalesforceスキーマを準備することはできないでしょうか?
- salesforceスキーマの作成はできる
- salesforceスキーマの中のテーブル作成は難しい
という結果になりました。
/docker-entrypoint-initdb.d
配下のファイルを実行してくれますが、それは初回のコンテナ起動時のみとなります。
2回目以降の起動では実行してくれません。
そのため、salesforceスキーマ側のテーブルが増えたときに、この方式では対応することができません。
タスクでスキーマを作成する
bundle exec rails g task CreateSalesforceSchema
のようにしてタスクを作り、そのタスクの中でスキーマやスキーマ内のテーブルを作るのはどうでしょうか?
- salesforceスキーマの作成はできる
- salesforceスキーマの中のテーブル作成もできる
- saelsforceスキーマの中のテーブルが追加されたことに気づきにくい
Rakeタスクはmigrationとは違い、実行しなくてもRailsを起動することができます。
そのため実際にテーブルを参照するときまでタスクを実行しないといけないことに気づきにくいという問題があります。
個人や数人で開発しているときは「タスク追加したので実行おねがいしま〜す」と言って回れば良いので大丈夫そうですが、
大人数で開発していると思わぬところで落ちる可能性があります。
またRakeタスクは実行順序の規定がないため、なにか順序が必要なDDLがあった際に困ることになりそうです。
(タスクを追加するたびにREADME.md
にその手順を追加する必要がありそう)
環境構築手順に手オペを入れる
なしで。
Rakeタスクよりもオペレーションが煩雑になりそうですね。
salesforceスキーマ内のテーブル作成
こちらもsalesforceスキーマの作成と同様にmigrationで行います。
1
| $ bundle exec rails g migration CreateSalesforceProduct2
|
ファイルを修正します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
| class CreateSalesforceProduct2 < ActiveRecord::Migration[6.1]
def up
if Rails.env.production?
return
end
ActiveRecord::Base.connection.execute <<~SQL
CREATE TABLE salesforce.product2 (
externalproductid__c character varying(255),
family character varying(255),
externalid character varying(255),
lastvieweddate timestamp without time zone,
stockkeepingunit character varying(180),
name character varying(255),
externaldatasourceid character varying(18),
displayurl character varying(1000),
lastmodifieddate timestamp without time zone,
isdeleted boolean,
isactive boolean,
systemmodstamp timestamp without time zone,
lastmodifiedbyid character varying(18),
createddate timestamp without time zone,
quantityunitofmeasure character varying(255),
createdbyid character varying(18),
productcode character varying(255),
description character varying(4000),
lastreferenceddate timestamp without time zone,
sfid character varying(18) COLLATE pg_catalog.ucs_basic,
id integer NOT NULL,
_hc_lastop character varying(32),
_hc_err text
);
CREATE SEQUENCE salesforce.product2_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE salesforce.product2_id_seq OWNED BY salesforce.product2.id;
ALTER TABLE ONLY salesforce.product2 ALTER COLUMN id SET DEFAULT nextval('salesforce.product2_id_seq'::regclass);
ALTER TABLE ONLY salesforce.product2
ADD CONSTRAINT product2_pkey PRIMARY KEY (id);
CREATE INDEX hc_idx_product2_lastmodifieddate ON salesforce.product2 USING btree (lastmodifieddate);
CREATE INDEX hc_idx_product2_systemmodstamp ON salesforce.product2 USING btree (systemmodstamp);
CREATE UNIQUE INDEX hcu_idx_product2_externalproductid__c ON salesforce.product2 USING btree (externalproductid__c);
CREATE UNIQUE INDEX hcu_idx_product2_sfid ON salesforce.product2 USING btree (sfid);
SQL
end
def down
if Rails.env.production?
return
end
ActiveRecord::Base.connection.execute <<~SQL
DROP TABLE salesforce.product2;
SQL
end
end
|
salesforceスキーマのときと同様に、production環境では実行しないように制御しています。
index
等はDROP TABLE
すれば一緒に消えてくれるので、down
はDROP TABLE
のみやっています。
search_pathの追加
config/database.yml
に schema_search_path
で設定します。
1
2
3
4
5
6
7
8
9
10
11
| development: &development
adapter: postgresql
encoding: unicode
database: <%= ENV['DB_NAME'] || 'heroku_rails_sample_development' %>
username: <%= ENV['DB_USERNAME'] || 'root' %>
password: <%= ENV['DB_PASSWORD'] || 'password' %>
host: <%= ENV['DB_HOST'] || '127.0.0.1' %>
port: <%= ENV['DB_PORT'] || '5432' %>
schema_search_path: "public,salesforce"
...
|
public
をつけないと私の環境ではmigrationができなかったのでつけています。
実際の動作
dockerを起動し、migartionを実行し、rails consoleから値の保存と参照までやってみます。
dockerの起動
docker-compose.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| version: "3"
services:
dbserver:
image: postgres:12
container_name: herokuconndb
ports:
- 5432:5432
environment:
POSTGRES_USER: root
POSTGRES_PASSWORD: password
POSTGRES_INITDB_ARGS: "--encoding=UTF-8"
volumes:
- database:/var/lib/postgresql/data
volumes:
database:
driver: local
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| $ docker compose up
[+] Running 2/2
⠿ Volume "heroku-rails-sample_database" Created 0.0s
⠿ Container herokuconndb Started 0.4s
Attaching to herokuconndb
herokuconndb | The files belonging to this database system will be owned by user "postgres".
herokuconndb | This user must also own the server process.
herokuconndb |
herokuconndb | The database cluster will be initialized with locale "en_US.utf8".
herokuconndb | The default text search configuration will be set to "english".
herokuconndb |
...
...
herokuconndb | 2021-08-07 12:33:50.050 UTC [1] LOG: starting PostgreSQL 12.8 (Debian 12.8-1.pgdg100+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit
herokuconndb | 2021-08-07 12:33:50.051 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432
herokuconndb | 2021-08-07 12:33:50.051 UTC [1] LOG: listening on IPv6 address "::", port 5432
herokuconndb | 2021-08-07 12:33:50.053 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
herokuconndb | 2021-08-07 12:33:50.064 UTC [77] LOG: database system was shut down at 2021-08-07 12:33:49 UTC
herokuconndb | 2021-08-07 12:33:50.068 UTC [1] LOG: database system is ready to accept connections
|
rails データベース作成
config/database.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| development: &development
adapter: postgresql
encoding: unicode
database: <%= ENV['DB_NAME'] || 'heroku_rails_sample_development' %>
username: <%= ENV['DB_USERNAME'] || 'root' %>
password: <%= ENV['DB_PASSWORD'] || 'password' %>
host: <%= ENV['DB_HOST'] || '127.0.0.1' %>
port: <%= ENV['DB_PORT'] || '5432' %>
schema_search_path: "public,salesforce"
test: &test
adapter: postgresql
encoding: unicode
database: <%= ENV['DB_NAME'] || 'heroku_rails_sample_test' %>
username: <%= ENV['DB_USERNAME'] || 'root' %>
password: <%= ENV['DB_PASSWORD'] || 'password' %>
host: <%= ENV['DB_HOST'] || '127.0.0.1' %>
port: <%= ENV['DB_PORT'] || '5432' %>
schema_search_path: "public,salesforce"
production: &production
adapter: postgresql
encoding: unicode
database: <%= ENV['DB_NAME'] %>
username: <%= ENV['DB_USERNAME'] %>
password: <%= ENV['DB_PASSWORD'] %>
host: <%= ENV['DB_HOST'] %>
port: <%= ENV['DB_PORT'] %>
schema_search_path: "public,salesforce"
|
1
2
3
| $ bundle exec rake db:create
Created database 'heroku_rails_sample_development'
Created database 'heroku_rails_sample_test'
|
rails データベースmigration
db/migrate/yyyymmddhhmmss_create_salesforce_schema.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| class CreateSalesforceSchema < ActiveRecord::Migration[6.1]
def up
if Rails.env.production?
return
end
ActiveRecord::Base.connection.execute <<~SQL
CREATE SCHEMA salesforce;
SQL
end
def down
if Rails.env.production?
return
end
ActiveRecord::Base.connection.execute <<~SQL
DROP SCHEMA salesforce CASCADE;
SQL
end
end
|
db/migrate/yyyymmddhhmmss_create_salesforce_product2.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
| class CreateSalesforceProduct2 < ActiveRecord::Migration[6.1]
def up
if Rails.env.production?
return
end
ActiveRecord::Base.connection.execute <<~SQL
CREATE TABLE salesforce.product2 (
externalproductid__c character varying(255),
family character varying(255),
externalid character varying(255),
lastvieweddate timestamp without time zone,
stockkeepingunit character varying(180),
name character varying(255),
externaldatasourceid character varying(18),
displayurl character varying(1000),
lastmodifieddate timestamp without time zone,
isdeleted boolean,
isactive boolean,
systemmodstamp timestamp without time zone,
lastmodifiedbyid character varying(18),
createddate timestamp without time zone,
quantityunitofmeasure character varying(255),
createdbyid character varying(18),
productcode character varying(255),
description character varying(4000),
lastreferenceddate timestamp without time zone,
sfid character varying(18) COLLATE pg_catalog.ucs_basic,
id integer NOT NULL,
_hc_lastop character varying(32),
_hc_err text
);
CREATE SEQUENCE salesforce.product2_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE salesforce.product2_id_seq OWNED BY salesforce.product2.id;
ALTER TABLE ONLY salesforce.product2 ALTER COLUMN id SET DEFAULT nextval('salesforce.product2_id_seq'::regclass);
ALTER TABLE ONLY salesforce.product2
ADD CONSTRAINT product2_pkey PRIMARY KEY (id);
CREATE INDEX hc_idx_product2_lastmodifieddate ON salesforce.product2 USING btree (lastmodifieddate);
CREATE INDEX hc_idx_product2_systemmodstamp ON salesforce.product2 USING btree (systemmodstamp);
CREATE UNIQUE INDEX hcu_idx_product2_externalproductid__c ON salesforce.product2 USING btree (externalproductid__c);
CREATE UNIQUE INDEX hcu_idx_product2_sfid ON salesforce.product2 USING btree (sfid);
SQL
end
def down
if Rails.env.production?
return
end
ActiveRecord::Base.connection.execute <<~SQL
DROP TABLE salesforce.product2;
SQL
end
end
|
1
2
3
4
5
6
| $ bundle exec rake db:migrate
== 20210807151451 CreateSalesforceSchema: migrating ===========================
== 20210807151451 CreateSalesforceSchema: migrated (0.0033s) ==================
== 20210807234933 CreateSalesforceProduct2: migrating =========================
== 20210807234933 CreateSalesforceProduct2: migrated (0.0102s) ================
|
その他の設定
複数形の対応
Product2
というModelを使うと product2s
というテーブルにアクセスしようとするので、 「product2
は複数形でも product2
として扱うよ」ということを教えてあげる必要があります。
config/initializers/inflections.rb
1
2
3
| ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.uncountable %w( product2 )
end
|
Modelの作成
Product2
というModelを用意します。
migrationファイルは不要なので --no-migration
をつけます。
1
2
3
4
5
6
7
| $ bundle exec rails g model Product2 --no-migration
Running via Spring preloader in process 16648
invoke active_record
create app/models/product2.rb
invoke test_unit
identical test/models/product2_test.rb
create test/fixtures/product2.yml
|
動作確認
salesforce.product2の参照
1
2
3
4
5
6
| $ bundle exec rails console
Running via Spring preloader in process 16763
Loading development environment (Rails 6.1.4)
irb(main):001:0> Product2.all
Product2 Load (1.2ms) SELECT "product2".* FROM "product2" /* loading for inspect */ LIMIT $1 [["LIMIT", 11]]
=> #<ActiveRecord::Relation []>
|
ちゃんとアクセスできています。
余談ですが、 Inflector の設定を行わないと、ここで product2s
というテーブルにアクセスしようとしてしまいます。
salesforce.product2への書き込み
1
2
3
4
5
6
| irb(main):002:0> p = Product2.new(name: 'sample product')
irb(main):003:0> p.save
TRANSACTION (1.0ms) BEGIN
Product2 Create (1.7ms) INSERT INTO "product2" ("name") VALUES ($1) RETURNING "id" [["name", "sample product"]]
TRANSACTION (1.9ms) COMMIT
=> true
|
いちど抜けてもう一度入り直し、Product2
をすべて取得してみます。
1
2
3
4
5
6
| $ bundle exec rails console
Running via Spring preloader in process 16908
Loading development environment (Rails 6.1.4)
irb(main):001:0> Product2.all
Product2 Load (1.1ms) SELECT "product2".* FROM "product2" /* loading for inspect */ LIMIT $1 [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Product2 externalproductid__c: nil, family: nil, externalid: nil, lastvieweddate: nil, stockkeepingunit: nil, name: "sample product", externaldatasourceid: nil, displayurl: nil, lastmodifieddate: nil, isdeleted: nil, isactive: nil, systemmodstamp: nil, lastmodifiedbyid: nil, createddate: nil, quantityunitofmeasure: nil, createdbyid: nil, productcode: nil, description: nil, lastreferenceddate: nil, sfid: nil, id: 1, _hc_lastop: nil, _hc_err: nil>]>
|
無事に先程のデータが取得できました。
つづいて psql
で入って、 salesforce.product2
に書き込まれているかを確認します。
1
2
3
4
5
6
7
8
9
10
| $ bundle exec rails dbconsole
Password for user root:
psql (13.3, server 12.8 (Debian 12.8-1.pgdg100+1))
Type "help" for help.
heroku_rails_sample_development=# select * from salesforce.product2;
externalproductid__c | family | externalid | lastvieweddate | stockkeepingunit | name | externaldatasourceid | displayurl | lastmodifieddate | isdeleted | isactive | systemmodstamp | lastmodifiedbyid | createddate | quantityunitofmeasure | createdbyid | productcode | description | lastreferenceddate | sfid | id | _hc_lastop | _hc_err
----------------------+--------+------------+----------------+------------------+----------------+----------------------+------------+------------------+-----------+----------+----------------+------------------+-------------+-----------------------+-------------+-------------+-------------+--------------------+------+----+------------+---------
| | | | | sample product | | | | | | | | | | | | | | | 1 | |
(1 row)
|
ちゃんと salesforce.product2
に書き込まれていました。
まとめ
Ruby on RailsでHeroku Connectの環境を作る場合
salesforce
スキーマはmigrationで作るsalesforce
スキーマのテーブルもmigarationで作るsearch_path
の設定を忘れずに