Limit number of objects in has_many association
Asked Answered
T

4

29

I have an album which has_many photos. A counter_cache setup updates the photos_count column in the album table. How do I limit the number of photos for an album?

Theresa answered 14/2, 2010 at 23:20 Comment(2)
check the count before inserting?Scarberry
probably not the most cute of all, but the most safe is @Marcel Jackwerth sulution, others with use of validates_associated allow you to create children without limit using parent.children.createOsorio
H
48

In my case, it was sufficient to use validates_length_of:

class Album
  has_many :photos
  validates_length_of :photos, maximum: 10
end

class Photo
  belongs_to :album
  validates_associated :album
end
Hallmark answered 21/5, 2015 at 16:13 Comment(5)
This is the cleanest solutionBatwing
Can confirm this works really well. Only problem is if you go the other way and just start creating a bunch of "Photos" that belong to album the same cannot be said. You will need to add :validate => true to the belongs_to, though I haven't tested to make sure this will always work as intended.Gillan
actually belongs_to :album, validate: true according to api.rubyonrails.org/classes/ActiveRecord/Associations/… checks associations on the parent save, not on child save... so you can add as many children as you want, and this could be a problemOsorio
No need of validates_associated :album in Photo model. It will work without validates_associated as well.Snubnosed
I could create 11 photos using this codePled
G
29

Use a validation hook:

class Album
  has_many :photos
  validate_on_create :photos_count_within_bounds

  private

  def photos_count_within_bounds
    return if photos.blank?
    errors.add("Too many photos") if photos.size > 10
  end
end

class Photo
  belongs_to :album
  validates_associated :album
end
Gourami answered 14/2, 2010 at 23:27 Comment(7)
Thanks for the advice guys. I have got Marcel's code working.Theresa
photos.size is a better way to go - blog.hasmanythrough.com/2008/2/27/count-length-size, from a similar question - https://mcmap.net/q/269704/-validate-the-number-of-has_many-items-in-ruby-on-railsTabethatabib
Amended as you suggested, @TabethatabibGourami
this solution still have a problem, you can add as many children, as you want with album.photos.create, at least with Rails 5.1.4, so it seems @Marcel Jackwerth sulution probably the best from this point of viewOsorio
@Osorio Yes, because it only validates on creation of the parent model. You could validate always by removing _on_create.Gourami
@Gourami I've checked it without _on_create, but with parent.children.create call from console I still can create one more child and exceed the limit by 1 (at least for my polymorphic association)... I suppose the problem here is with the photos.size > 10 - on the moment of validation it is valid and became invalid just after the new photo creation... it could be only rails console issue though, I didn't check if it shows the same behavior then calling create from an application codeOsorio
@Osorio Maybe try photos.length, and maybe change has_many :photos, touch: true so it updates the parent object and reruns the validations defined there. PS: The solution of @Hallmark may be the better one with current Rails versions, it may still need touch.Gourami
S
9

How about adding a custom validation method to the Photo model?

  LIMIT = 50

  validate_on_create do |record|
    record.validate_quota
  end

  def validate_quota
    return unless self.album
    if self.album.photos(:reload).count >= LIMIT
      errors.add(:base, :exceeded_quota)
    end
  end
Starobin answered 14/2, 2010 at 23:26 Comment(2)
thanks @Marcel Jackwerth! spend some time in rails console checking all 3 solutions, found out yours is the only which really preventing from creating "unwanted children" ))Osorio
This solution worked best for my similar scenario. Glad you included .reload in your solution. I initially omitted it, and found some (but not all) scenarios would use an outdated count, allowing excessive child records to be created.Lanni
M
0
ActiveRecord::Base.transaction do
  ActiveRecord::Base.connection.execute('LOCK TABLE pictures IN EXCLUSIVE MODE')
  if (@album.pictures.count < 10) 
    @album.pictures.create()
  end
end

I believe this is the most correct solution. It guards against concurrency issues/race conditions.

Mycobacterium answered 4/4, 2018 at 6:38 Comment(1)
I would add a reload in there to ensure ActiveRecord isn't giving you stale results. Something like @album.pictures.reload.count < 10Lanni

© 2022 - 2024 — McMap. All rights reserved.