0

I have the following models:

class Course < ApplicationRecord
  belongs_to :super_course, inverse_of: :courses
  ...
end

class SuperCourse < ApplicationRecord
  has_many :courses, dependent: :nullify, inverse_of: :super_course
  ...
end

This two models relate each other as the following: a SuperCourse groups plenty of Courses that have some particular conditions. The thing is that, in a backoffice I can change the SuperCourse of a Course, and I don't want to have empty SuperCourses (i.e. that don't have any Course associated).

What I've been trying to do is adding an after_update callback to the Course model, so that it checks if the previous SuperCourse now doesn't have any Course associated, but I don't know if this is the best solution (AFAIK, callbacks are not quite recommended). Following the response in this very old thread, this is what I get right now:

class Course < ApplicationRecord
  belongs_to :super_course, inverse_of: :courses

  after_update :destroy_empty_super_course

  ...

  private

  def destroy_empty_super_course
    id = super_course_id_changed? ? super_course_id_was : super_course_id
    super_course = SuperCourse.find(id)
    super_course.destroy if super_course.courses.empty?
  end
end

But when I test this, I don't even get what I want. This is the rspec snippet that fails:

context "when super_course is updated" do
  let(:super_course) { create(:super_course, courses: []) }
  let(:course) { create(:course, super_course: super_course) }
  let(:new_super_course) { create(:super_course, courses: []) }
  let(:new_course) { create(:course, semester: course.semester, subject: course.subject, super_course: new_super_course) }

  subject { course.update!(super_course: new_super_course) }

  it "should remove old super_course" do
    expect { subject }.to change(SuperCourse, :count).by(-1)
  end
end

Is this my best choice? If so, how can I make it work? If no, what is the best option?

Thank you all in advance!

2 Answers2

0

I've been working a bit more on it, and as I'm using Rails 5.1 (thanks to https://stackoverflow.com/a/20657833/7023504), I got to this working code for the callback:

def destroy_empty_super_course
  id = saved_changes[:super_course_id]&.first
  return unless id
  super_course = SuperCourse.find(id)
  super_course.destroy if super_course.courses.empty?
end

And after changing the let for let!, the test started passing! Anyway, I'd still like to know if this is the best solution for this case, but at least I have something working :)

0

Callback is the only viable solution for this one, although, to be more elegant and consistent with Rails style it is common to use around callback for that purposes. The reason why your version failed was because after_update callback takes place after the update was committed, so *_changed? and *_was methods are not aware of the dirty changes that were on the record before the save. To get the data in the period before the save is done around callback can be used - it allows to perform save yielding the block, making dirty changes available before the yield statement practically being after save and after update logic in the statements after yield.

def destroy_empty_super_course
  return unless super_course_id_changed?

  id = super_course_id_was

  yield

  super_course = SuperCourse.find(id)
  super_course.destroy if super_course.courses.empty?
end