Are there alias/helper functions for objects stored in a polymorphic association?

Viewed 93

I have the following class:

class CartItem < ApplicationRecord
  belongs_to :inventory_item, polymorphic: true
end

Where an inventory item can be, say, a book or a pen.

Before, you could buy only pens, and the class looked like this:

class CartItem < ApplicationRecord
  belongs_to :pen
end

As such, there are lots of references to @cart_item.pen in the codebase. Is there any automatic way in Rails to provide alias methods in the CartItem model to access a pen if a pen is stored in the inventory_item columns? I assumed I would have to write these #pen and #pen= methods by hand, but when I suggested I do that the team lead said:

so you shouldn't have to add either the setter or getter method if you set up the polymorphic relationship correctly

since a cart_item would still belong to a pen (polymorphically) both of those methods would exist for you

If he's right, what should I do?

1 Answers

The simple answer is that your team leader is dead wrong and your job is to explain that diplomatically. The polymorphic: true option does not alter the methods generated by the belongs_to macro. belongs_to is evaluated at class eval and it does not know what types your polymorphic association associates to since it does not know or care about the rows in your database. Neither does one association in one class alter another class.

If your team leaders was correct (which is very easy to refute) it would actually be mentioned in the docs (as well as being the source many of interesting code reloading bugs).

class Pen < ApplicationRecord
  has_many :cart_items, as: :inventory_item
end
class CartItem < ApplicationRecord
  belongs_to :inventory_item, polymorphic: true
end
require 'test_helper'
class CartItemTest < ActiveSupport::TestCase
  test "Does your team leader know what he is talking about?" do
    cart_item = CartItem.create(inventory_item: Pen.create!)
    refute(cart_item.respond_to?(:pen), "Nope.")
  end
end

While it certainly is possible if you really wanted to to modify the class at runtime to add getters/setters based on the rows loaded that sounds like a pile of bugs waiting to happen and a better solution is just an alias that emits a depreciation warning:

class CartItem < ApplicationRecord
  belongs_to :inventory_item, polymorphic: true

  def pen
    ActiveSupport::Deprecation.warn("#{__method__} is deprechiated. Use inventory_item")
    inventory_item
  end

  def pen=(pen)
    ActiveSupport::Deprecation.warn("#{__method__} is deprechiated. Use inventory_item=")
    self.inventory_item = pen
  end
end

This will allow your legacy code to keep working until you have fixed it.

Related