dmitriid

dmitriid

Ash query composition appreciation post

  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.

Most Liked

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.

zachdaniel

zachdaniel

Creator of Ash

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

Last Post!

dmitriid

dmitriid

@deviprsd

Great idea! I definitely need to add my thoughts there as well, if I manage to arrange them into a coherent form :slight_smile:

Where Next?

Popular in Discussions Top

Crowdhailer
I’ve been hearing much about the new formatter and it’s something I have been keen to try. I find examples buy far the most illuminating...
248 19814 150
New
acrolink
How does the two languages compare when it comes to server side application development? Any experiences or ideas? Thank you.
New
nburkley
AWS re:Invent is on at the moment with some interesting announcements. One new feature in particular is the Lambda Runtime API for AWS La...
New
chuck
Let me start by stating an assumption: Phoenix is a great approach to building REST APIs. There are many reasons for this, but I will ass...
New
slashdotdash
Phoenix Live View is now publicly available on GitHub. Here’s Chris McCord’s tweet announcing making it public.
New
AstonJ
Seen any cool LiveView demos, sample apps or examples? Please post them here! :003:
New
sergio
Kind of like when jquery came out, it was super necessary. Existing drag and drop libraries have a bunch of baggage to support old browse...
New

Other popular topics Top

rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list. ...
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New
dogweather
I wrote this comment on r/haskell, and it’s not popular there. :wink: But I think I’m on to something… Haskell reminds me of Java, and e...
New
sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New

We're in Beta

About us Mission Statement