Can I fake a polymorphic association with a virtual schema field?

So I’ve opted for the “easy method” of polymorphic associations, where you have a column for each possible polymorphic type. That’s the database implementation, but now I’m wondering what to do on the schema implementation.

The table looks something like this:

Table inbox_items {
  id int [pk]
  profile_id int [ref: <> profiles.id]
  
  post_id int [ref: < posts.id]
  poll_id int [ref: < posts.id]
  comment_id int [ref: < comments.id]
}

Is there a way for me to add a virtual field to the schema so that I can cast to and from the correct column? That way I could just create it like %InboxItem{association: %Post{}} and it would know to save the id to the post_id column. Similarly can I make it able to respond to something like Repo.preload(inbox_item, :association) so I can fill the virtual field when loading?

Are you sure about that?

You can create a custom ecto type but I would reconsider if this is the path you want to go down. You’ve read the ecto guides on the subject?

Yeah, it’s the first method shown in the guide. It’s really just one workaround or another, because the polymorphic association is what I need more or less. Do you have any alternative suggestions? I tried the many-to-many method and it was a nightmare, and the guides are remarkably incomplete on the subject.

I read a few blog posts but they all recommended against the more complex approaches. I had implemented the many-to-many and it was awful, and I considered table inheritance in postgres but was advised against it.

Actually almost my entire domain has these kinds of relationships. I really shouldn’t be using a relational db but I really wanted to use elixir, and I have limited graph db experience so I don’t want to deal with more learning since I’m on a time crunch to get my mvp shipped.

1 Like

Hello maybe I don’t get what you want but I think polymorphic_embed | Hex could helps you

Thanks, took a look at that. It’s not quite what I need but nice to know it exists.

I’m basically just trying to do a different version of:

Table inbox_items {
  recipient_id int [ref: > profiles.id, not null]
  
  inboxable_type varchar [not nul]
  inboxable_id int [not null]
}

I’m trying to do it in a way that preserves referential integrity. Thing is, a lot of the other methods have flaws that are imo worse than being without an FK constraint.

What about exclusive belongs to. You make your inbox_items hold fk for all your other models

1 Like

That’s what I’m using in the database side of things. What I’m trying to avoid having to do blow apart the schema with several different possible associations on every load from the db and save.

Thought the link you sent was one I’d seen before. See now that it’s not, checking it out.

That worked for assigning it to the correct column. For some reason I didn’t think of using pattern matching to assign the association. I supposed I could do the same thing if I used a virtual attribute. Do you have any idea how I could maybe load the correct struct into the virtual field with queries? Otherwise I’ve got to check :post_id, :poll_id, :comment_id etc manually. Is there a way I can make preload work to load the association?

# you want to left_join and preload the fields
query =
  from(
    item in InboxItem,
    left_join: post in assoc(item, :post),
    left_join: poll in assoc(item, :poll),
    left_join: comment in assoc(item, :comment),
    preload: [
      post: post,
      poll: poll,
      comment:comment 
    ]
  )

items = Repo.all(query, opts)

# I don't think it will be possible to set the virtual_field in the ecto query
# You can do something like below to set it but I think this part should be done
# in the view
Enum.map(items, fn item ->
  model =
    cond do
      is_binary(item.post_id) ->
        item.post

      is_binary(item.poll_id) ->
        item.comment

      is_binary(item.comment_id) ->
        item.comment
    end
   # add the model to the virtual field manually
  %{item | virtual_field: model}
end)
1 Like

Hypothetically speaking, if we can write a query that preload the good association by finding the one that is not nil, could we use something like :

select_merge: %{virtual_field: preload_querry()}

I have no idea of how to write that hypotheticall preload_querry though…