環境情報
- Rails: 6.1.6.1
- Ruby: 3.1.2
- Database: PostgreSQL 11.x
- Elasticsearch 7.x
DBに保存したらElasticsearchも更新されたい
Railsプロジェクトで、データベースへの保存を行ったら同時にElasticsearchへの反映も行いたい、というケースが出てきました。
Elasticsearchの利用には、elasticsearch-railsを使っているのですが、データの反映は簡単でした。
# Elasticsearchへの保存が失敗してもロールバックしたいので… class Foo < ApplicationRecord def apply! ApplicationRecord.transaction do # なんやかんかして self.applied = true self.save! self.__elasticsearch__.index_document # => これでElasticsearchにも反映される end end end
画面側で確認しても問題なし👍Elasticsearchを使う検索フォームで検索してもOK。
問題発生(テストが落ちる)
しかし、RSpecでテストを書いたら、落ちる…😥
require 'rails_helper' RSpec.describe Foo, type: :model do describe '#apply!' do subject { Foo.create!(applied: false) } it '反映されること' do expect { subject.apply! }.to change { subject.applied? }.from(false).to(true) definition = Elasticsearch::DSL::Search.search definition.query do must do term applied: true end end result = Foo.__elasticsearch__.search(definition).records expect(result).to be_present # => ❌存在しないと言われる👻 end end end
調査
なんとなく、もしかして非同期更新か?と思い、雑に2秒待つようにしたら、テストが通るように!
require 'rails_helper' RSpec.describe Foo, type: :model do describe '#apply!' do subject { Foo.create!(applied: false) } it '反映されること' do expect { subject.apply! }.to change { subject.applied? }.from(false).to(true) sleep(2) # => 2秒待つ… definition = Elasticsearch::DSL::Search.search definition.query do must do term applied: true end end result = Foo.__elasticsearch__.search(definition).records expect(result).to be_present # => 🟢通った👍 end end end
とりあえずテストは通るようになったけれど、テストにsleepとか入れたくありません。どうにか同期的にデータ更新できる方法はないものか…とコードを読んでいきます。
Yardを読むと、@param options [Hash] Optional arguments for passing to the client
と書いてあるけれど、いやそのHashにどう設定すればいいかがわからん!😠
ググったら、zennの記事がヒットしました。
コードの感じからすると、Kotlinかなんかだろうか…?とりあえずrefreshオプションがあることがわかりました。
解決方法
というわけで、該当箇所を修正。
# apply!メソッドの一部 self.__elasticsearch__.index_document(refresh: true) # => Elasticsearchに同期的に反映
これで、sleepなしでもRSpecが通るようになりました👍
テストの時だけ同期をとるようにしたい
しかし、なぜ非同期でElasticsearch側に反映されるかというと、パフォーマンス向上のためですね。つまり、毎回同期をとるように更新していたら、遅くなります。ループでFoo#apply!
メソッドを呼ぶこともあるので、それは避けたい…。
そもそも、普段は即時同期が取れていなければならないほどシビアな条件でもありません。単に、テストの時だけ同期が取れていればいいわけです。
そこで、該当箇所を以下のように修正。
# apply!メソッドの一部 self.__elasticsearch__.index_document(refresh: Rails.env.test?) # => テストの時だけElasticsearchに同期的に反映
これで、テストの時だけElasticsearchへの反映が同期的になりました👏👏👏