Model has multiple has-many / belongs-to - how to make association to both in one changeset?

Hi, I am following the Writing a Blog Engine in Phoenix tutorial on hackernoon and trying to adopt it to a travel blog, and I am a complete newb to functional programming.

I have created the typical User and Post models, and added a Place model. A travel blog post is about a particular place, so the schema for Post looks like:

schema "posts" do 
   ...
  belongs_to :user, PxBlog.User
  belongs_to :user, PxBlog.Place
end

And User schema is:
schema “users” do

has_many :posts, PxBlog.Post
end

And Place schema is:
schema “places” do

has_many :posts, PxBlog.Post
end

In the post_controller, and new action I had built an association between User and Post, but I do not understand how to adopt it to build an assoc to the Place model in one pipe:

Currently this works between User and Post.

changeset = 
      conn.assigns[:user]
      |> build_assoc(:posts)
      |> Post.changeset()

How do I add the assoc to Place model in ‘one’ changeset assignment? The following does not seem the right approach:

changeset = 
      conn.assigns[:user]
      |> conn.assigns[:place]
      |> build_assoc(:posts)
      |> Post.changeset()

I’m just trying to get around functional programming at the moment, and this | notation is confusing me. Thanks for looking and your help.

You can do it like this:

changeset = 
      conn.assigns[:user]
      |> build_assoc(:posts, place_id: place_id)
      |> Post.changeset()

Though I think it makes more sense to do that in the create action rather than new - because the relationship is not altered by the user in the new form.

The |> operator just takes the value on the left-hand side and add it as the first argument on the function on right-hand side. So in this case, conn.assigns[:user] |> build_assoc(:posts) is the same as build_assoc(conn.assigns[:user], :posts).

You could never do something like

  conn.assigns[:user]
  |> conn.assigns[:place]
  |> build_assoc(:posts)

Because conn.assigns[:place] isn’t even a function - it’s a struct (also you probably wouldn’t store a %Place{} in the assigns map. You would get the place_id from the route params, for example).

Hope this helps :slightly_smiling_face:

Hi, thanks for taking the time to explain, especially the |> operator. That makes complete sense now when you put it like that.

You are right about putting this in the change action, but, unfortunately, am now stuck with the next issue, as place_id which is available from route params, post_params["place_id"], is now of type value and I get a type error when trying to make a Repo.insert with this changeset.

There is also something I can’t seem to get my head around in your suggestion:
changeset =
conn.assigns[:user]
|> build_assoc(:posts, place_id: place_id)
|> Post.changeset()

Indeed, why do we have conn.assigns[:user] |> build_assoc(:posts, place_id: placed_id) and not the other way around, i.e. conn.assigns[:place] |> build_assoc(:posts, user_id: user_id), as Post belongs_to both User and Place?

Many thanks.

Ah, sorry, ignore what I just wrote about the error - I figured that I had not put :place_id in my list of params to cast in Post changeset.

The order of conn.assigns[:user] and conn.assigns[:place] is still troubling for me though.

Hi Phoebe,

Make sure your post schema has different field names. You have “user” twice. I assume you want the second to be “place”.

Hope this helps.

1 Like

Phoebe,

Just to shed some light on the conn.assigns issue it is important to note:

  1. conn is the connection struct. You can find more information of the fields that holds here: https://github.com/elixir-plug/plug/blob/master/lib/plug/conn.ex

  2. conn.assigns is a map that will hold any information you put on it. Normally, you put the logged in user struct under the key “user” inside this map so you can retrieve it as conn.assigns[:user]. So if you don’t add a “place” key inside this map you wont be able to access it as conn.assigns[:place]. If you are adding the place key inside the conn.assigns map, it must be a struct because the build_assoc receives as fist parameter a struct.

You can check the build_assoc documentation here; https://hexdocs.pm/ecto/Ecto.html#build_assoc/3

  1. If you do have a struct in the conn.assigns[:place] I assume it should work either way. Basically what build_assoc does under the hood is to add the id of the given struct to the association.

This is a snippet of the ecto build_assoc/3 official documentation:

Another function in `Ecto` is `build_assoc/3`, which allows
  someone to build an associated struct with the proper fields:
  Repo.transaction fn ->
    post = Repo.insert!(%Post{title: "Hello", body: "world"})
    # Build a comment from post
    comment = Ecto.build_assoc(post, :comments, body: "Excellent!")
    Repo.insert!(comment)
  end
  In the example above, `Ecto.build_assoc/3` is equivalent to:
  %Comment{post_id: post.id, body: "Excellent!"}

Hope this helps.

1 Like

Thanks, very helpful!