2

I've got user and they've got pets. All kinds of pets. Reptiles, fish, birds, and mammals. The Mammal model has two sub-classes, inherited through single table inheritance (STI): cats and dogs. My user has a method to get their last pet.

In a static pages controller, I have a method to visit the user's last pet:

  def go_to_pet
    redirect_to current_user.last_pet || root_path
  end

Now the issue I'm running into is that when a cat or a dog is returned as the user's last pet, the app is looking for the cats or dogs controller, but I only have and need a mammals controller. The mammals controller is handling both cats and dogs pretty fine when it is actually invoked.

My question is such: how can I retrieve an instance of the Mammal class? I've tried Mammal.find(cat.id), but it will return a cat automatically because that's the whole point of STI.

The only ugly workaround I can think of is checking the class in the static pages controller (in the code block above) and then have redirect_to use the mammals controller, but I'd like to rather avoid this.

I tried to explicitly load the base class through Mammal.find(cat.id), but it returned an instance of the inherited class Cat based on the type column automatically. I had hoped to get an instance of the Mammal class.

  • 4
    `current_user.last_pet.becomes(Mammal)` https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-becomes – Alex Jul 11 '23 at 18:32
  • Ooohhh it also works the other way around! Somehow I assumed it only works for changing the base class to the derived class. Thanks a lot! – Matthias Benjamin Schönborn Jul 11 '23 at 19:03
  • if you don't want a `Mammal` instance in the view, you can `redirect_to mammal_url(current_user.last_pet.id)` and pass in a cat as `@pet = current_user.last_pet` which will be an instance of `Cat` – Les Nightingill Jul 11 '23 at 20:57
  • Unfortunately that won't work, because it might return a reptile, fish or bird as well, who all don't have a mammal_url. – Matthias Benjamin Schönborn Jul 12 '23 at 06:29

2 Answers2

1

I think you're trying to shoot yourself in the foot. If you have a Dog and Cat class instead of just a Mammal class then they probably deserve their own controllers. And if they behave the same then just make them trivial subclasses of a parent with shared behavior.

But if you really wanna do what you describe, I'd probably do it something like:

  def go_to_pet
    last_pet = current_user.last_pet
    if last_pet.class.superclass.abstract_class?
      redirect_to url_for(controller: last_pet.class.base_class.name.tableize, id; last_pet.id)
    else
      redirect_to current_user.last_pet || root_path
    end
  end

But mostly I'd probably just head it off in the routes.

  resources :dogs, controller: 'mammals'
kwerle
  • 2,225
  • 22
  • 25
0

Update: After I wrote this answer the now accepted answer was posted, which is assigning the correct controller in routes.rb. This solves my problem in an even better way.


Alex wrote the correct answer in a comment to the question. It is:

becomes(Mammal)

api.rubyonrails.org documentation

I call it in the user model when retrieving the last pet like so:

if @pet.instance_of?(Cat) || @pet.instance_of?(Dog)
  @pet = @pet.becomes(Mammal)
end

So now I can redirect using redirect_to current_user.last_pet || root_path with confidence. Thanks again to Alex.

  • 1
    If you add more `Mammal`s this could get clunky. I am not sure if `Mammal` is abstract or not, but something like `@pet.becomes(@pet.class.base_class)` might work – engineersmnky Jul 12 '23 at 13:37