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

AstonJ
As the title says, please share what you’ve been up to with Elixir. Whether that’s been learning it, looking into it, making stuff with i...
2977 94592 917
New
cblavier
Hey there, It’s been more than a year since we started using LiveView as our main UI library and building a whole library of UI componen...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
heathen
Quite interesting article Google brought me. Didn’t find any mentions about it here. What do you think in general? Would you use togethe...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
New
AstonJ
Since we have deprecated our Erlang sections (as we have dedicated Erlang Forums now) let’s add this thread for those who’d like to post ...
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

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
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
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
webofbits
With AI doing more of the implementation work, I’ve been wondering how much coding I should deliberately keep doing myself. My main conc...
#ai
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews