Phoenix Web Development
上QQ阅读APP看书,第一时间看更新

Understanding the gotchas of associations

Cool! We now have our Poll, representing a row in the database with an ID of 1. Let's try to access the options on that poll:

iex(12)> poll.options
#Ecto.Association.NotLoaded<association :options is not loaded>

This seems strange. We queried for the Poll and got that back, so why didn't it load our Options association with it? Well, this is actually intentional behavior for Ecto; it's designed to not lazy-load data from the database. Anyone who has ever worked with a lazy loading relational model can tell you just what sorts of problems it tends to introduce over time concerning the performance and maintenance of an application, so to avoid that problem Ecto just straight up doesn't lazy load any associations. Instead, you'll need to tell Ecto that you want to include those associations directly, either via a join statement or via Repo.preload. So let's try our statement again, but this time with a Repo.preload instead:

It is a common scenario to see your tests fail or see Controllers throw error messages in development mode. Any time you get an error message about a changeset not matching the expected value or something going wrong in the display of a changeset, make sure you're not missing a preload statement!
iex(13)> poll = Repo.get!(Poll, 1) |> Repo.preload(:options)
[debug] QUERY OK source="polls" db=4.3ms
SELECT p0."id", p0."title", p0."inserted_at", p0."updated_at" FROM "polls" AS p0 WHERE (p0."id" = $1) [1]
[debug] QUERY OK source="options" db=3.7ms
SELECT o0."id", o0."title", o0."votes", o0."poll_id", o0."inserted_at", o0."updated_at", o0."poll_id" FROM "options" AS o0 WHERE (o0."poll_id" = $1) ORDER BY o0."poll_id" [1]
%Vocial.Votes.Poll{__meta__: #Ecto.Schema.Metadata<:loaded, "polls">, id: 1,
inserted_at: ~N[2017-10-05 20:18:08.931657],
options: [%Vocial.Votes.Option{__meta__: #Ecto.Schema.Metadata<:loaded, "options">,
id: 1, inserted_at: ~N[2017-10-05 21:14:32.058102],
poll: #Ecto.Association.NotLoaded<association :poll is not loaded>,
poll_id: 1, title: "Yes", updated_at: ~N[2017-10-05 21:14:32.059711],
votes: 0},
%Vocial.Votes.Option{__meta__: #Ecto.Schema.Metadata<:loaded, "options">,
id: 2, inserted_at: ~N[2017-10-05 21:15:03.797493],
poll: #Ecto.Association.NotLoaded<association :poll is not loaded>,
poll_id: 1, title: "No", updated_at: ~N[2017-10-05 21:15:03.797517],
votes: 0}], title: "Sample Poll", updated_at: ~N[2017-10-05 20:18:08.933798]}
iex(14)> poll.options
[%Vocial.Votes.Option{__meta__: #Ecto.Schema.Metadata<:loaded, "options">,
id: 1, inserted_at: ~N[2017-10-05 21:14:32.058102],
poll: #Ecto.Association.NotLoaded<association :poll is not loaded>,
poll_id: 1, title: "Yes", updated_at: ~N[2017-10-05 21:14:32.059711],
votes: 0},
%Vocial.Votes.Option{__meta__: #Ecto.Schema.Metadata<:loaded, "options">,
id: 2, inserted_at: ~N[2017-10-05 21:15:03.797493],
poll: #Ecto.Association.NotLoaded<association :poll is not loaded>,
poll_id: 1, title: "No", updated_at: ~N[2017-10-05 21:15:03.797517],
votes: 0}]

Huzzah! We're now getting our poll object out of the database and including the options as part of our preload statement! We have our way of getting the data in and out, we have our way of accessing the data via Elixir data structures, and we understand how to tell Ecto to relate our two separate schemas to each other! Now, imagine that you're writing code in your application and every single time you want to do something with Polls and Options you have to alias both schemas, include appropriate references and changesets for both, link everything together in the right way, include code to handle preloads and joins…

…and then imagine you have to do that every single time. Yikes! That's a ton of extra boilerplate code that we do not want to have to deal with! In addition, that code becomes incredibly brittle, because what if you change those changesets? New columns could get added pretty frequently and, if you have to search and find for the nine separate places you referenced those schemas could get incredibly tricky far too quickly! What's worse, you're even more likely to miss a spot where you needed to go through and change something, so you'd also have a broken application in a way that would be painstakingly difficult to diagnose and repair! We want to avoid that situation, so instead, let's use another recent Ecto concept to tackle this problem: Contexts.