So I have this Type, which has a belong_to association with this OtherType, which can be nil.
I want to write get %OtherType{}.name from Type and it becomes much more complicated than I’d expect.
Here’s my take:
def other_type_name(type) do
type
|> Repo.preload(:other_type)
|> get_in([Access.key(:other_type)])
|> case do
nil -> nil
%OtherType{name: name} -> name
end
end
This is aweful.
I wanted to write: type |> preload |> get_in(:other_type, :name) but it fails if association other_type is not present. Not get_in([Access.key(:other_type), Access.key(:name)]).
I miss so much Ruby’s try method: Type.try(:other_type).try(:name), which forwards the nil value as expected… I still disagree with this comment as the nil value is completely expected.
I found this way which I find pretty satisfying, though I’d still be curious to discuss the whole point here.
def other_type_name(%Type{} = type) do
typer
|> Repo.preload(:other_type)
|> case do
%Type{other_type: %OtherType{name: name}} -> name
_ -> nil
end
end
Cool thing here is that keys are static and not dynamic, so this is optimized for compilation.
Have you tried going an extra level deep with get_in. Something like:
def other_type_name(type) do
type
|> Repo.preload(:other_type)
|> get_in([Access.key(:other_type), Access.key(:name)])
end
I know that works to navigate nil with regular map keys, though I haven’t used Access.key much
edit to add
Well, the docs suggest that won’t work. However, with a default value in the first Access.key call it could, but it loses some of the elegance that way.
def other_type_name(%Type{} = type) do
type
|> Repo.preload(:other_type)
|> case do
%{other_type: nil} -> nil
type -> type.other_type.name
end
end
Summary of the changes:
skips matching on __struct__ again in the case, since it’s already happened in the function head
makes the nil case explicit
Also consider making the Repo.preload a responsibility of the caller; doing preloads in helper functions like this can lead to poor performance at scale when other_type_name gets called while looping over a a list of Types.
BUT
Note that unloaded associations need careful thought in that situation; for instance, your original code will silently return nil if the association isn’t loaded. Explicitly matching on other_type: nil means that unloaded associations cause a KeyError at type.other_type.name.
I tend to use pattern matching liberally in cases like this. You can pattern match on unloaded associations and then use recursion to produce the correct result:
I should mention that there is a danger of missing n+1 problems when “automatically” performing preloads like this because if you loop through a collection and call this method on each one you won’t get an error that essentially is telling you to add the preload to the collection query. Note this problem affects your original implementation as well.
Personally I prefer to use specific tools to id/monitor performance issues, not app logic. Just make sure your query generation code has all the includes it needs.
Thanks all, I like your solutions too, though it seems like there isn’t an idiomatic way to do that, and it’s a matter of preferences.
I understand the N+1 query issue, but it won’t be an issue in my case: for me if there is a query that loads multiple instances, it must include the preload of all required associations, and if it is missing it is the caller responsibility, not the callee that was too permissive.
Regarding the double type matching, I thought this would not impact performances, but if it does, then I would remove it too.