Using manage_relationship with nested relationships

I have three resource modules:
defmodule Patient do
use Ash.Resource, data_layer: AshPostgres.DataLayer

postgres do
table "patients"
repo Repo
end

attributes do
uuid_primary_key(:id)
attribute(:first_name, :string)
attribute(:last_name, :string)
timestamps()
end

relationships do
has_many :event_series, EventSeries
end
end

defmodule EventSeries do
use Ash.Resource, data_layer: AshPostgres.DataLayer

postgres do
table "event_series"
repo Repo

custom_indexes do
index [:patient_id, :initial_date], unique: true
end
end

attributes do
uuid_primary_key(:id)
attribute(:initial_date, :date)
timestamps()
end

relationships do
belongs_to :patient, Patient, allow_nil?: false
has_many :exams, Exam
end
end

defmodule Exam do
use Ash.Resource, data_layer: AshPostgres.DataLayer

postgres do
table "exams"
repo Repo
end

attributes do
uuid_primary_key(:id)
attribute(:performed_on, :date)
timestamps()
end

relationships do
belongs_to :event_series, EventSeries, allow_nil?: false
end
end
defmodule Patient do
use Ash.Resource, data_layer: AshPostgres.DataLayer

postgres do
table "patients"
repo Repo
end

attributes do
uuid_primary_key(:id)
attribute(:first_name, :string)
attribute(:last_name, :string)
timestamps()
end

relationships do
has_many :event_series, EventSeries
end
end

defmodule EventSeries do
use Ash.Resource, data_layer: AshPostgres.DataLayer

postgres do
table "event_series"
repo Repo

custom_indexes do
index [:patient_id, :initial_date], unique: true
end
end

attributes do
uuid_primary_key(:id)
attribute(:initial_date, :date)
timestamps()
end

relationships do
belongs_to :patient, Patient, allow_nil?: false
has_many :exams, Exam
end
end

defmodule Exam do
use Ash.Resource, data_layer: AshPostgres.DataLayer

postgres do
table "exams"
repo Repo
end

attributes do
uuid_primary_key(:id)
attribute(:performed_on, :date)
timestamps()
end

relationships do
belongs_to :event_series, EventSeries, allow_nil?: false
end
end
With this in place, I was able to figure out how to insert patients into the database pretty easily, however my problems arose when trying to do the exams. The exams are loaded from an outside data source where we derive the event series based on a date value. So what I tried to do was:
Exam
|> Ash.Changeset.for_create(:create, %{performed_on: performed_on})
|> Ash.Changeset.manage_relationship(:event_series, %{patient_id: patient_id, initial_date: examdate}, type: :append_and_remove, on_no_match: :create)
|> Api.create()
Exam
|> Ash.Changeset.for_create(:create, %{performed_on: performed_on})
|> Ash.Changeset.manage_relationship(:event_series, %{patient_id: patient_id, initial_date: examdate}, type: :append_and_remove, on_no_match: :create)
|> Api.create()
When doing this, an error is returned saying that the event series is invalid because the patient relationship is not defined. Is it possible to do this with a call to manage_relationship or am I fundamentally missing something? Thanks so much for any assistance, Ash is very cool so far!
12 Replies
ZachDaniel
ZachDaniel•3y ago
So you have two options. To do it the way you're doing it, you can just add attribute_writable?: true to the patient relationship. And actually that is how I'd suggest that you do it, so lets just leave it at the first option. With that said, I'd suggest defining a custom action for this logic, and putting it in the resource.
# on exam

actions do
create :create do
accept [:performed_on]
argument :event_series, :map, allow_nil?: false
change manage_relationship(:event_series, type: :append_and_remove, on_no_match: :create)
end
end
# on exam

actions do
create :create do
accept [:performed_on]
argument :event_series, :map, allow_nil?: false
change manage_relationship(:event_series, type: :append_and_remove, on_no_match: :create)
end
end
Then you can do Exam |> Ash.Changeset.for_create(:create, %{performed_on: performed_on, event_series: %{...}}) |> Api.create()
mylanconnolly
mylanconnollyOP•3y ago
Thanks so much! I tried the first option and that solved my problem for the meantime. I'm going to probably refactor the code to use the custom actions, though, since that looks cleaner for what I'm ultimately trying to accomplish. thanks for answering so quickly and for the hard work on Ash!
ZachDaniel
ZachDaniel•3y ago
Generally speaking, you should prefer to put as much of your business logic behind your resources as possible, as they are meant to be more "domain objects" than "database objects" (although the two intersect often).
mylanconnolly
mylanconnollyOP•3y ago
That makes sense, I'll try to keep that in mind. It's a different way compared to what I'm used to but I can definitely see the usefulness of it I just encountered another hiccup. I updated the code slightly:
defmodule EventSeries do
use Ash.Resource, data_layer: AshPostgres.DataLayer

postgres do
table "event_series"
repo Repo
end

attributes do
uuid_primary_key(:id)
attribute(:initial_date, :date)
timestamps()
end

identities do
identity :patient_initial_date, [:patient_id, :initital_date]
end

relationships do
belongs_to :patient, Patient, allow_nil?: false
has_many :exams, Exam
end
end
defmodule EventSeries do
use Ash.Resource, data_layer: AshPostgres.DataLayer

postgres do
table "event_series"
repo Repo
end

attributes do
uuid_primary_key(:id)
attribute(:initial_date, :date)
timestamps()
end

identities do
identity :patient_initial_date, [:patient_id, :initital_date]
end

relationships do
belongs_to :patient, Patient, allow_nil?: false
has_many :exams, Exam
end
end
(note that I removed the custom_indexes block and replaced it with an identies block). I changed the changeset logic somewhat to include the identity:
Exam
|> Ash.Changeset.for_create(:create, %{performed_on: performed_on})
|> Ash.Changeset.manage_relationship(:event_series, %{patient_id: patient_id, initial_date: examdate}, type: :append_and_remove, on_no_match: :create, use_identities: [:patient_initial_date])
|> Api.create()
Exam
|> Ash.Changeset.for_create(:create, %{performed_on: performed_on})
|> Ash.Changeset.manage_relationship(:event_series, %{patient_id: patient_id, initial_date: examdate}, type: :append_and_remove, on_no_match: :create, use_identities: [:patient_initial_date])
|> Api.create()
It is possible for multiple exams to exist in the same series, so I was hoping it'd look up the event series in the database first, but it doesn't seem to be doing that. Is this something I can change or should I just create the series as a separate action at this point? Just trying to wrap my head around it. Thanks!
ZachDaniel
ZachDaniel•3y ago
What should happen if it looks it up?
mylanconnolly
mylanconnollyOP•3y ago
I was hoping it'd use the event series that it found in the database
ZachDaniel
ZachDaniel•3y ago
That is what append_and_remove should do
mylanconnolly
mylanconnollyOP•3y ago
hm, strange... I might be doing something wrong. I see some errors about a violated uniqueness constraint in those cases where the series already exists in the database
ZachDaniel
ZachDaniel•3y ago
lol so we should be giving you an error for this initital_date I guess we're allowing an identity with an invalid field 😢
mylanconnolly
mylanconnollyOP•3y ago
hmm, like my resource is messed up or the data is invalid?
ZachDaniel
ZachDaniel•3y ago
no there is a typo there inititial_date -> initial_date
mylanconnolly
mylanconnollyOP•3y ago
lol woops. ok good catch. I should probably call it a night, it looks like 🙂 working like a charm now. thanks again!

Did you find this page helpful?