has_one without belongs_to in ecto/phoenix/elixir
Asked Answered
T

2

5

Problem

I have a files table and there are many other tables that create a one-to-one association, e.g. users might have a avatar and posts might have photo.

A possible solution

A possible solution would be to create users_files and posts_files tables and use has_one :through. However, this looks excessive.

The ideal solution

The ideal solution would be to define tables like this

users
 - avatar_id

posts
 - photo_id

and have with: parameter in has_one so the schema looks like this

schema "users" do
    has_one :avatar, MyApp.FileDb, with: :avatar_id, foreign_key: :id #id is default

end

schema "posts" do
    has_one :photo, MyApp.FileDb, with: :photo_id, foreign_key: :id

end

and that way you don't need to define a belongs_to on files. Is there a similar mechanism already? What is the standard way to deal with this in Phoenix?

Tuner answered 19/9, 2015 at 15:49 Comment(0)
L
10

You cannot get away of not having the belongs_to because that's where the foreign key is defined. You have two alternatives:

  1. Flip the relationship so both users and posts have an avatar_id and photo_id pointing to the files table

  2. Define both "users_files" and "posts_files" tables without a "files" table. "users_files" and "posts_files" will have the complete table structure which can be shared at the model level in Ecto. We actually talk about this case in Ecto docs: http://hexdocs.pm/ecto/Ecto.Schema.html#belongs_to/3 (see the polymorphic section)

Lichenology answered 19/9, 2015 at 16:23 Comment(1)
so its ok two have multiple belongs_to in files schema: belongs_to :users && belongs_to :posts this would require users_id and posts_id in the file table.. it's nice for tables like imagesPolad
W
0

I believe what's being described here is a Unidirectional One-to-One association. A files table can be used as a base table for avatars, post photos, and many others. If you make the files table the "owning" side, then you end up having an FK column for every entity that has a file association.

Unfortunately, I believe that if you put the FK on the avatar (by using belongs_to), then you can't cascade delete the file (that's reserved for the has_one side). So, if you delete the avatar, the file is now orphaned. Ideally, the avatar would not only be the owner of the association (have the FK), but also have the ability to cascade the delete.

Anyway, if you'd rather not put the FK on the files table, then you can address the file cascade by deleting the file (not the avatar). It gets the job done, but it's a little hackish feeling (mostly because you're saying an avatar "belongs" to a file, when from a domain perspective, it's really the other way around).

For instance, instead of using Repo.delete(avatar), you'd create a delete function that actually deletes the file. In that function, you can preload the file, and delete that. This will cascade the deletion of the avatar (assuming you have has_one in the file schema, which does not affect the db).

For example:

def delete(%__MODULE__{} = avatar) do
  avatar
  |> Repo.preload([:file])
  |> Map.get(:file)
  |> Repo.delete()
  |> case do
    {:ok, _file} -> {:ok, avatar}
    {:error, changeset} -> {:error, changeset}
  end
end

Worth noting, Doctrine uses terms like mappedBy and inversedBy to avoid these ORM/domain naming conflicts. Granted, it's rarely an issue in Ecto, and I think this is probably more of an exception.

Windy answered 14/2, 2018 at 23:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.