Rails’ Dirty Modules: Checking models states

Wonderful Monday(?)

*All characters appearing in this article are fictitious

9:00 AM

Me「It’s Monday already. I really don’t feel like working」

Me「Unfortunatelly, it can not be helped. Gotta check Slack…」

Slack「You received a message」

Me「Here we go… Let me see this message…」

Me「It seems I have a User and Profile models, and need to update the Profile before updating the User

Me「Well, let’s dive in…」

The Code

Me「Ok, we’ll start by checking the code to understand what I have to do:」

class User < ApplicationRecord
  has_one :profile, dependent: :destroy  # User has one Profile
  before_update :update_profile             # and I have to implement this callback
  ...
  def update_profile
    # TODO
  end
end

Me「Let’s see the message again, to understand exactly what I should do here:」

Mr. PM「Should check if either email or password will change. If so, we should set a flag
Mr. PM「And btw, I’ll be offline for some beer」

Me「Wow, beer in the morning…」
Me「Anyways, let’s keep going…」

Let’s check these Modules

Me「First of all, I’ll need to search for something to help me with this feature」

3 hours later

Me「Humm, these modules ActiveModel::Dirty and ActiveRecord::AttributeMethods::Dirty are interesting. Let’s play a little:」

user = User.new
=> #<User id: nil, created_at: nil, updated_at: nil, name: nil, email: nil>
user.changed?
=> false                      # Cool, it checks if the object user has changed
user.name = "Harry"  # Let's add a name
=> "Harry"
user.changed?
=> true                       # Good, It shows changed

Me「Interesting… let’s try more of these methods」

Aggressively Reading ActiveModel::Dirty Docs

user = User.new
=> #<User id: nil, created_at: nil, updated_at: nil, name: nil, email: nil>
user.changed?
=> false
user.name = "Harry"
=> "Harry"
user.changed?
=> true
user.changed               # Nice, it shows the attributes with changes
=> ["name"]
user.name_changed?  # This checks if the attribute has been changed
=> true
user.email_changed?  # email has not changed to it should be false:
=> false
user.name_change      # This shows an array with the changes for the attribute:
=> [nil, "Harry"]
user.name = "Steven"
=> "Steven"
user.name_change      # Wait, shouldn't it be ["Harry", "Steven"]?
=> [nil, "Steven"]
user.name_was           # Humm, and wasn't this supposed to be "Harry"??Weird...
=> nil
user.save!
=> true
user.name = "John"
=> "John"
user.name_change      # Ohh, I think I got it! I had to save the object:
=> ["Steven", "John"]
user.name_was           # Now it makes sense:
=> "Steven"

Me「So, recapping what I learned:」

changed? => Check if the object has changed
changes => See all the changes to the model’s attributes
[attr_name]_changed? => Check if the attribute has been changed
attribute_was => Attribute before the change

Me「It’s a little complicated…」

Me「Those methods seem to be very reliant on data from the database」

Me「There are many other methods to try, but these might be just what I need…」

Implementing the feature

Me「Finally…」

Me「Time to get the work done!」

Me「Let’s see the code again:」

class User < ApplicationRecord
  has_one :profile, dependent: :destroy
  before_update :update_profile
  ...
  def update_profile
    # TODO
  end
end

Me「So, What I need to do is to set email and password flags to the Profile model」

Me「”What are these flags going to do inside the Profile?”, you may ask… I have no idea, hehe」

Me「It looks like I can use [attr_name]_changed? to send a boolean flag.」

Me「However, ActiveRecord::AttributeMethods::Dirty seems to offer a more idiomatic alternative called: will_save_change_to_[attr_name]?

Me「This should make the code more understandable」

class User < ApplicationRecord
  has_one :profile, dependent: :destroy
  before_update :update_profile
  ...
  def update_profile
    attributes = {
      email_flag: will_save_change_to_email?,               # check if the email has changed
      password_flag: will_save_change_to_password?  # check if the password has changed
    }

    profile.update!(attributes)  # update the profile with the flags
  end
end

Me「Yep, I did it!」

Me「Let’s add this change (with some tests, of course) to a new commit and push the changes」

Me「Done! Now, I should go out for some beers, too!」

Afterstory

Me「These modules I discovered are really useful. There are so many other methods that I can use!」

Me「Next time I need to deal with this kind of problem, I have a solution!」

Me「BTW, I can’t forget to send a message to PM saying I finished the feature:」

Me「Hey! I’ve finished the feature」
Mr. PM「Great!」
Mr. PM「And I have news for you」
Me「What news?」
Mr. PM「We won’t need this feature anymore…」

Me「!?」

Reference:

ActiveModel::Dirty – Rails API
ActiveRecord::AttributeMethods::Dirty – Rails API

We are hiring new talent. Check out our careers portal!