dmitriid
- First, you don’t get Ash.
- Then you hate it’s verbose low-level API
- Then you atempt to write everyting declaratively in resource actions
- Then you discover
action :name, :return_type do
run fn input, context ->
.... whatever you want, including direct Ecto queries etc. ....
{:ok, result}
end
end
- And then you start noticing things in documentation that tell you: you can use Ash.Query here
I’m building a blog engine (how original). One of the main things that a blog has is: pages and tags. So, my question was: given a tag, load all of its pages, given a specific offset and a limit.
For example you have many pages tagged with ash, and a user requests page 2 from those pages. And their pages can additionally have their own tags, and calculations, and aggregations, and things that you may want to load. And you want a total count of those pages etc.
My first attempt
My first attempt was: why not use Ecto?
action :get_pages_for_tag, :map do
argument :tag_id, :uuid
argument :limit, :integer
argument :offset, :integer
run fn input, _context ->
query = from tag in Tags,
where: tag.id = ^input.arguments.tag_id,
join: page_tags in PageTags,
on: page_tags.tag_id == tag.id,
join: page in Pages,
on: page.id,
select: page
....
end
end
This poses a few issues: it’s a bit tedious to write out, you still have to add code to load related data (like al the related tags for the loaded pages) etc. There were some issues with haveing macros inside macros inside macros which would confuse the compiler (I think).
Second attempt and epiphany
I decided to play with Ash.Query in the console while skimming through docs. And these things simultaneously struck me:
Ash.Queryis a slightly higher level of abstraction than pure Ecto- You can
loadrelated data by simply asking Ash to load it - Documentation for
Ash.Query.loadis this:
load(query, options)
@spec load(
t() | Ash.Resource.t(),
atom
| Ash.Query.Calculation.t()
| list(atom | Ash.Query.Calculation.t())
| list({atom | Ash.Query.Calculation.t(), term})
) :: t()
Yes. It accepts a resource or a query as the first parameter!
So, without getting into the details of the actual resources involved, I give you this beauty:
query =
MyApp.Blog.Tag
# we don't need other fields, just id
|> Ash.Query.select([:id])
# load a calculaiton that counts no of pages in this tag
|> Ash.Query.load(:no_of_pages)
# load pages related to this tag, with limit and offset
|> Ash.Query.load(
pages:
MyApp.Blog.Page
|> Ash.Query.load([:tags, :external_scripts])
|> Ash.Query.limit(paging[:limit])
|> Ash.Query.offset(paging[:offset])
)
# regular filters on the :pages relationship.
# these could be added directly in the `load(pages: ...)` query
|> Ash.Query.filter(
Ash.Query.expr(
pages.is_published == true and pages.is_tag == false and
pages.published_at <= ^NaiveDateTime.utc_now()
)
)
|> Ash.Query.filter(Ash.Query.expr(id == ^input.arguments[:tag_id]))
{:ok, result} = query |> MyApp.Blog.read_one()
Boom. One srather straightforward piece of code to do what I need.
Yes, those can be used in declarative actions, too
read :pages_for_tag do
load(pages:
MyApp.Blog.Page
# only select specific fields
|> Ash.Query.Select([:id, :title])
# load relations for pages
|> Ash.Query.load([:tags, :external_scripts]) ....)
filter(... your filters ...)
end
Caveats
Don’t blindly trust code. The query above generates three separate queries that are fetched from the database. This is totally fine in my case, but might not be fine for you.
Trending in Discussions
Other Trending Topics
Latest Ash Threads
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #blog-post
- #ai
- #phoenix_html
- #elixirconf-us
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #hex
- #security
- #metaprogramming










Showing Posts 1 to 10- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
zachdaniel
Great stuff! A couple tips for you:
if you want the tags and then for that given tag the pages that match that criteria, you’d want to do the filtering in the load query. The query you have is more like “tags where they have a published page that matches these criteria”, as opposed to “the tag, loading the pages that match these criteria”
You don’t need to use
Ash.Query.exprwithAsh.Query.filter. It already wraps it for you.absowoot
There’s also the Ash Realworld git that has some good example code for posts and tags.
dmitriid
That’s what I did in the code right after I posted this
Unfortunately, can’t update the post anymore…
Ah. I would sometimes run into
function x is undefinedwhen running without theexpr, but can’t remember when. So now I tend to just stickexpreverywheredmitriid
Thank you! I took a quick look and… so that’s how you use
prepare!zachdaniel
This happens when you’ve not done
require Ash.Querytypically.dmitriid
Somehow I’ve had issues even after require’ing Ash.Query
zachdaniel
deviprsd
I’m in step no. 3, the realworld git is definitely a big help
CYMKD
Amazing stuff.
Im in step 3 as well, literally did a lot generic actions with run() and now fighting understanding the Resource Actions in detail (hoping to definitely contribute to DX docs once I feel like im not just blindly making things work)
deviprsd
Maybe we should have a thread of all the docs pain points somewhere so we can gather them and be able clean them up.
Last night I had to dig through discord to find out how to actually use Ash.PubSub but it is made using pubsubs easy-peasy. Took a solid minute there to get the instructions.
Edit: I kind of made a github discussion thread Documentation Pain Points Thread · ash-project/ash · Discussion #776 · GitHub to help out