Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails validating that a Has Many relationship has at least 1 entry

I have an event model. Each event can have multiple sessions.

I want to ensure that no model can exist without it having at least 1 session associated with it.

  validates :sessions, :length => { :minimum => 1 }

The problem is - when I go to try to create sessions on a particular event by calling my model method:

create_sessions()

Which does something like :

sessions.create(event_id: id,date: x,day_of_the_week:x.strftime("%A"),classPin: pin)

for each of the dates the event will run.

It fails to save with the error:

ActiveRecord::RecordNotSaved in EventsController#create
You cannot call create unless the parent is saved

Of course - by this point the new event record has not yet been saved - so this association cannot yet be created due to create on an association not available until the parent is saved!

Therefore how does any validation between this kind of relationship work - because the validation occurs at save time....but I want to validate the count of sessions will be greater than 0 before we save the event!

like image 470
RenegadeAndy Avatar asked Aug 02 '15 22:08

RenegadeAndy


2 Answers

I think a simpler way would be to create your events and sessions with new instead of create it will only create the objects but not store them to the database. And after you have created your sessions do a save on your event.

@event = event.new(event_args)
@event.sessions.new(session_args)
@event.save

Addition
The @event.save will store the @event object and all associated session objects. Without further configuration only new objects will be safed see Active Record Autosave Association for more information.

like image 167
Johannes Thorn Avatar answered Oct 16 '22 17:10

Johannes Thorn


The main point is that your validation conflicts with the way ActiveRecord works. You are creating a paradox.

There are a number of solutions, but you must relax the validation. That is, it should not run when the record is new.

You can either define it like this:

class Event < ActiveRecord::Base
  validates :sessions, length: { minimum: 1 },
                       unless: :new_record?
end

Or, to have even more flexibility, you can use a custom validation:

class Event < ActiveRecord::Base
  validate :session_count_validation

  private

  def session_count_validation
    if !new_record? && sessions.count < 1
      errors.add(:base, "Not enough sessions!")
    end
  end
end

This second style also has the advantage of being more performant: sessions.count will simply count how many associated Session records exist, while your original validation will load all of them in memory, and check the length of the relation/array.

Then, to ensure that your creation logic is sound, you should use a transaction:

begin
  ActiveRecord::Base.transaction do
    event = Event.create!(args)

    sessions = dates.map do |date|
      event.sessions.build(event_id: id,
                               date: date,
                    day_of_the_week: date.strftime("%A"),
                           classPin: pin)
    end

    sessions.each(&:save!)
  end
rescue
  # your rescue logic
  # e.g. display an error to the User
end

So that everything will be safely rolled back if any save! operation fails.

like image 26
tompave Avatar answered Oct 16 '22 16:10

tompave