読者です 読者をやめる 読者になる 読者になる

mmag

ハマったことメモなど

あっちこっちでpreload問題

Elixir Ecto

こんばんは。

Ectoでは関連を明示的にpreloadすることがよくあります。例えばUserPostを複数持っていて、それをUserControllershowアクションでこんな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しちゃったりしそうなんで、どうなるかなというとこですが、ちょっとこれで行ってみようかな。

テーマはFB matteをベースにしてます。作者さんに感謝を込めて。