Skip to Content
Course content

83: Active Record Validations and Callbacks

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was working on a SaaS project where we let admins create custom subscription tiers. One afternoon, I noticed our revenue for the day was... negative. It turned out a bug in the admin panel allowed a "-50.00" value to be submitted for the monthly price. Because we had no server-side validations on the SubscriptionPlan model, Active Record just happily saved that negative number into the database. For three hours, we were literally paying people to use our software. That's the hard way of learning that client-side validation is a courtesy, but server-side validation is a necessity.

Keeping the Garbage Out of Your Database

Validations are your last line of defense. They ensure that only "clean" data actually hits your disk. In Active Record, validations happen before the data is saved to the database. If any validation fails, the record is marked as invalid, and the save method returns false without ever executing the SQL INSERT or UPDATE.

You'll mostly use the validates helper. It's a Swiss Army knife that handles the most common scenarios. For example, if we were fixing that subscription bug, we'd do this:

class SubscriptionPlan < ApplicationRecord
  validates :name, presence: true, uniqueness: true
  validates :price, numericality: { greater_than_or_equal_to: 0 }
  validates :sku, length: { is: 8 }
end

In this snippet, presence: true ensures the name isn't nil or an empty string. uniqueness: true checks the database to make sure no other plan has that name. I personally find numericality to be one of the most important validations—it prevents the "negative price" nightmare I mentioned earlier. If you need something more complex, you can write a custom validation method. Just define a method and use validate :method_name (singular) to call it.

Hooking into the Object Lifecycle

While validations check if data is correct, callbacks allow you to trigger logic at specific moments in an object's life. Think of them as "event listeners" for your model. You might want to hash a password before saving a user, or send a welcome email immediately after a record is created.

Active Record provides a variety of hooks. The most common ones are before_validation, before_save, after_create, and after_commit. Here is how you might use them to keep your data consistent:

class User < ApplicationRecord
  before_validation :normalize_email
  after_create :send_welcome_email

  private

  def normalize_email
    self.email = email.downcase.strip if email.present?
  end

  def send_welcome_email
    UserMailer.welcome(self).deliver_later
  end
end

Notice that I used before_validation for the email. This is a pro tip: if you're modifying a field that needs to be validated, do it before the validation step. If you normalize the email before_save, you might be validating a string with trailing spaces and then saving a trimmed version, which can lead to weird edge cases.

The Danger of Over-Automating

I have to give you a warning here: callbacks are a double-edged sword. It's tempting to put all your business logic in after_save or after_create, but this is a fast track to "Callback Hell." When your models become bloated with callbacks, testing becomes a nightmare because simply creating a test object triggers a chain reaction of emails, API calls, and database updates.

A good rule of thumb is that callbacks should be used for internal state consistency—like calculating a total price based on line items. If the action involves an external service (like sending an email or hitting a Stripe API), consider moving that logic into a Service Object. Keep your models lean; your future self will thank you when you're trying to run a migration or a seed script without accidentally emailing 5,000 users.




📋 Practical Task

Exercise: Implementing a User Profile Sanitizer and Credit Balance Guard

You are building a credit-based system for a freelance marketplace. You need to ensure that user data is clean and that their credit balance never drops below zero.

Requirements:

  • Create or modify a User model.
  • Validations:
    • The username must be present, unique, and at least 3 characters long.
    • The credit_balance must be a number and cannot be less than 0.
  • Callbacks:
    • Implement a before_validation callback that removes any leading or trailing whitespace from the username.
    • Implement an after_create callback that logs a message to the console saying "New user created: [username]".

Testing your work: Try to save a user with a negative balance or a duplicate username. Verify that user.save returns false and that the user.errors object contains the expected messages. Then, save a user with a username like `" ruby_dev "` and verify that it is saved as `"ruby_dev"` in the database.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.