あっちこっちで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しちゃったりしそうなんで、どうなるかなというとこですが、ちょっとこれで行ってみようかな。
知らなかった、と言ったな。あれは嘘だ。
はい。
先日、Ecto.Changeset.change/2 知らなかった - mmagというエントリを書いたのですが、前に自分が書いたコードでchange/2
使ってました。はい、知ってました。
例えば、User
とOrganization
がmany_to_many
な関係にあるとします。あるユーザを、ある団体に入れたいとしましょう。
user = Repo.get!(User, 1) org = Repo.get!(Organization, 1)
Ectoではこういうとき、put_assoc/4
を使って以下のようにやる方法が用意されています。
user |> Repo.preload(:organizations) |> Ecto.Changeset.change |> Ecto.Changeset.put_assoc(:organizations, [org]) |> Repo.update
user
のorganizations
にorg
を入れた上で、user
をupdate
するという感じです。このときのorganizations
にあたるassociationはpreloadしておく必要があります。忘れていても、エラーメッセージが結構わかりやすく教えてくれたりもしますが。
Ecto.Changeset.change/2 知らなかった
Ectoのmany_to_many
のドキュメントを読んでいたら、使用例の中でEcto.Changeset.change/2
という関数が使われていました。
構造体からchangesetをつくってくれる関数なようです。知らなかった。struct
入れてもchangeset
入れてもいいみたい。キャストやバリデーション無しで変更を入れたいときに使うのじゃ、とのこと。
user = Repo.get(User, 1) changeset = Ecto.Changeset.change(user) changeset = Ecto.Changeset.change(user, %{name: "john"}) changeset = Ecto.Changeset.change(changeset, %{age: user.age + 1})