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

mmag

ハマったことメモなど

あっちこっちでpreload問題

こんばんは。

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しちゃったりしそうなんで、どうなるかなというとこですが、ちょっとこれで行ってみようかな。

知らなかった、と言ったな。あれは嘘だ。

はい。

先日、Ecto.Changeset.change/2 知らなかった - mmagというエントリを書いたのですが、前に自分が書いたコードでchange/2使ってました。はい、知ってました。

例えば、UserOrganizationmany_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

userorganizationsorgを入れた上で、userupdateするという感じです。このときの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})

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