select fields from nested associations

I have two tables: users and user_profiles, with a one-to-one relationship. I know it might sound a bit silly, but I don't always need the information from the profile. Back to the point: I created a read action for searching that looks like this:
read :search_instructors_by_name do
argument :query, :ci_string do
constraints allow_empty?: true
default ""
end

require Ash.Query

prepare fn %{arguments: %{query: full_name}} = query, _context ->
query
|> Ash.Query.load(user_profile: :full_name)
|> Ash.Query.filter(expr(role == :instructor))
|> Ash.Query.filter(contains(user_profile.full_name, ^full_name))
end
end
read :search_instructors_by_name do
argument :query, :ci_string do
constraints allow_empty?: true
default ""
end

require Ash.Query

prepare fn %{arguments: %{query: full_name}} = query, _context ->
query
|> Ash.Query.load(user_profile: :full_name)
|> Ash.Query.filter(expr(role == :instructor))
|> Ash.Query.filter(contains(user_profile.full_name, ^full_name))
end
end
The full_name is a calculation field based on other fields in user_profiles. For the search functionality, I only need the user's id and full_name — nothing more. However, when I trigger the search, it returns a whole bunch of fields, including full_name and all other user_profile fields. Is there a way to modify the search so that it only returns a simple map with just the :id and :full_name fields? In Ecto terms, would be something like:
Comment
|> join(:inner, [c], p in Post, on: c.post_id == p.id)
|> select([c, p], {p.title, c.text})
Comment
|> join(:inner, [c], p in Post, on: c.post_id == p.id)
|> select([c, p], {p.title, c.text})
I’ve tried many things without success — such as adding Ash.Query.select(:id) or using modify_query, but I’m not sure how to get an alias. Another option is to do some post-processing with Ecto.map, but I’m wondering if there’s a more elegant solution.
7 Replies
ZachDaniel
ZachDaniel5d ago
When loading, if you add the strict?: true option, then all attributes are not selected by default you'll still get structs back but you won't be selecting things you don't need
prepare fn %{arguments: %{query: full_name}} = query, _context ->
query
|> Ash.Query.load([user_profile: :full_name], strict?: true)
|> Ash.Query.filter(expr(role == :instructor))
|> Ash.Query.filter(contains(user_profile.full_name, ^full_name))
end
prepare fn %{arguments: %{query: full_name}} = query, _context ->
query
|> Ash.Query.load([user_profile: :full_name], strict?: true)
|> Ash.Query.filter(expr(role == :instructor))
|> Ash.Query.filter(contains(user_profile.full_name, ^full_name))
end
is that sufficient for what you're looking for? getting an actual map back is doable, but doesn't really have much tangible benefit in my experience
catrapato
catrapatoOP5d ago
Added another tool to my toolbox => strict?: true . Thanks! Out of curiosity, what do you recommend for returning a map?
ZachDaniel
ZachDaniel5d ago
There are a few ways to do it One way, using declarative calculations:
calculate :profile_with_less_data, :map, expr(%{
full_name: user_profile.full_name
})
calculate :profile_with_less_data, :map, expr(%{
full_name: user_profile.full_name
})
One way, using dynamic calculations
prepare fn %{arguments: %{query: full_name}} = query, _context ->
query
|> Ash.Query.calculate(:user_profile_with_less_data, :map, expr(%{full_name: user_profile.full_name}))
|> Ash.Query.filter(expr(role == :instructor))
|> Ash.Query.filter(contains(user_profile.full_name, ^full_name))
end
prepare fn %{arguments: %{query: full_name}} = query, _context ->
query
|> Ash.Query.calculate(:user_profile_with_less_data, :map, expr(%{full_name: user_profile.full_name}))
|> Ash.Query.filter(expr(role == :instructor))
|> Ash.Query.filter(contains(user_profile.full_name, ^full_name))
end
catrapato
catrapatoOP5d ago
Interesting!!
ZachDaniel
ZachDaniel5d ago
In the second one, it would be under user.calculations.user_profile_with_less_data If you just want to make full_name available as a field on user you can also easily do that
calculate :full_name, :string, expr(user_profile.full_name)
calculate :full_name, :string, expr(user_profile.full_name)
Which would give you
prepare fn %{arguments: %{query: full_name}} = query, _context ->
query
|> Ash.Query.load(:full_name)
|> Ash.Query.filter(expr(role == :instructor))
|> Ash.Query.filter(contains(full_name, ^full_name))
end
prepare fn %{arguments: %{query: full_name}} = query, _context ->
query
|> Ash.Query.load(:full_name)
|> Ash.Query.filter(expr(role == :instructor))
|> Ash.Query.filter(contains(full_name, ^full_name))
end
catrapato
catrapatoOP5d ago
OMG! This is just bananas — in a good way! Thanks, Zach. I’m all set. 🙏 This is great! After applying your suggestion, I can see that I'm now making only one query instead of two. 🚀
forest
forest4d ago
That last way is awesome.

Did you find this page helpful?