Albaを使う時に循環参照を防ぐ
はじめに
Jsonシリアライザとして、Albaを採用した後、実際に使った後、循環参照にハマったので、その解決方法を書きます。
使用しているAlbaのバージョンは、1.6.0です。
循環参照が発生するコード
commentsテーブルのparent_idカラムには、コメント返信先の親コメントIDが入っています。
class CommentResource
include Alba::Resource
root_key!
attributes :id, :body
one :user, resource: UserListResource
one :parent, resource: CommentParentResource
end
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クラスに含めず、必ず子クラスに含めるようにする。
class BaseCommentResource
include Alba::Resource
attributes :id, :body
end
class CommentResource < BaseCommentResource
root_key!
one :user, resource: UserListResource
one :parent, resource: CommentParentResource
end
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段回目までしか含まれないらしい。
この解決方法はスマートじゃない感もあるので、この機能を追加して欲しいわけではない。
機能ではなく、こういう感じで書くといいよ的なドキュメントがあってもいいかもしれない。この解決方法でいいのであれば、僕が英語でドキュメントを書きます。
何がいい解決策かは分からないが、ペインはあったので、それを作者の方に伝えられればいいかなと思って、この記事を書きました。
Alba、便利に使わせてもらってます。ver2のリリースにも期待してます!!
[追記] withinを使おう
Albaの作者である大倉さんにTwitterのDMでこの記事を伝えたところ、within
を使うと良いとアドバイスいただきました!ありがとうございます。
ドキュメントは全部読んでいたつもりでしたが、見逃していました。。思いっきり、循環参照について書かれていた。。
その中にサンプルコードのリンクがあります。
同じような関連付けで開発環境で動かしてみたところ、うまく動きました。サンプルコードの構造であれば、rails zeitwerk:check
は通るのだが、今回記事に書いた構造だと rails zeitwerk:check
が通らず、同じエラーが出る。
両方ともコードは循環参照しているはずだが、なぜサンプルコードは問題なく、今回のコードがzeitwerkに問題ありなのか、原因をはっきりとは理解できなかった。
なので、少しモヤモヤするが、循環参照の問題を回避するために、通常は within
を使い、それでも回避できない場合は今回のようにクラスの構造を修正して回避するしかないかなと思う。
[追記2] issue登録した
大倉さんが調査してくれるとの事だったので、issue登録させていただいた。
[追記3] もっといい解決方法
大倉さんに回答いただきました。
one :parent, resource: 'CommentParentResource'
というようにリソースをStringで指定する事で、zeitwerk:checkのエラーを回避でき、通常のコードも問題なく動く。
こちらの方がBaseクラスを作成して複雑にするよりも、シンプルに解決できる。
Discussion