简体   繁体   中英

Defining custom methods within a model/class in Rails 4

I have the following model:

class ActivityLog < ActiveRecord::Base
  validates :user_id, :instance_id, :action, presence: true
  validates :user_id, :instance_id, :action, numericality: true

  def log
    ActivityLog.create(
      user_id: current_user ? current_user.id : -1,
      instance_id: instance_id,
      action: actions.index(action)
      )
  end

  private 

  def actions
    ['start','stop','create','destroy']
  end

end

When I call the following line from the rails console, I get an error:

ActivityLog.log(user_id: 1, instance_id:1, action: 'create')

# Error returned from console
NoMethodError: undefined method `log' for #<Class:0x007fb4755a26a8>

Why doesn't my method call work? I defined it in the class, but it says it is undefined. What am I missing or misunderstanding? Thank you.

Creating method log

Say you have a class User , and within the class, you define the method has_cell_phone . (The contents of that method does not matter.) When you define a method in a class as def has_cell_phone , that method can be called on any User object. While the class User is itself a class object, you would call it on an object whose immediate class is User . In correct terms, you would be writing an instance method for an instance of the User class.

You are getting that error because the method log you defined works only for an _instance of the ActivityLog class. If you do the following, you can call log correctly, given your current code:

activity_log = ActivityLog.create  # with required params
activity_log.log

Secondly, you are calling log with parameters, while your method definition does not require any. (That would look like def log(params) .)

Now, here is where you modify your existing code. When you want to call a method on the entire class (meaning the class itself), you prepend the keyword self to the class-method definition. For example, for the User class, it would be def self.create_user_with_cell_phone . You can also add arguments to that method. The arguments you provide in your "method call" line, I would add those to your class method, like so:

def self.log(instance_id, action)
  # ...
end

ActivityLog.log(1, 'create')

You would not need to include the user_id , because, based on your logic, it checks if the current_user object is true , and follows from there.

Creating a class constant

A second look at your question, I found that you are defining a method actions . Remember what I said about instance methods? Since it appears that actions will always remain constant, I recommend that you make it one! To do so, it's recommended you place the following line in your class, before any method definitions.

ACTIONS = ['start','stop','create','destroy']

Then, any time you want to call ACTIONS while inside the ActivityLog class, you do the following: ACTIONS.index(action) . If you want to call this constant outside of its class, you would do this: ActivityLog::ACTION . It is similar syntax to a class-method call, instead you use :: to separate the class from the constant. Re-examining your code, it should now look like this:

class ActivityLog < ActiveRecord::Base
  ACTIONS = ['start','stop','create','destroy']

  validates :user_id, :instance_id, :action, presence: true
  validates :user_id, :instance_id, :action, numericality: true

  def self.log(instance_id, action)
    ActivityLog.create(
      user_id: (current_user ? current_user.id : -1),
      instance_id: instance_id,
      action: ACTIONS.index(action)
    )
  end
end

log is an instance method as defined; it will only work if you have a concrete instance of an ActivityLog .

If you want to make that a class method, you should attach it to the class via the self keyword.

def self.log
    # code here
end

You're writing an instance method and calling it like a class method.

To write a class method, you need to add self before your method names ( self.log , self.actions ). This will let you call the method like you expect, and is probably the best way to write an alternate constructor like this. If your methods don't depend on a specific instance of the class, which it seems like you are doing here, then it is better to make them class methods.

Alternatively, you can create an instance of your class and call the instance methods you have defined. Instead of ActivityLog.log , make a new logger with Activity.new and call the log method on it. In one line, this would look like Activity.new.log , but you should probably store the new object in a variable to keep track of it.

One final alternative would be to use the initialize method. By writing def initialize you change the constructor for your class, so that instead of calling ActivityLog.log you can call ActivityLog.new . This makes it clearer that you are constructing a new object and is idiomatic in ruby. However, it does remove the descriptive method name. I would recommend this route if you aren't going to construct your class with any other methods, but if you want to have several, go with the class method.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM