dmitriid

dmitriid

  1. First, you don’t get Ash.
  2. Then you hate it’s verbose low-level API
  3. Then you atempt to write everyting declaratively in resource actions
  4. Then you discover
action :name, :return_type do
   run fn input, context ->
       .... whatever you want, including direct Ecto queries etc. ....
       {:ok, result}
   end
end
  1. 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.Query is a slightly higher level of abstraction than pure Ecto
  • You can load related data by simply asking Ash to load it
  • Documentation for Ash.Query.load is 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.

Showing Posts 1 to 10

zachdaniel

zachdaniel

Creator of Ash

Great stuff! A couple tips for you:

  1. 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”

  2. You don’t need to use Ash.Query.expr with Ash.Query.filter. It already wraps it for you.

pages_query =
  MyApp.Blog.Page
  |> Ash.Query.load([:tags, :external_scripts])
  |> Ash.Query.limit(paging[:limit])
  |> Ash.Query.offset(paging[:offset])
  |> Ash.Query.filter(
      pages.is_published == true and pages.is_tag == false and
        pages.published_at <= ^NaiveDateTime.utc_now()
  )

query =
  MyApp.Blog.Tag
  |> Ash.Query.select([:id])
  |> Ash.Query.load(:no_of_pages)
  |> Ash.Query.load(pages: pages_query)
  |> Ash.Query.filter(id == ^input.arguments[:tag_id])
absowoot

absowoot

There’s also the Ash Realworld git that has some good example code for posts and tags.

dmitriid

dmitriid OP

you’d want to do the filtering in the load query.

That’s what I did in the code right after I posted this :slight_smile: Unfortunately, can’t update the post anymore…

You don’t need to use Ash.Query.expr with Ash.Query.filter. It already wraps it for you.

Ah. I would sometimes run into function x is undefined when running without the expr, but can’t remember when. So now I tend to just stick expr everywhere :slight_smile:

dmitriid

dmitriid OP

Thank you! I took a quick look and… so that’s how you use prepare! :slight_smile:

zachdaniel

zachdaniel

Creator of Ash

This happens when you’ve not done require Ash.Query typically.

dmitriid

dmitriid OP

Somehow I’ve had issues even after require’ing Ash.Query :thinking:

zachdaniel

zachdaniel

Creator of Ash

:thinking: would need to see some specific example, but typically that is what causes that issue.

deviprsd

deviprsd

I’m in step no. 3, the realworld git is definitely a big help

CYMKD

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

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

Where Next? Top

Trending in Discussions Top

budgie
A little off-topic, but I feel like people here have a good head on their shoulders. I used to be quite good at making software. Was luc...
New
juhalehtonen
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New
axelson
Hi there! :wave: @frigidcode and I (but mostly him) have been running an Elixir Book club, we’re almost done with Designing Elixir Syste...
New
achempion
I’ve been using Emacs as my main code editor for more than a two years. It’s a custom build version although I’ve tried doom emacs and sp...
New
budgie
I love Elixir. It’s one of 2 programming languages I’ve ever fallen in love with. But I don’t use it anymore. Serverless was the promis...
New
jtormey
Lately I’ve been thinking about how to organize components as a LiveView application grows. One of the pain points I’ve found (for myself...
New
Null-logic-0
What IDE or editor are you using for Elixir development? Personally, I use Zed, and I really like it, but sometimes I wish there were a ...
New

Other Trending Topics Top

KristerV
Hey. Is there anyone here who creates agents in their apps? Not talking about using agents, but creating them. I’m finding it pretty diff...
New
mudasobwa
I fully migrated to my own harness from Anthropic/Gemini and I think it’s time to share it. Welcome DSH, the DeepSeek Harness, fully writ...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
OndrejValenta
(I just needed to vent somewhere and LinkedIn is full of hope, or hype, I’m not sure which exactly) AI dream has many faces, but general...
New
CodeSync
Code BEAM Europe 2026 – Call for Talks is Open! We’re excited to announce that Code BEAM Europe 2026 is now open for talk submissions :t...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews