あっちこっちでpreload問題
こんばんは。
Ectoでは関連を明示的にpreloadすることがよくあります。例えばUser
がPost
を複数持っていて、それをUserController
のshow
アクションでこんなjsonで返したいとき、
{ "user": { "name": "joe_noh", "posts": [ {"title": "...", body: "..."}, {"title": "...", body: "..."}, {"title": "...", body: "..."} ] } }
viewはこんな感じに書いたとします。
defmodule UserView do def render("show.json", %{user: user}) do %{user: render_one(user, __MODULE__, "user.json")} end def render("user.json", %{user: user}) do %{ name: user.name, posts: render_many(user.posts, PostView, "post.json") } end end defmodule PostView do def render("post.json", %{post: post}) do %{ title: post.title, body: post.body } end end
"user.json"をrenderするとき、posts
をpreloadしておかないと、user.posts
は
#Ecto.Association.NotLoaded<association :posts is not loaded>
という初期値のままなので、protocol Enumerable not implemented
などエラーが出ます。で、どうするかというと、preloadです。以下のようにしておくと、user.posts
にPostのリストが入って意図したとおりにrenderできます。
user = Repo.preload(user, :posts)
はい、ここで問題。このpreloadはどこでやるべきか。MVCで言えばMに関数つくって、Cから呼ぶ、というのがベタかなと思ってやっていたんですが、段々と
いろんなとこで同じようなpreload書き始めちゃって散らかっちゃって。ある時点のuser
の関連はどれがpreload済みでどれがpreloadされてないのか分からんという。なんとなく思っている程度なんですが、ビジネスロジックのためにpreloadしているところと、renderするためにpreloadしているものが混ざると苦しくなるんじゃないかなぁという気がしています。
じゃあどこにまとめようかということで、最初は自分でも狂ってんなと思ったんですが、Viewでいいんじゃないの、と考え始めました。各Viewモジュールがrenderに必要な属性を知ってて、こんな感じに何をpreloadしたらいいのか教えるようにする。
defmodule UserView do def render("show.json", %{user: user}) do user = preload_requirements(user) %{user: render_one(user, __MODULE__, "user.json")} end def render("user.json", %{user: user}) do %{ name: user.name, posts: render_many(user.posts, PostView, "post.json") } end def requirements do [posts: PostView.requirements] end defp preload_requirements(user) do Repo.preload(user, requirements()) end end defmodule PostView do def render("index.json", %{posts: posts}) do posts = preload_requirements(posts) %{posts: render_many(posts, __MODULE__, "post.json")} end def render("post.json", %{post: post}) do %{ title: post.title, body: post.body, comments: render_many(post.comments, CommentView, "comment.json") } end def requirements do [comments: CommentView.requirements] end defp preload_requirements(post) do Repo.preload(post, requirements()) end end
考え方としては、renderに必要なやつらはViewがカバーしてくれるんで、MやCではその辺忘れていいよってとこです。まー気をつけないと循環しちゃったり、使わないやついっぱいpreloadしちゃったりしそうなんで、どうなるかなというとこですが、ちょっとこれで行ってみようかな。