mpj
mpj

Reputation: 5367

Rails 4 - has_one and has_many at the same time

I have the following setup:

class Album < ActiveRecord::Base
  has_many :photos
end

class Photo < ActiveRecord::Base
  belongs_to :album
end

What I would like to do, is defining a profile_photo and a cover_photo for each album, but I'm not sure on how to approach the design.

First attempt has been to use has_one :profile_photo, class: 'Photo' with a migration creating a albums.profile_photo_id field and a has_one :album on the Photo model. Didn't work, as the SQL query generated was not correct:

> album.profile_photo

Photo Load (0.4ms)  SELECT  "photos".* FROM "photos" WHERE "photos"."album_id" = $1 LIMIT 1  [["album_id", 16]]

=> #<Photo id: 14, album_id: 16, image: "image_l.jpg", created_at: "2015-05-21 20:03:42", updated_at: "2015-05-21 20:03:42">

Another approach would be to add booleans to the Photo model, like photos.is_profile_photo and then on the class create a scoped association, but I feel this is not optimal as:

Is there a "Rails way" to do this that I'm missing?

Upvotes: 0

Views: 604

Answers (2)

mpj
mpj

Reputation: 5367

I have finally gone with the second approach I mentioned.

class AddCoverAndProfileToPhotos < ActiveRecord::Migration
  def change
    add_column :photos, :is_profile, :boolean, default: false
    add_column :photos, :is_cover, :boolean, default: false
  end
end

Album class:

class Album < ActiveRecord::Base   
  has_many :photos, inverse_of: :album, dependent: :destroy

  has_one :profile_photo,
    -> { where is_profile: true },
    class_name: 'Photo'

  has_one :cover_photo,
    -> { where is_cover: true },
    class_name: 'Photo'

  accepts_nested_attributes_for :photos, allow_destroy: true

  delegate :empty?, to: :photos

  validates_associated :photos

  def remove_profile_photo
    profile_photo.update(is_profile: false) if profile_photo.present?
  end

  def remove_cover_photo
    cover_photo.update(is_cover: false) if cover_photo.present?
  end

  def set_profile_photo(photo)
    remove_profile_photo
    photo.update(is_profile: true)
  end

  def set_cover_photo(photo)
    remove_cover_photo
    photo.update(is_cover: true)
  end
end

And finally Photo class:

class Photo < ActiveRecord::Base
  mount_uploader :image, PhotoUploader

  belongs_to :album
  validates_presence_of :image
  validates_presence_of :album

  validates_uniqueness_of :is_profile, scope: :album, if: :is_profile?
  validates_uniqueness_of :is_cover, scope: :album, if: :is_cover?
end

Upvotes: 0

runn3r85
runn3r85

Reputation: 56

I would add 2 columns to the Album table

profile_photo_id
cover_photo_id

They would hold the id of the photo that was the profile and/or cover photo.

Then, in the Album model, you can easily add:

belongs_to :profile_photo, class_name: "Photo"
belongs_to :cover_photo, class_name: "Photo"

Then in the Photo model, you need:

has_many :profile_albums, class_name: "Album", foreign_key: "profile_photo_id"
has_many :cover_albums, class_name: "Album", foreign_key: "cover_photo_id"

(Note you can name them whatever you want, I picked these. You just need the class_name and foreign_key to point to the correct model with that id column)

Then you have the following associations:

Album.profile_photo => returns the photo with ID in the Album

Photo.profile_albums => returns all albums that have Photo as a profile picture

(the same applies to cover photo)

Upvotes: 2

Related Questions