🚀

elasticsearch-railsのQueryDSL内でselfがModelのインスタンスを参照しなかった件

2023/11/29に公開

発生した問題

  • モデルファイル上でElasticsearchのクエリコードを書いていた際に、 self の出力がおかしい
  • Elasticsearchの import メソッドを呼び出した際の、クエリDSL内で用いた self から出力された情報が想定していたものではなかった

-実装したいコード-

  • Userのインスタンスに対して、そのUserに紐づいている商品のインデックスを更新する処理
  • self を用いて、Userのインスタンスを指し示したい( self.exhibits.now_on_sale.ids
# user.rb
def user_exhibits_no_recommendation
    begin
      Exhibit.__elasticsearch__.import(
        query: -> {
          # ココ
          includes(:exhibit_images, :thinkings, :user, exhibit_sub_form_values: :exhibit_sub_form).where(id: self.exhibits.now_on_sale.ids) }
      )
    rescue StandardError
      nil
    end
end

考察

  • query -> { include(:exhibit_images, ... が呼べている時点で、 self がExhibitなのは妥当なのでは?
  • 具体的な原因は不明

原因(結果)

  • Exhibit.__elasticsearch__.import を呼び出すと、 ClassMethodsProxy のインスタンスが生成され、このインスタンスが self として機能する
  • query ブロック内の selfExhibit.__elasticsearch__ によって返される ClassMethodsProxy インスタンスを指すので、 self はUserのインスタンスではなく、Exhibitに関連するElasticsearchの操作を代理実行するプロキシオブジェクトを指す

確認したこと

-確認コード-

  • 確認した点
    • モデルファイル内で通常時に self が指し示すデータ
    • ElasticsearchのクエリDSL内で self が指し示すデータ
# user.rb
def user_exhibits_no_recommendation
    begin
      p '-----'
      p self
      p '-----'

      Exhibit.__elasticsearch__.import(
        query: -> {
          p '----------------'
          p self
          p '----------------'
          includes(:exhibit_images, :thinkings, :user, exhibit_sub_form_values: :exhibit_sub_form).where(id: "#{ここにUserのインスタンスを入れたい}".exhibits.now_on_sale.ids) }
      )
    rescue StandardError
      nil
    end
end

-出力ログ-

  • 出力結果
    • モデルファイル上で self を用いた場合、一般的にその self が指し示すのはModelのインスタンスであるが、 import メソッドのクエリDSL内で self を使用すると、指し示すデータが変わった
      • #<User id: 1, ...>: Userのインスタンス
      • [PROXY] Exhibit(id: integer, ...): Exhibitの情報(?)
"-----"
#<User id: 1, signin_count: 5, nickname: "ユーザー1", ..., updated_at: "2023-09-01 13:03:38.000000000 +0900">
"-----"
"----------------"
[PROXY] Exhibit(id: integer, game_id: integer, ..., updated_at: datetime)
"----------------"

解決策

  • 先にUserのインスタンス情報を変数に格納する
def user_exhibits_no_recommendation
  begin
    # NOTE: selfのままでは更新されないため代入
    user = self

    Exhibit.__elasticsearch__.import(
      query: -> { includes(:exhibit_images, :thinkings, :user, exhibit_sub_form_values: :exhibit_sub_form).where(id: user.exhibits.now_on_sale.ids) }
    )
  rescue StandardError
    nil
  end
end

結論

  • Elasticsearchのブロック処理内で self が指し示すのは @__elasticsearc__
  • @__elasticsearch__ は、 @elasticsearch ||= ClassMethodsProxy.new(self)で初期化され、中身は ClassMethodsProxy のインスタンスである
  • ClassMethodsProxy.new(self)self は、メソッドの呼び出し対象であるExhibitクラスを指すので、 ClassMethodsProxy のインスタンスはExhibitクラスのElasticsearch関連のメソッドを代理実行できるようになる
# コード
def self.__elasticsearch__ &block
  @__elasticsearch__ ||= ClassMethodsProxy.new(self)
  @__elasticsearch__.instance_eval(&block) if block_given?
  @__elasticsearch__
end

※ 補足として、 self の出力の先頭に付いている [PROXY] は、プロキシオブジェクトであることを明示するために inspect メソッドがデバックやログ出力で表示する際に付けている

参考

Discussion