I’m still pretty new using phoenix, but coming from the rails world, things go well.
I’m wondering how/when models’ associations are loaded.
Eg. A typical case is:
# in the controller
def show(conn, %{"id" => id}) do
object = Repo.get(Object, id) |> Repo.preload(:association)
other_variable = object.association.name
Object.do_something(object)
# etc.
end
# in the model
def do_something(%__MODULE__{} = object) do
object = object |> Repo.preload(:association)
get_something = object.association.whatever
# etc.
end
So, the situation is, I need to preload :association in the controller, but I will use it in the model too. For independence reasons, I want to be able to call Object.do_something(object) somewhere else than in the controller (eg. IEX), so I want to make sure :association is preloaded within the model’s method.
Questions:
Is this a good practice to start every method with the preload of all required associations at every level for layer independence?
Do you confirm that if it is already loaded, nothing happens, hence there is no penalty for trying?
Do you also confirm that models, like in ActiveRecord, are loaded lazily, which means it waits for object.attribute to being called to actually trigger the Repo.get(Object, id) query?
Since it is so common, shouldn’t there be a more elegant syntax than having to reassign object = object |> Repo.preload(:association)?
Going further, I’m wondering why shouldn’t there be a behavior of autoload: if object.association is NotYetLoaded, the query goes, and if it needs to be loaded earlier (eg. to avoid N+1 requests), then it is actually loaded earlier the proper way.
No. That would be extra SQL queries in places where you might not need them.
Correct. Doing something like this:
order = Repo.get(Order, 1)
order = Repo.preload(order, :customer)
order = Repo.preload(order, :customer)
…will result in loading the customer only once.
No. You have no object to call before loading it in the first place. You get access to fields after you load their records from the DB.
Sure. Roll your own, takes a minute:
defmodule Orders do
def with_customer(order_id) do
Repo.get(Order, order_id)
|> Repo.preload(:customer)
end
end
# ...much later in your controller or anywhere else...
order_to_show_in_ui = Orders.with_customer(params["order_id"])
Have a look at Phoenix contexts – one of their purposes is to wrap a category / group of functionality into a singular module so the programmers also get a good idea which is where, topically, in the project.
Phoenix is not designed to pollute your project’s codebase with a lot of auto-generated conveniences. When you need one such, it’s relatively easy to make Phoenix generate its scaffold for you – or you roll your own as above (I usually do that but it’s down to preference really).
It’s a philosophical thing. Authors of Phoenix don’t like implicit behaviour. Don’t forget that some of them come from Rails and don’t want to emulate it.
TL;DR: The idea is to be explicit and not implicit.
Good points. One thing to point out though, by default, Repo.preload/3 won’t reload a preloaded association. So,
record = Repo.preload(record, :association) # this will hit the DB if :association is not loaded
record = Repo.preload(record, :association) # this will NOT hit the DB since association is already loaded
It seems you’re still thinking in the rails way of doing Model.find(id).
I would suggest not using Repo in your controllers. Maybe have a function you call instead of using Repo everywhere.
def get_user(%User{} = user), do: Repo.preload(user, :likes)
def get_user(id) do
from(u in User, where: u.id == ^id, preload: [:likes])
|> Repo.one()
end
This will get you a preloaded user every time you call the function whether with a user id or with a user record. Doing
32 |> get_user() |> get_user() |> get_user() # will hit the DB once
# or
current_user |> get_user() |> get_user() # might hit the DB, but also only once
I wouldn’t say this is the recommended way, but it should work for you. You might want to rethink the structure/flow of your code in the future.
One thing to keep in mind, in phoenix (or ecto particularly), you never hit the DB unless you are calling one of the Repo functions. No Repo call, no DB hit.