Help with building associations

Hey all!

I know this a really simple problem to be stuck on, but I can’t quite work out how to build associations between different models. I know that I need to be using Ecto.build_assoc/3, and that I need to start from the has_one or has_many.

I’ve read through the Ecto docs, re-read the relevant sections in Programming Phoenix, and even tried to get the “a-ha!” moment from looking at projects like Phoenix Trello, as it has similar associations.

I’m trying to model a marketplace where a Conversation happens between a buyer, seller (both Users), around an Item. My Conversation model is:

schema "conversations" do
  belongs_to :buyer, Tic.User
  belongs_to :seller, Tic.User
  belongs_to :item, Tic.Item
end

I suspect my confusion might be caused by having two references to the User model?

How would I go about creating a new Conversation, from an Item page. My thought was that when a button is clicked from the page, a create action on the ConversationController fires with data about the Item and using Repo.preload the Item’s user (the seller). The buyer’s user account could then be picked up from the connection as per Programming Phoenix.

What I’ve been trying in iex thus far has been variations on:

buyer = Repo.get(User, 9)
item = Repo.get(Item, 11)
item = Repo.preload(item, :user)
seller = item.user

conversation = Ecto.build_assoc(item, :conversations)
conversation = Ecto.build_assoc(buyer, :conversations)
conversation = Ecto.build_assoc(seller, :conversations)

I know that’s really messy and convoluted, but I’ve been trying to break everything down as much as possible to work out what’s what. The issue I’m getting is associating the two Users with the Conversation. Do I need to specify how the Conversation model refers to users somewhere (i.e. “buyer” and “seller”)? Perhaps the Polymorphic Associations from the belongs_to docs is what I need to be looking at?

Once again, apologies for the probably simple problem, but I’m struggling to get my head round this. Cheers!

1 Like

I’d split conversation into messages, something like

schema "messages" do
  belongs_to :user, Tic.User
  belongs_to :item, Tic.Item
end

this way you model a message that belongs to one user and one item. If tree structure is desired an extra nullable parent_id with message id could do that

1 Like

I had planned to have messages as a child of conversations if that makes sense? Each message would have no knowledge of the item, only the conversation it is a part of? Would this not be the best way to model my data then?

I thought that I would need a Conversation structure to better segregate all the messages for a particular item. My intention is that each conversation between two users about an item is private, and only viewable by those two people.

1 Like

I tried to reproduce your situation (very quickly) and think it actually might be the case with having multiple relationships with the same table, say here Ecto.build_assoc(buyer, :conversations) a buyer is a user so how does ecto know what relationship to use, seller_id or buyer_id? There might be a way to do that but it’s not as straightforward as few min of playing with iex. Anyway what you want makes sense, but the way to achieve it doesn’t seem right to me.

I had planned to have messages as a child of conversations if that makes sense? Each message would have no knowledge of the item, only the conversation it is a part of

If you want this type of structure make a conversation that belongs to an item and has many messages then, each message would belong to a conversation and a user.

Add many to many relationship between conversations and users and you’re set with the privacy thing - any user can be limited with viewing the conversations they participate in but you wouldn’t have to hardcode two users in the app design - if there are two it works as you want now, if more - they all have access.

2 Likes

Thanks very much! That’s really helped!

It confirmed that the issues I’m having probably are the multiple relationships to the same table. For now, I can probably build the Conversation struct manually until I find a more elegant way of doing things.

With regards to the data, that’s the structure I have now (between items and conversations, conversations and messages, and messages and users). The only relationship I’m going to change is as you suggested, make the conversations to user be a many-to-many relationship. I’m not sure I’d ever want to open out the messaging to more than two parties, but it’s nice to have the option!

Incidentally, would I be right in saying that adopting this approach avoids the issues with the multiple relationships on the same table?

Thank you so much for your help! :+1: This is not only my first real foray in Elixir/Phoenix, but also relational data structures, so there’s a bit of a learning curve!

1 Like

Incidentally, would I be right in saying that adopting this approach avoids the issues with the multiple relationships on the same table?

yes, this way you’d have few normal one to many and additionally one (also normal) many to many relationship

1 Like

Me again, I’ve made the changes and turned the relationship between users and conversations in a many-to-many. However, I’m struggling to build all the associations I need (conversation has many users, and one item) in a clear and efficient way.

I’ve got to the point where, when a user is on an item, and they enquire, they trigger the Conversation.create action in the controller which looks like this:

def create(conn, %{"item" => item_id}, user) do
    item = Repo.get(Item, item_id)
    seller = Repo.get(User, item.user_id)

    conversation = 
      Repo.insert!(%Conversation{})
      |> Repo.preload(:users)
      |> Ecto.Changeset.change()
      |> Ecto.Changeset.put_assoc(:users, [user,seller])

    case Repo.update(conversation) do
      {:ok, conversation} ->
        conn
        |> redirect(to: conversation_path(conn, :show, conversation.id))
      {:error, changeset} ->
        conn
        |> put_flash(:info, "Error starting conversation")
        |> redirect(to: marketplace_path(conn, :show, item))
    end
  end

However, this doesn’t feel right to me. It creates the conversation as I’d expect, relating the users to the conversation in the join table. It obviously doesn’t associate the Item and the Conversation, and I know one way to accomplish that. This just doesn’t feel very idiomatic Elixir to me.

In this case, where the only data coming from the POST action in the view like this…

<%= button("Start a conversation", to: "/conversations?item=#{@item.id}", method: "post") %>

…what’s the best way to create all the relations I need? Am I totally barking up the wrong tree here?

1 Like

After playing with a few more variations, some of which work, some that don’t; my big question is what struct do I start creating the changeset with?

Do I, as above, start with an empty Conversation struct, do I start with my current user, or do I start with the Item (and it’s user)? I think that’s the root of my confusion.

1 Like

not sure if I follow your confusion, but from what I understand I’d create a changeset on a conversation with all required assocs (s. https://hexdocs.pm/ecto/Ecto.Changeset.html#cast_assoc/3 you can also cast associations with their changesets) and fill it with data from the post, current user etc

2 Likes

I had got stuck on the idea that I had to base all of the change sets I create in the controller on a base struct (like and empty conversation, or the current_user from the connection). Equally, I think I was getting confused between where and when I used the various *_assoc functions, which made things even worse! :smiley:

All sorted now though! Thanks to everyone for the help.

2 Likes

that would be me, you are welcome :slight_smile: Sorry for the 16 days late answer, I really missed your last post, glad you figured everything out

1 Like

Yup, apologies. :blush: Thought someone else had chimed in too. My mistake.

1 Like