2

It seems that alias_method doesn't work the same way inside a class << self block as it does outside. Specifically, when I use alias_method for an instance method and subsequently override the method, the alias name can be used to access the original method. However, I am not able to do this with a class method.

This is my use case: I am overriding the destroy and destroy_all methods that are inherited from ActiveRecord base because the code as currently written makes it very easy to accidentally delete rows when you meant to delete join records. That design could change with a larger project, but for now this is my solution as for the most part, no one will ever want to be deleting rows from this table. But in order to still allow intentional deletion of rows in the table for the well-intentioned, I am using alias_method in order to preserve a handle to the original destroy and destroy_all methods. Using the alias is working to access the original :destroy (instance) method, but is not allowing me to access the original :desotry_all (class) method

class HoldReason < ActiveRecord::Base
  class << self
    alias_method :remove_hold_reasons_from_table, :destroy_all

    def destroy_all
      raise "Nope"
    end
  end

  alias_method :remove_hold_reason, :destroy

  def destroy
    raise "Nope"
  end
end

Here we see that while this strategy does work for the instance method to allow successful deletion of a single row:

> HoldReason.find_by(title: "Management Review").remove_hold_reason
  HoldReason Load (0.7ms)  SELECT  `hold_reasons`.* FROM `hold_reasons` WHERE `hold_reasons`.`title` = 'Management Review' LIMIT 1
   (0.5ms)  BEGIN
  SQL (4.6ms)  DELETE FROM `hold_reasons` WHERE `hold_reasons`.`id` = 23
   (0.6ms)  COMMIT
=> #<HoldReason id: 23, title: "Management Review", category: "Operations">

I am not able to access the original :destroy_all method to delete multiple rows in one query; instead, I get the overridden method even when I use the alias:

> HoldReason.remove_hold_reasons_from_table
  HoldReason Load (0.7ms)  SELECT `hold_reasons`.* FROM `hold_reasons`
RuntimeError: Nope

What is going on that I can't seem to do this for the class methods, and how can I fix it (other than just use a raw SQL query to do the delete)?

Andrew Schwartz
  • 4,440
  • 3
  • 25
  • 58
  • Your question is unclear. The code snippet you posted clearly shows that `alias_method` *does* work: otherwise you'd get a `NoMethodError`. – Jörg W Mittag Mar 11 '16 at 08:55
  • I have edited the question. The intention is that using the alias name gives me access to the original method before it was overridden to raise an error. This is what is working for instance methods but not class methods; in the class method case, the alias name still uses the overridden method, while in the instance method case I get access to the original, non-overridden method when I use the alias. – Andrew Schwartz Mar 11 '16 at 12:54
  • Sorry, I still don't understand. In both cases, you are able to call the method under its alias name. So, `alias_method` is working in both cases. There is no difference between the two cases. There is exactly *one* implementation of `alias_method` in the `Module` class, and it works exactly the same for *all* modules, be they *actual* modules, classes, or singleton classes. `alias_method` does one thing and one thing only: copy the method under a new name. Period. And that is working, as is evidenced by the fact that you don't get a `NoMethodError`. – Jörg W Mittag Mar 11 '16 at 13:04
  • Also, just as a general rule: if you think that a widely-used method used by hundreds of thousands of Ruby programmers every single day is broken, and noone ever noticed except you … you're very likely wrong. – Jörg W Mittag Mar 11 '16 at 13:05
  • Ok I'd appreciate a little more benefit of the doubt here. I'm not saying this critical method is broken, that would indeed be foolish of me. I'm saying that *in this specific case* because of the way that *other specific code* is set up, deleting rows from the hold_reasons table happens when people mean to delete rows from a join table. > model.hold_reasons.destroy_all. – Andrew Schwartz Mar 11 '16 at 13:20
  • Of course I get that alias_method is doing something in both cases for exactly the reason you say. The difference is that in the :destroy case, the : remove_hold_reason alias is giving me the original :destroy method that *does not raise an error*, while in the :destroy_all case, the :remove_hold_reasons_from_table method *is raising an error*. Yes, both methods are defined, but they are not referring to the original methods in the same way -- do you see the difference I am talking about? – Andrew Schwartz Mar 11 '16 at 13:22
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/106013/discussion-between-andrew-schwartz-and-jorg-w-mittag). – Andrew Schwartz Mar 11 '16 at 13:31

1 Answers1

0

The answer is that I was not getting the new :destroy_all method; I was correctly getting the old :destroy_all method, and this called :destroy under the hood. Of course, by calling :destroy, it got the new :destroy method, which raised the error. So it only seemed like I was getting the new class method because the new class method and the new instance method raise exactly the same error.

This is made clearer if I modify this slightly:

class HoldReason < ActiveRecord::Base
  class << self
    alias_method :remove_hold_reasons_from_table, :destroy_all

    def destroy_all
      raise "Don't destroy_all (multiple: class method)"
    end
  end

  alias_method :remove_hold_reason, :destroy

  def destroy
    raise "Don't destroy (single: instance method)"
  end

end

results in

> HoldReason.remove_hold_reasons_from_table
RuntimeError: Don't destroy (single: instance method)
Andrew Schwartz
  • 4,440
  • 3
  • 25
  • 58