Many To Many Relationship Example

After reading https://www.ash-hq.org/docs/guides/ash/latest/topics/relationships.md#has-many and https://ash-hq.org/docs/dsl/ash/latest/ash-resource-dsl/relationships/many_to_many I am a little bit confused since the examples use different code. I am looking for a simple example which I can copy and paste. Something like a blog post which has multiple tags associated via a many to many relationship:
defmodule Example.Blog.Post do
use Ash.Resource,
data_layer: AshPostgres.DataLayer

attributes do
uuid_primary_key :id

attribute :content, :string do
allow_nil? false
end
end

postgres do
table "blog_posts"
repo Example.Repo
end

actions do
defaults [:create, :read]
end
end
defmodule Example.Blog.Post do
use Ash.Resource,
data_layer: AshPostgres.DataLayer

attributes do
uuid_primary_key :id

attribute :content, :string do
allow_nil? false
end
end

postgres do
table "blog_posts"
repo Example.Repo
end

actions do
defaults [:create, :read]
end
end
defmodule Example.Blog.Tag do
use Ash.Resource,
data_layer: AshPostgres.DataLayer

attributes do
uuid_primary_key :id

attribute :name, :string do
allow_nil? false
end
end

postgres do
table "tags"
repo Example.Repo
end

actions do
defaults [:create, :read]
end
end
defmodule Example.Blog.Tag do
use Ash.Resource,
data_layer: AshPostgres.DataLayer

attributes do
uuid_primary_key :id

attribute :name, :string do
allow_nil? false
end
end

postgres do
table "tags"
repo Example.Repo
end

actions do
defaults [:create, :read]
end
end
- What do I have to change in the existing files? - How do I have to do the third resource? - How would I add a tag to a blog post the Ash way?
3 Replies
ZachDaniel
ZachDaniel3y ago
There should probably be a guide specifically for this. Here is how I'd typically set it up:
defmodule Example.Blog.PostTag do
use Ash.Resource,
data_layer: AshPostgres.DataLayer

attributes do
uuid_primary_key :id
end

identities do
identity :unique_post_tag, [:post_id, :tag_id]
end

relationships do
belongs_to :post, Example.Blog.Post do
allow_nil? false
end

belongs_to :tag, Example.Blog.Tag do
allow_nil? false
end
end
end

# in your Post resource

relationships do
many_to_many :tags, Example.Blog.Tag do
through Example.Blog.PostTag
source_attribute_on_join_resource :post_id
source_attribute_on_join_resource :tag_id
end
end
defmodule Example.Blog.PostTag do
use Ash.Resource,
data_layer: AshPostgres.DataLayer

attributes do
uuid_primary_key :id
end

identities do
identity :unique_post_tag, [:post_id, :tag_id]
end

relationships do
belongs_to :post, Example.Blog.Post do
allow_nil? false
end

belongs_to :tag, Example.Blog.Tag do
allow_nil? false
end
end
end

# in your Post resource

relationships do
many_to_many :tags, Example.Blog.Tag do
through Example.Blog.PostTag
source_attribute_on_join_resource :post_id
source_attribute_on_join_resource :tag_id
end
end
Stefan Wintermeyer
I can write a tutorial for this or enrich the examples in the current docs. Let's have that discussion in #documentation Bonus question: What is "the Ash" way of adding a Tag to a Post? I know how to do this in Ecto but I am still unsure how an Ash developer tackles this best.
ZachDaniel
ZachDaniel3y ago
There are two ways: manage_relationship and a custom change. manage_relationship:
update :add_tag do
accept []

argument :tag_name, :string do
allow_nil? false
end

change manage_relationship(
:tag_name,
:tags,
on_lookup: :relate,
on_no_match: :create
)
end
update :add_tag do
accept []

argument :tag_name, :string do
allow_nil? false
end

change manage_relationship(
:tag_name,
:tags,
on_lookup: :relate,
on_no_match: :create
)
end
custom change
update :add_tag do
accept []

argument :tag_name, :string do
allow_nil? false
end

change fn changeset, _ ->
Ash.Changeset.after_action(fn changeset, post ->
tag_name = Ash.Changeset.get_argument(changeset, :tag_name)
# assuming the create code interface on tag
# (you can also make a dedicated upsert action)
tag = Example.Blog.Tag.create!(tag_name, upsert?: true, upsert_identity: :unique_name)
# assuming a create code interface on blog_tag
Example.Blog.BlogTag.create!(post.id, tag.id)
{:ok, post}
end)
end
end
update :add_tag do
accept []

argument :tag_name, :string do
allow_nil? false
end

change fn changeset, _ ->
Ash.Changeset.after_action(fn changeset, post ->
tag_name = Ash.Changeset.get_argument(changeset, :tag_name)
# assuming the create code interface on tag
# (you can also make a dedicated upsert action)
tag = Example.Blog.Tag.create!(tag_name, upsert?: true, upsert_identity: :unique_name)
# assuming a create code interface on blog_tag
Example.Blog.BlogTag.create!(post.id, tag.id)
{:ok, post}
end)
end
end

Did you find this page helpful?