How to get the association data safety in Ecto Model?

Viewed 213

I have a model User which has one Subscription. Now I want to get the data in related subscription. A user may have no subscription.

This is my code:

user = Repo.get(User, 1) |> Repo.preload(:subscription)

hash = %{name: user.name, subscription_type: user.subscription.type}

So here when I try to get subscription_type, if the subscription is nil, I will get an error.

In Ruby I can use try method user.subscription.try(:type) or safe navigation operator user.subscription&.type. How about in Elixir?

One way I can think of is: user.subscription && user.subscription.type

But I feel it's redundant, any idea?

2 Answers

We barely get to the nested elements of maps/structs with a dot notation in ; I would say [opinionated] it’s counter-idiomatic. Instead we use two approaches:

Access

If all the elements in the chain were maps (or have implemented the Access behaviour in general,) we could use get_in/2

foo = %{bar: %{baz: 42}}
get_in foo, [:bar, :baz] #⇒ 42
get_in foo, [:sna, :fu] #⇒ nil

Pattern matching

schemas, unfortunately, are structs and do not implement Access; for those we might use pattern matching

hash =
  User
  |> Repo.get(1)
  |> Repo.preload(:subscription)
  |> case do 
    %User{name: name, subscription: %Subscription{type: type}} ->
      %{name: name, subscription_type: type}
    %User{name: name} ->
      %{name: name}
  end

Access.key/2

Also, one might use Access.key/2 to seeply get to the values in nested structs.

user = User |> Repo.get(1) |> Repo.preload(:subscription)
type = get_in(user, [Access.key(:subscription), Access.key(:type))

There are quite big topic about safe navigation.
The easiest solution and idiomatically right would be pattern matching.

user = Repo.get(User, 1) |> Repo.preload(:subscription)
subscription_type = case user do
                      %{subscription: %{type: type}} when is_binary(type) -> type
                      _ -> nil
                    end
hash = %{name: user.name, subscription_type: subscription_type}
Related