3

What is the preferred way of selecting a specific record out of a has_many relation for a specific model in Rails? (I'm using Rails 5.)

I have a model User and a model Picture which are related via the following code:

class User < ApplicationRecord
  has_many :pictures, as: :imageable
  # ...
end

class Picture < ApplicationRecord
  belongs_to :imageable, polymorphic: true
  # ...
end

What I want to do is to allow a User to set a profile picture from the images associated with it, so I can call @user.profile_picture on a User object and retrieve the profile picture.

mindseyeblind
  • 119
  • 10
  • You'd need to store it in `User`, probably as `has_one` relation. You can call this relation `profile_picture` :) – Jagjot Nov 22 '16 at 11:07
  • So `has_one :profile_picture, as: :imageable`? How does this persist in the DB? Do I need to create a `ProfilePicture` model? – mindseyeblind Nov 22 '16 at 11:08
  • Yes, and don't forget the migration for this relation as well. – Jagjot Nov 22 '16 at 11:10
  • So to sum it up: `has_one :profile_picture` in `User` model. New `ProfilePicture` model that `has_one :picture, as: :imageable` and `belongs_to :user`. Is that all? – mindseyeblind Nov 22 '16 at 11:14
  • Yups! That sums it up. – Jagjot Nov 22 '16 at 11:15
  • Why use polymorphism if you are creating a simple one-to-one relationship between two known tables? Placing the foreign key column on the owning side (users) is far simpler and will allow effective joins. – max Nov 22 '16 at 11:20

1 Answers1

2

You can add an additional one-to-one relationship.

# create by running:
# rails g migration AddPrimaryPictureToUser
class AddPrimaryPictureToUser < ActiveRecord::Migration[5.0]
  def change
    add_column :users, :primary_picture_id, :integer
    add_index :users, :primary_picture_id
    add_foreign_key :users, :pictures, column: :primary_picture_id
  end
end

class User < ApplicationRecord
  has_many :pictures, as: :imageable
  # This should be belongs_to and not has_one as the foreign key column is 
  # on the users table
  belongs_to :primary_picture, 
             class_name: 'Picture',
             optional: true
end


class Picture < ApplicationRecord
  belongs_to :imageable, polymorphic: true
  has_one :primary_user, 
          foreign_key: 'primary_picture_id',
          class_name: 'User'
  # ...
end

The main reason to do it this way vs for example a boolean flag on the pictures table is that having a separate relationship makes it easy to join which is important for avoiding N+1 queries which is an issue if you are listing a bunch of users together with their primary image.

@users = User.includes(:primary_picture).all
max
  • 96,212
  • 14
  • 104
  • 165
  • Interesting. Can you review the solution suggested by the commenter above and tell me about the benefits/tradeoffs of each architecture? – mindseyeblind Nov 22 '16 at 11:20
  • Using polymorphism in this one-to-one relationship is a mistake since it will not allow you to do a direct join between users and their primary image. Using polymorphism also means you lose referential integrity since the relationship is not backed by a foreign key. – max Nov 22 '16 at 11:25
  • Alright, thanks! Why is `optional: true` only set in `User` and not `Picture`? – mindseyeblind Nov 22 '16 at 12:02
  • In Rails 5 the default for `belongs_to` was changed to `optional: false` which means that the model automatically validates the presence of the association - this is not the case for `has_one`. – max Nov 22 '16 at 13:43