Hello everyone!
Something happened at work today and it kinda got me off-guard because I was expecting different behavior from Ecto. What happened is: I was trying to insert a record with a given association that already existed in the database without casting the foreign key directly.
For example:
schema "post" do
field :title, :string
field :body, :string
end
schema "category" do
field :name, :string
field :description, :string
end
schema "category_post" do
belongs_to :post, Post
belongs_to :category, Category
field :remarks, :string
end
def changeset(category_post, attrs) do
post
|> cast(attrs, [:remarks])
|> cast_assoc(:post)
|> cast_assoc(:category)
|> validate_if_posts_has_the_right_amount_of_comments() # needs the retrieved post
|> validate_if_category_has_the_right_amount_of_followers() # needs the retrieved category
end
And then, trying to create a “category_post” with its relationship:
def create_category_post(attrs, post_id, category_id) do
post = Posts.get_post!(post_id)
category = Categories.get_category!(category_id)
attrs = Map.merge(attrs, %{category: category, post: post})
%CategoryPost{}
|> CategoryPost.changeset(attrs)
|> Repo.insert()
end
Is this even possible? I noticed that for this case Ecto always tries to insert a new “post”/ “category” if I use cast_assoc/3
.
2 Likes
If the record already exists, You should use put_assoc instead.
But for this use case, I would use build_assoc.
1 Like
Hi @kokolegorille, thanks for the reply.
Could you elaborate on why would you choose one over the other?
Do You mean cast_assoc over put_assoc, or put_assoc over build_assoc?
I would use cast_assoc in case I want to use aggregate root.
I would use put_assoc, if the association already exists.
And the last case is to use build_assoc in case I want to set … a belongs to. For example if I need to set an owner, or in your case, a category.
Sorry about the confusion. I meant build_assoc
over put_assoc
.
If it’s not asking too much, could you exemplify?
Instead of using a normal post, You will use…
post = build_assoc(category, :posts)
and then, pass this post to the changeset, You don’t need to cast anything, category_id will be set for You.
I see. But in this specific case, I would have to have a has_many
association on the other side, right?
In my scenario, I only have the belongs_to
association on the record that is currently being inserted.
PS.: I’ve updated my original post examples to better represent the challenge I’m facing.
Yes, You need to have the has_many on the other side… or cast category_id on the post side, but it’s not something I like to do when I can do with build_assoc.
Maybe You can simply pass a post where You manually set category_id, like this %Post{category_id: category.id} before passing it to changeset, but I have never tried.
Hi!
You can add a field
for your FK id
and simply cast that in your changeset 
schema "table_table" do
belongs_to :post,
Post,
[foreign_key: :post_id]
field :related_post, :integer,
[source: :post_id]
end
def changeset(schema, attrs) do
schema
|> cast(attrs, [:related_post])
|> cast_assoc(:post)
end

trying to update my own code but I can’t so posting it again, this is clearer:
schema "table_table" do
belongs_to :related_post,
Post,
[foreign_key: :post_id, define_field: false]
field :post_id, :integer
# and you can add the following to opts if you want
# the field and the column on the DB to have
# different names:
# [source: :"table_column_id"]
end
def changeset(schema, attrs) do
schema
|> cast(attrs, [:post_id])
|> cast_assoc(:related_post)
end
1 Like