Loading related resource, filtered using a property on the Join Table

https://hexdocs.pm/ash/relationships.html#more-complex-data-loading Looking here I see I can specify a query for the loaded resource which is sweet. What I'm wondering is if we have some metadata about the relationship present on the join table, what is the right/ash way to filter the loaded tables? A contrived example would be check User A's relationship to other users, filtered on created_date or some enum. Specifically for many_to_many, though I assume the answer would apply for all
29 Replies
ZachDaniel
ZachDaniel3y ago
At the moment, in order to do that what you'd need to do is load the relationship through the join table I believe. For example:
join_query = JoinResource |> Ash.Query.filter(...) |> Ash.Query.load(:destination)
Query.load(join_relationship: join_query)
join_query = JoinResource |> Ash.Query.filter(...) |> Ash.Query.load(:destination)
Query.load(join_relationship: join_query)
gvanders
gvandersOP3y ago
Would that Query.load be a standalone query, so query the join_table with the filter of User A and the extra filter we want? Or is the load :join_relationship part of the resource query? I tried the latter and am getting errors I guess what I'm wondering is if its possible to
recent_friends_query = JoinResource |> Ash.Query.filter(...) |> Ash.Query.load(:destination)

Resource |> Ash.Query.load([:name, :blah, recent_friends: recent_friends_query])
recent_friends_query = JoinResource |> Ash.Query.filter(...) |> Ash.Query.load(:destination)

Resource |> Ash.Query.load([:name, :blah, recent_friends: recent_friends_query])
ZachDaniel
ZachDaniel3y ago
Well, kind of So every many_to_many relationship has a corresponding has_many relationship if you don't configure it, we call it <name>_join_relationship So you can do what you had there, but you'd have to go through the join resource There are some ways to solve for this, specifically manual relationships and/or calculations which allow you to write arbitrary code to produce the result set you want
gvanders
gvandersOP3y ago
Yeah this felt like something that would be cleanest with calculations. Haven't done anything with those yet
ZachDaniel
ZachDaniel3y ago
Manual relationships are also quite useful because they can be upgraded to be filterable using AshPostgres.ManualRelationship, where you provide the underlying ecto join implementation So if the product of a calculation is one or more other resources, a manual relationship should be preferred although at the moment, you can't provide arguments to a manual relationship It won't hurt to start it as a calculation to start though
gvanders
gvandersOP3y ago
Thank you! If I want to be able to hook up a form to let the user change the join_params, I see there is this _join but I don't quite understand it. This field should be present using auto?:true on in for_create , for_update of a many_to_many resource? I am not seeing it, I assume it has something to do with managed relationships? https://hexdocs.pm/ash_phoenix/AshPhoenix.Form.Auto.html#module-many-to-many-relationships
ZachDaniel
ZachDaniel3y ago
Yeah, so what you'd need to do is configure the join writable fields in the manage_relationship call The docs for Ash.Changeset.manage_relationship show how that can be done, but it requires setting explicit options
* `:ignore`(default) - Does not look for existing entries (matches in the current relationship are still considered updates)
* `:relate` - Same as calling `{:relate, primary_action_name}`
* `{:relate, :action_name}` - the records are looked up by primary key/the first identity that is found (using the primary read action), and related. The action should be:
* many_to_many - a create action on the join resource
* has_many - an update action on the destination resource
* has_one - an update action on the destination resource
* belongs_to - an update action on the source resource
* `{:relate, :action_name, :read_action_name}` - Same as the above, but customizes the read action called to search for matches.
* `:relate_and_update` - Same as `:relate`, but the remaining parameters from the lookup are passed into the action that is used to change the relationship key
* `{:relate_and_update, :action_name}` - Same as the above, but customizes the action used. The action should be:
* many_to_many - a create action on the join resource
* has_many - an update action on the destination resource
* has_one - an update action on the destination resource
* belongs_to - an update action on the source resource
* `{:relate_and_update, :action_name, :read_action_name}` - Same as the above, but customizes the read action called to search for matches.
* `{:relate_and_update, :action_name, :read_action_name, [:list, :of, :join_table, :params]}` - Same as the above, but uses the provided list of parameters when creating
the join row (only relevant for many to many relationships). Use `:all` to *only* update the join record, and pass all parameters to its action The default value is `:ignore`.
* `:ignore`(default) - Does not look for existing entries (matches in the current relationship are still considered updates)
* `:relate` - Same as calling `{:relate, primary_action_name}`
* `{:relate, :action_name}` - the records are looked up by primary key/the first identity that is found (using the primary read action), and related. The action should be:
* many_to_many - a create action on the join resource
* has_many - an update action on the destination resource
* has_one - an update action on the destination resource
* belongs_to - an update action on the source resource
* `{:relate, :action_name, :read_action_name}` - Same as the above, but customizes the read action called to search for matches.
* `:relate_and_update` - Same as `:relate`, but the remaining parameters from the lookup are passed into the action that is used to change the relationship key
* `{:relate_and_update, :action_name}` - Same as the above, but customizes the action used. The action should be:
* many_to_many - a create action on the join resource
* has_many - an update action on the destination resource
* has_one - an update action on the destination resource
* belongs_to - an update action on the source resource
* `{:relate_and_update, :action_name, :read_action_name}` - Same as the above, but customizes the read action called to search for matches.
* `{:relate_and_update, :action_name, :read_action_name, [:list, :of, :join_table, :params]}` - Same as the above, but uses the provided list of parameters when creating
the join row (only relevant for many to many relationships). Use `:all` to *only* update the join record, and pass all parameters to its action The default value is `:ignore`.
That is the example for on_lookup So if you want to support something like [:foo] being editable on the join resource action, you'd do something like this:
change manage_relationship(:relationship,
on_match: {:update, :update, :update, [:foo]},
on_missing: {:create, :create, :create, [:foo]}
)
change manage_relationship(:relationship,
on_match: {:update, :update, :update, [:foo]},
on_missing: {:create, :create, :create, [:foo]}
)
I'd really like to change that format at some point to support a nested keyword list so you don't have to provide action names just to change the join fields, i.e on_match: [join_attributes: [:foo]], but thats for another day. But that would pass the :foo field along to the join resource actions, and should cause the :_join form to be added
gvanders
gvandersOP3y ago
Is there has_many through?
ZachDaniel
ZachDaniel3y ago
There is not (but if the link is only one relationship away, that is the same as a many_to_many)
gvanders
gvandersOP3y ago
Also thanks a ton for all the responses. Got the join relationship forms working
ZachDaniel
ZachDaniel3y ago
Eventually I'd like to have has_many ..., through: [:arbitrary, :link, :of, :things] and has_one ...., through: [...]
gvanders
gvandersOP3y ago
Yeah I see some past chatter about through chaining What would the API of that be for keyword list? I see with many_to_many you specify source/destination attributes for the through. How could you condense into keyword list for each step of the through chain Picking up elixir so learning how people think about these keyword config lists is super helpful
ZachDaniel
ZachDaniel3y ago
In that case, it would be referring to already defined relationships on the other resources So it would just be a list of atoms
gvanders
gvandersOP3y ago
ahhhhh that makes sense
ZachDaniel
ZachDaniel3y ago
Which, honestly, would not be difficult to define as a manual relationship
gvanders
gvandersOP3y ago
so the agreement would be if you want to do 2+ level through chains, create resources for your join tables
ZachDaniel
ZachDaniel3y ago
Yeah, well currently we need a resource for everything and although its more verbose than something like ecto which allows you to state just the join table as a string, in Ash it makes sense because everything we do is based off of configuration of resource actions So you could even do things like write policies on the join resource and have those enforced
gvanders
gvandersOP3y ago
I like having everything explicit Ash feels like it does lots of magic, so having the configuration at least not be magic is a good compromise
ZachDaniel
ZachDaniel3y ago
has_many :foos, {HasManyThrough, path: [:foo, :bar, :baz]}

