Rails -- self vs. @
Asked Answered
S

3

17

I am following Michael Hartl's RoR tutorial, and it is covering the basics of password encryption. This is the User model as it currently stands:

class User < ActiveRecord::Base
    attr_accessor :password

    attr_accessible :name, :email,: password, :password_confirmation

    email_regex = /^[A-Za-z0-9._+-]+@[A-Za-z0-9._-]+\.[A-Za-z0-9._-]+[A-Za-z]$/
                                              #tests for valid email addresses.

    validates :name, :presence => true,
                     :length => {:maximum => 50}
    validates :email, :presence => true,
                      :format => {:with => email_regex},
                      :uniqueness => {:case_sensitive => false}
    validates :password, :presence => true,
                         :length => {:maximum => 20, :minimum => 6},
                         :confirmation => true

    before_save :encrypt_password

    private

    def encrypt_password
        self.encrypted_password = encrypt(password)
    end

    def encrypt(string)
        string
    end
end

I posted a previous question about before_save not working, and it turns out that what I had accidentally done is written my encrypt_password as:

def encrypt_password
    @encrypted_password = encrypt(password)
end

I understand that if self.encrypted_password sets the encrypted_password attribute, but why does @encrypted_password not do that as well? In the response to the previous post about before_save not working someone said that the instance variable was "forgotten" after the method ended with the way I had originally coded it -- why was this the case? Can someone please explain how self and @ work differently in the context of the code above?

NOTE: I already took a look at the posts here and here, but they both say that "self" is calling the attribute = method, and I don't even understand how that method could exist here since I never created it or declared the encrypted_password w/ attr_accessor. So I am still confused, and this is not a re-posting of those questions.

Spinneret answered 13/6, 2011 at 6:12 Comment(1)
Related: Ruby: self vs @Carbonaceous
S
20

The accessors for encrypted_password have been automatically added by Rails for you because a field by that name exists in the users table.

Any field you add to a table will be automatically made available via self.field_name.

Here is where Michael Hartl's tutorial creates the encrypted_password field in the users table.

Also look at the user_spec.rb (Listing 7.3) in the linked page, where the author is testing for the presence of the encrypted_password field.

UPDATED:

As @mu points out, the @ is used for Ruby instance variables (aka "iv"). But encrypted_password is an "attribute" defined by Rails, and is not an instance variable.

If you run User.find(1).instance_variables, you will see that there is an iv called @attributes, which is of type Hash.

Inside that iv is where the encrypted_password is stored. Rails has defined accessor methods for encrypted_password, which gets/sets the data for that attribute in the @attributes Hash.

Note that you could also get/set the data via @attributes["encrypted_password"] called from within the User class (but the accessor methods are convenient way to do just that).

Satyr answered 13/6, 2011 at 6:32 Comment(3)
So that explains why "self" does work, but why doesn't @ do the trick?Spinneret
@Kvass: @ doesn't work because ActiveRecord doesn't pay attention to your instance variables, the method call works because AR added that as a way to change the encrypted_password property. AR's properties are separate from your object's instance variables.Vulcan
@Spinneret Think of it as: def encrypted_password= (value) someinternalmagic end; the "active" nature requires a setter (method).Nightfall
M
2

If you let me, I'd like to rephrase the answer.

I explained in this post, that as soon as you create a (rails-) Model with the same (singular) name as one of the (plural) tablenames of your database, the "magic" of rails will create setters and getters in order to modify your table's records.

This is because your model inherits all methods from the ActiveRecord::Base Class, which defines basic CRUD accessors (Create, Read, Update, Delete).

The key point related to your question, is that you don't know how rails implements the instance variable related to your database table column, And you shouldn't. :) All you have to know is that at that point, you have setters and getters available to CRUD (create, read, update, delete) your database column "encrypted_password".

In your example, maybe rails uses an instance variable called @encrypted_password, maybe rails uses an hash-instance-variable called @attributes["encrypted_password"], or maybe rails uses an instance variable called @you_will_never_guess_encrypted_password.

-

And that's a good point you don't know about the internal rails behavior with instance variables. In 2019 Rails further development may lead the framework to use @complicated-hash-instance-variable to store the encrypted_password value.

In fact the best approach is to let rails manage its "private" "affair" ;) with instance variables, and just use the getter and setter methods it provides to you. So your application will still work with encrypted_password in the next century (I hope so ^^).

So if you use @encrypted_password it may work with some "imaginary" version of rails and it won't work anymore with other rails versions. Actually with a current version of rails it doesn't work.

-

The second key point is that when you want to use the getter "encrypted_password" Rails created for your encrypted_password database table column, you prefix it with "self" in order to tells Ruby : "ok I want to use the encrypted_password method of my User instance variable."

In Ruby, a method is called by passing its name to a receiver. You write it like this :

my_receiver.my_method

In your case we pass the method encrypted_password to the User instance variable. But we don't know how this instance variable will be named, so we use the word self to tell Ruby : "I'm talking about any instance variable of the User class that calls the encrypted_password method".

For instance we could have named our instance variable "toto" :

toto = User.new

so toto.encrypted_password would display the encrypted password, and self in this very case in our code would reference toto.

However, thanks to Ruby, if you don't give any receiver when calling a method, Ruby will assume you pass it to self.

Reference : Pragmatic Programmer's guide

So in your example, you even don't need to put "self." as prefix. You could have it written like this :

class User < ActiveRecord::Base

    def encrypt_password
        encrypted_password = encrypt(password)
    end
end

I hope this helps to clarify this interesting subject.

Monosymmetric answered 19/2, 2013 at 9:37 Comment(3)
In your last example, you need to use self.encrypted_password = ..., otherwise it becomes a local variable.Lichenology
No because before to create a local variable, ruby check if a method using this very name exist in the current object (by default Self), and then in the parent objects hierarchy. [Reference][1] [1]:ruby-doc.org/docs/ProgrammingRuby/html/classes.htmlMonosymmetric
Agreed with @Monosymmetric . My current experience (Rails 3.2) is that "self." is required, for all the reasons discussed here. Coding "encrypted_password = 'foobar' " creates (sets) a local instance variable named encrypted_password. A later call to save() will NOT see this as a change, so no updates will be sent to the database.Gripper
G
2

TL;DR -

Always write self.widget_count = 123 if you intend to save widget_count back to the database.

(But please do read the long answers, as the reason why is valuable to know.)

Gripper answered 24/2, 2017 at 19:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.