Reputation: 9859
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 would be to create users_files
and posts_files
tables and use has_one :through
. However, this looks excessive.
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?
Upvotes: 5
Views: 4259
Reputation: 952
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
andinversedBy
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.
Upvotes: 0
Reputation: 51339
You cannot get away of not having the belongs_to because that's where the foreign key is defined. You have two alternatives:
Flip the relationship so both users and posts have an avatar_id and photo_id pointing to the files table
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)
Upvotes: 10