Corret pattern to access already loaded relationship data?

Lets assume we have something like this:

defmodule From do
  relationships do
    has_many :questions, Question
  end
end

defmodule Question do
  relationships do
    belongs_to :form, Form
    has_many :answers, Answer
  end
end

defmodule Document do
  relationships do
    belongs_to :form, Form
    has_many :answers, Answer
  end
end

defmodule Answer do
  relationships do
    belongs_to :question, Question
    belongs_to :document, Document
  end
end

My main entry point is a LiveView which loads the document like

document = Ash.read_one!(Document, load: [answers: :question, form: [:questions]])
# the "double" question join might seem excessive 
# but its required for some validations further down the line, so ignore it for now

In different parets of the liveview I need to access as an example the answer for a given question / question id. Of course I can just do

Enum.find(document.answers, fn answer -> answer.question_id == question_id end)

However, doing this in the LiveView feels spaghetti (and not in a good way).

I couldn’t find anyting in the docs about accessing patterns to access already loaded relationship data. In my head I would imagine something like:

Ash.calculate(document, :answer_for_question, args: %{question: question})
# perhaps a bit wordy?

MyContextOrCodeInterface.get_answer(%{question: question, document: document})
# deciding on finding the correct answer for the current document feels 
# like business logic that should live in the document and is 
# perhaps not code interface dependent?

# or perhaps?
defmodue Document do
  # same relationships as above

  calculations do
    calculate :get_answer, :struct do
      constraints instance_of: Answer
      load :answers

      argument :question, :struct, constraints: [instance_of: Question]
      argument :question_id, :uuid

      calculation &get_answer/2
    end
  end

  defp get_answer(records, %{arguments: %{question: %Question{id: question_id}}}), do: find_answers(records, question_id)
  defp get_answer(records, %{arguments: %{question_id: question_id}}), do: find_answers(records, question_id)

  defp find_answers(records, question_id) do
    Enum.map(records, fn record ->
      Enum.find(record.answers, fn answer -> answer.question_id == question_id end)
    end)
  end
end

I can’t decide which way is better. I could totally imagine that in the future deciding on returning an answer for a given document and question could totally depend on the actor as an example, so the code interface and liveview feel very wrong.

Much regards

The problem of loading data for LiveView is a difficult one :slight_smile:

What is right depends on a lot of factors, like how often the data might change, how much data you’re fetching, how much of it is shown conditionally/should be lazy loaded, etc.

I think the best thing to do here would be to create a module designed to localize these things that you’re doing to present this data into a module that you keep near the resource.

That keeps the spaghetti out of the liveview, but also doesn’t necessarily add “presentation only” concerns to the resource.

2 Likes

So you feel that the concern of preparting this data structure shouldn’t live on any of the presented resources?

One idea that I had this morning was to create an ash resource with a bunch of relationships to question, answer, form, etc. and persist it in ETS. This way fetching a given resource would be basically free and I wouldn’t have to keep potentially pretty big constructs in memory.

What do you think?

A few thoughts there.

It’s okay to put view layer logic in the resource assuming there is some tangible benefit for doing so. For instance, we often put useful calculations into the resource because its accessed in multiple places, or because we want some optimization that Ash.Resource can provide (like calculation loading dependencies), or because we display this same information over our APIs and want to ensure it doesn’t drift, etc.

But ultimately it can be a trap to fall into of “everything in my app must fall into a resource concept”. Knowing how to draw that line can be difficult though :slight_smile:

Given that ultimately what you need is to centralize a concept of “for some piece of data in memory, how to get the X for Y”, a function does a perfectly good job. It’s always a safe bet to start there, where possible.

As for using ETS to wrap various resources into some kind of presentational resource, or an aggregate resource that interacts with many others, that too is very possible. But unless I could articulate the benefit, I would definitely not make that choice. For example, perhaps there are performance issues doing it any other way and we end up doing it for caching, etc. etc. etc.

Finally, something to keep in mind, is that Ash has a facility for building arbitrary logic into a resource that retains the benefits of an Ash.Resource, like policies, type casting, telemetry, etc. That facility is “generic actions”. For example:

action :get_answer, :struct do
  argument :question, :struct, constraints: [instance_of: Question]
  constraints instance_of: Answer

  run fn input, _ -> 
    input.arguments.question 
    # ....
  end
end