defmodule HasManyThrough do
use Ash.Resource.ManualRelationship

def load(resources, ....) do
# do the logic to get resources through a list of relationships
end
end
has_many :foos, {HasManyThrough, path: [:foo, :bar, :baz]}

defmodule HasManyThrough do
use Ash.Resource.ManualRelationship

def load(resources, ....) do
# do the logic to get resources through a list of relationships
end
end
gvanders
gvandersOP3y ago
Decided I'm gonna explicitly create the join has_many resources too and set w join_relationship
ZachDaniel
ZachDaniel3y ago
Yeah, I can see why you'd do that 👍 first class the link relationship
gvanders
gvandersOP3y ago
woah! so the main verboseness of a manualrelationship is defining the load? Because otherwise that doesn't seem so bad
ZachDaniel
ZachDaniel3y ago
It depends 🙂 If all you want to do is load the related data (and not filter/aggregate the values), then yes but if you want to do things like Ash.Query.filter(query, exists(foos, bar == ^baz))
gvanders
gvandersOP3y ago
ahhh
ZachDaniel
ZachDaniel3y ago
Then you'll need to do use AshPostgres.ManualRelationship, and define how to join to the target
gvanders
gvandersOP3y ago
Thanks 🙂
ZachDaniel
ZachDaniel3y ago
my pleasure 🙂 If you think you'll have lots of "through" relationships, you could try and throw a generic manual relationship together, and we could potentially put it up in a guide while waiting for it to be implemented in core.
gvanders
gvandersOP3y ago
Believe only 1 level so far, but there's a chance I run into 2 soon. In which case, definitely

Did you find this page helpful?