0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

RailsのSTIで子クラスがfindに引っかからなくてハマった話

Last updated at Posted at 2022-01-21

前提環境

$ ruby -v
ruby 3.0.3p157 (2021-11-24 revision 3fb7d2cadc) [x64-mingw32]
$ rails -v
Rails 7.0.1

問題

ツイッターのクローンアプリを学習用に作っていて、ツイート系のモデルがTweet < ApplicationRecordNormalTweet < TweetReply < NormalTweetとなる設計で実装しました。
※Tweetはツイートの作成者author、NormalTweetはツイート内容text、Replyは返信先reply_toを持ちます。

それで開発中NormalTweet.find()にその子クラスであるReplyが引っかるときと引っかからない時がある!(Couldn't find NormalTweet with 'id'=n (ActiveRecord::RecordNotFound))という問題が発生。

原因

Rails(のデフォルトではdevelopment環境)では、クラスが使用されるときにロードされるようになっています。(lazy loading)
その時、NormalTweetクラスは読み込まれるものの、その子クラスであるReplyクラスは読み込まれていないことが原因でした。

対処

STIクラスをプリロードするようにします。

問題が起きたため、Railsガイドのものを少しだけ変更しています。

  • constantizesafe_constantizeに変更
  • プリロード中にプリロードが起こらないように変更

lib/sti_preload.rbとしてファイルを作成

lib/sti_preload.rb
module StiPreload
  unless Rails.application.config.eager_load
    extend ActiveSupport::Concern

    included do
      cattr_accessor :preloaded, instance_accessor: false
      cattr_accessor :preloading, instance_accessor: false
    end

    class_methods do
      def descendants
        preload_sti unless preloaded
        super
      end

      # Constantizes all types present in the database. There might be more on
      # disk, but that does not matter in practice as far as the STI API is
      # concerned.
      #
      # Assumes store_full_sti_class is true, the default.
      def preload_sti
        return if self.preloading
        self.preloading = true

        types_in_db = \
          base_class.
          unscoped.
          select(inheritance_column).
          distinct.
          pluck(inheritance_column).
          compact

        types_in_db.each do |type|
          logger.debug("Preloading STI type #{type}")
          type.safe_constantize # Railsガイドのものから変更: safeでないconstantizeを使うと例外が発生して正常に動作しない
        end

        self.preloading = false
        self.preloaded = true
      end
    end
  end
end

モデル(ルートのみ)にrequire文、include文を追加

tweet.rb
require "sti_preload"

class Tweet < ApplicationRecord
  belongs_to :author, class_name: "User"

  # このincludeは一番下!!!!!!!絶対です!!!!!
  include StiPreload
end

ここでincludeは一番下に置くことに注意!
これが上にあると、その下のアソシエーションなどが設定されない状態でクラスがロードされてしまうので、Association named 'xxx' was not found on xxx; perhaps you misspelled it? (ActiveRecord::AssociationNotFoundError)などのエラーが発生してハマります!!!

この問題の原因、対処方法などは https://ruby-jp.slack.com/ で教えて頂きました。
回答して頂いた方々にはこの場をお借りして感謝申し上げます。ありがとうございました。

参考サイトなど

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?