🐦

Albaを使う時に循環参照を防ぐ

2022/06/22に公開

はじめに

https://zenn.dev/imaginelab/articles/f5cf202cb64f49

Jsonシリアライザとして、Albaを採用した後、実際に使った後、循環参照にハマったので、その解決方法を書きます。

使用しているAlbaのバージョンは、1.6.0です。

循環参照が発生するコード

commentsテーブルのparent_idカラムには、コメント返信先の親コメントIDが入っています。

comment_resource.rb
class CommentResource
  include Alba::Resource
  root_key!

  attributes :id, :body

  one :user, resource: UserListResource
  one :parent, resource: CommentParentResource
end
comment_parent_resource.rb
class CommentParentResource < CommentResource
  def attributes
    allow_list = [
      :id,
      :user,
    ]
    super.select { |key, _| allow_list.include?(key.to_sym) }
  end
end

zeitwerkでエラーが出る

rails zeitwerk:check を実行後、エラーが出る。

rails aborted!
NameError: uninitialized constant CommentResource::CommentParentResource
/app/resources/comment_resource.rb:7:in `<class:CommentResource>'
/app/resources/comment_resource.rb:1:in `<top (required)>'
/app/resources/comment_parent_resource.rb:1:in `<top (required)>'
bin/rails:4:in `require'
bin/rails:4:in `<main>'
Tasks: TOP => zeitwerk:check

CommentParentResourceを読み込もうとして、継承元のCommentResourceを読み込みに行って、その中でCommentParentResourceを読み込む必要があるが、そもそも最初にCommentParentResourceを読み込もうとしていたので読み込めない。

結局、NameError: uninitialized constantエラーが出る。

解決方法

補足: #[追記3]-もっといい解決方法 の方がシンプルで良い解決手段です。

attributesだけを管理するBaseクラスを作成する。oneやhas_manyはBaseクラスに含めず、必ず子クラスに含めるようにする。

base_comment_resource.rb
class BaseCommentResource
  include Alba::Resource
  
  attributes :id, :body
end
comment_resource.rb
class CommentResource < BaseCommentResource
  root_key!

  one :user, resource: UserListResource
  one :parent, resource: CommentParentResource
end
comment_parent_resource.rb
class CommentParentResource < BaseCommentResource
  root_key!
  
  def attributes
    allow_list = [
      :id,
      :user,
    ]
    super.select { |key, _| allow_list.include?(key.to_sym) }
  end
  
  one :user, resource: UserListResource
end

継承元は、必ずBaseCommentResourceとすることで、不必要な関連先クラスを読み込むことを防ぎ、循環参照を防いでいる。

Albaへのフィードバック

普通にコードを書いていると、こういう問題は発生しがちだと思うので、Alba側で何かサポートがあると嬉しい。

例えば、ActiveModel::Serializersでは、デフォルトの設定では、ネストされたリソースは1段回目までしか含まれないらしい。

https://qiita.com/qsona/items/f9d58976c561b8331922

この解決方法はスマートじゃない感もあるので、この機能を追加して欲しいわけではない。

機能ではなく、こういう感じで書くといいよ的なドキュメントがあってもいいかもしれない。この解決方法でいいのであれば、僕が英語でドキュメントを書きます。

何がいい解決策かは分からないが、ペインはあったので、それを作者の方に伝えられればいいかなと思って、この記事を書きました。

Alba、便利に使わせてもらってます。ver2のリリースにも期待してます!!

[追記] withinを使おう

Albaの作者である大倉さんにTwitterのDMでこの記事を伝えたところ、within を使うと良いとアドバイスいただきました!ありがとうございます。

ドキュメントは全部読んでいたつもりでしたが、見逃していました。。思いっきり、循環参照について書かれていた。。

https://github.com/okuramasafumi/alba#circular-associations-control

その中にサンプルコードのリンクがあります。

https://github.com/okuramasafumi/alba/blob/main/test/usecases/circular_association_test.rb

同じような関連付けで開発環境で動かしてみたところ、うまく動きました。サンプルコードの構造であれば、rails zeitwerk:check は通るのだが、今回記事に書いた構造だと rails zeitwerk:check が通らず、同じエラーが出る。

両方ともコードは循環参照しているはずだが、なぜサンプルコードは問題なく、今回のコードがzeitwerkに問題ありなのか、原因をはっきりとは理解できなかった。

なので、少しモヤモヤするが、循環参照の問題を回避するために、通常は within を使い、それでも回避できない場合は今回のようにクラスの構造を修正して回避するしかないかなと思う。

[追記2] issue登録した

大倉さんが調査してくれるとの事だったので、issue登録させていただいた。

https://github.com/okuramasafumi/alba/issues/212

[追記3] もっといい解決方法

大倉さんに回答いただきました。

https://github.com/okuramasafumi/alba/issues/212#issuecomment-1168992307

one :parent, resource: 'CommentParentResource'

というようにリソースをStringで指定する事で、zeitwerk:checkのエラーを回避でき、通常のコードも問題なく動く。

こちらの方がBaseクラスを作成して複雑にするよりも、シンプルに解決できる。

Discussion