Rails in Depth

Active Record

Review

Review

The past two weeks, we have created a simplified Go Food web application using Ruby on Rails. Let's review it for a minute or two.

Tell me about:

  1. TDD
  2. Model and Migration
  3. Controller and Routes
  4. View and Helper

Active Record

Can You Guess...

From your experience in the past two weeks, can you guess:

  1. What is Active Record?
  2. What does it do?

What is Active Record?

Active Record is the object-relational mapping (ORM) layer of Rails. It is the M (model) in MVC that is responsible for representing business data and logic. Active Record facilitates the creation and use of business objects whose data requires persistent storage to a database.

 

Active Record as an ORM Framework

Active Record provides us a lot of features. These are some of its most important ability:

  1. Represent models and their data
  2. Represent associations between these models
  3. Represent inheritance hierarchies through related models
  4. Validate models before they get persisted to the database
  5. Perform database operations in an object-oriented fashion

 

Active Record Conventions (1)

By default, Active Record uses some naming conventions to find out how the mapping between models and database tables should be created.

  1. Model classes are named with singular noun in CamelCase. Example: Food, Cart, LineItem, and User.
  2. Database tables are named with plural noun in snake_case. Example: foods, carts, line_items, and users.
  3. Primary keys are, by default, named "id" with integer data type.
  4. Foreign keys are named with singularized table name followed by id in snake_case format. Example: food_id, cart_id, line_item_id, and user_id.

Active Record Conventions (2)

There are also some optional column names that will add additional features to Active Record instances.

  1. created_at - automatically gets set to the current date and time when the record is first created.
  2. updated_at - automatically gets set to the current date and time whenever the record is updated.
  3. lock_version - adds optimistic locking to a model.
  4. type - specifies that the model uses single table inheritance.
  5. (association_name)_type - stores the type for polymorphic associations.
  6. (table_name)_count - used to cache the number of belonging objects on associations.

Overriding Naming Conventions

If for one reason or another we need to break out of Rails naming conventions, we can use:

class Food < ApplicationRecord
  self.table_name = "my_foods"
  self.primary_key = "food_id"
end

CRUD with Active Record

Create (1)

Given your current Go Food app, try to:

  • Instantiate an object of Food class with a valid name, description, price, and image_url.
  • Create an object of Food class with a valid name, description, price, and image_url.

Create (2)

Instantiating new object with Active Record:

food = Food.new
food.name = 'Nasi Uduk'
food.description = 'Deskripsi'
food.price = 10000.0
food.image_url = 'Nasi Uduk.png'

# or

food = Food.new(
  name: 'Nasi Uduk',
  description: 'Deskripsi',
  price: 10000.0,
  image_url: 'Nasi Uduk.png'
)

# to persist it, use:

food.save
# we can also pass parameters as a block

Food.new do |f|
  f.name = 'Nasi Uduk',
  f.description = 'Deskripsi',
  f.price = 10000.0,
  f.image_url = 'Nasi Uduk.png'
  # if you wish to immediately save it,
  # f.save
end

# using block is particularly useful
# if you want to create & save an object
# without creating a local variable

Create (3)

Creating an object with Active Record:

# this will automatically persist the object to the database

food = Food.create(
  name: 'Nasi Uduk',
  description: 'Deskripsi',
  price: 10000.0,
  image_url: 'Nasi Uduk.png'
)

To create multiple rows, we can pass an array of attribute hashes as an argument to "create".

food = Food.create([
  {
    name: 'Nasi Uduk', description: 'Deskripsi', price: 10000.0, image_url: 'Nasi Uduk.png'
  },
  {
    name: 'Kerak Telor', description: 'Deskripsi', price: 10000.0, image_url: 'Kerak Telor.png'
  },
])

Save! and Create! (1)

Previously we used "save" and "create" to persist object to database. There are actually two more methods that we can use, "save!" and "create!". Here are the differences:

  • "save" returns true if the record was successfully persisted to the database and returns false otherwise.
  • "save!" returns true on success, but it raises exception instead of returning false otherwise.
  • "create" returns the Active Record object regardless of whether it was successfully persisted to the database or not.
  • "create!" returns the Active Record object on success, but it raises exception otherwise.

Save! and Create! (2)

Therefore, you need to handle them differently from "save" and "create".

if food.save
  # if persisted
else
  # if not persisted
end
begin
  food.save!
rescue RecordInvalid => error
  # if not persisted
end

Read (1)

Rails provides a lot of APIs to read data from database. We have used some of them. Can you guess what methods to use in these lines?

# returns all rows in foods table
foods = Food.your_method here

# returns the first row in foods table
food = Food.your_method here

# returns the first food named 'Nasi Uduk'
foods = Food.your_method here

# returns all foods with price 10000.0
foods = Food.your_method here

Read (2)

Here are the answers:

# returns all rows in foods table
foods = Food.all

# returns the first row in foods table
food = Food.first

# returns the first food named 'Nasi Uduk'
food = Food.find_by(name: 'Nasi Uduk')

# returns all foods with price 10000.0
foods = Food.where(price: 10000.0)

We are going to discuss more about this later in Active Record Query Interface topic.

Update (1)

To update an Active Record object, first we need to retrieve it using one of read APIs we learned just now. Then, we can set a new value and save it to the database.

food = Food.find_by(name: 'Nasi Uduk')
food.name = 'Kerak Telor'
food.save

# or

food = Food.find_by(name: 'Nasi Uduk')
food.update(name: 'Kerak Telor')

Update (2)

We can also do mass update by retrieving a collection of Active Record objects and then apply "update_all" method to them.

foods = Food.all
foods.update_all(price: 15000.0)

Delete

Similarly, to delete objects from database, we need to retrieve it first and only then we can delete it.

food = Food.first
food.destroy

We can also delete multiple objects using "delete_all" to a collection of Active Record objects.

foods = Food.where(price: 15000.0)
foods.delete_all
# PS: don't ever do this on production server, though

Validations

Why Validations?

We have briefly discussed about validations in "Validation and Unit Testing" topic. As we have learned from our brief experience with Rails in this class so far, with Active Record we can't persist any object that does not satisfies validations we put in our models.

That is the importance of Active Record validations. Once set in place, it ensures that only valid data are stored to the database.

When does Validation Happen?

The following methods trigger validations, and will save the object to the database only if the object is valid:

  • create
  • create!
  • save
  • save!
  • update
  • update!

Skipping Validations

The following methods skip validations, and therefore should be used with caution:

  • decrement! and decrement_counter
  • increment! and increment_counter
  • toggle
  • touch
  • update_all and update_attribute
  • update_column and update_columns
  • update_counters

Valid? and Invalid?

While validations are automatically run when we are about to persist data to the database, we can trigger it manually by invoking method "valid?" and "invalid?". We have used this a lot in our unit testing for models.

# remember this?
food = Food.new(name: nil)
food.valid? # return false
food.invalid? # return true

Errors

After ActiveRecord has performed validations, we can find out what errors found by using "erros" method.

food = Food.new(name: nil)
food.valid?

food.errors.messages
# will return {:name=>["can't be blank"], :description=>["can't be blank"], :price=>["is not a number"]}

food.errors[:name].any?
# will return true

food.errors.details[:name]
# will return [{:error=>:blank}]

Validation Helpers (1)

Rails provides many pre-defined validation helpers that we can use. Based on your experience with Rails so far, can you name some of them?

Validation Helpers (2)

Some validations that we have used so far:

  • presence
  • uniqueness
  • format 
  • length
  • numericality

 

Some new validations that we will learn:

  • acceptance
  • validates_associated
  • confirmation
  • exclusion
  • inclusion
  • absence
  • validates_with
  • validates_each

 

Presence

This helper validates that the specified attributes are not empty. Under the hood, it uses "blank?" method against specified attributes.

# This is a standard presence validation helper
class Food < ApplicationRecord
  validates :name, :price, :image_url, presence: true
end

# We can also validate presence from associated model
# this will actually check if the Order object is present,
# not just the order_id in LineItem object is present
class LineItem < ApplicationRecord
  belongs_to :order
  validates :order, presence: true
end

# This works just as the previous example,
# but from the opposite associated model
class Order < ApplicationRecord
  has_many :line_items, inverse_of: :order
end

# Since "false.blank?" returns true,
# if you want to validate presence of boolean attribute,
# use one of these:
validates :boolean_field_name, inclusion: { in: [true, false] }
validates :boolean_field_name, exclusion: { in: [nil] }

Uniqueness

This helper validates that the attribute's value is unique right before the object gets saved. However, it does not set uniqueness constraint in the database.

# This is a standard uniqueness validation helper
class User < ApplicationRecord
  validates :username, uniqueness: true
end

# We can specify the scope of uniqueness of an attribute
class Food < ApplicationRecord
  belongs_to :restaurant
  validates :name, uniqueness: { scope: :restaurant_id }
end

Format

This helper validates the attributes' values by testing whether they match a given regular expression that is specified using ":with" option.

class Order < ApplicationRecord
  validates :email, format: {
    with: /.+@.+\..+/i,
    message: 'must be in valid email format'
  }
end

Length

This helper validates the length of the attributes' values.

class User < ApplicationRecord
  validates :password, length: { minimum: 8 }
  # but you can also use some variations like:
  # validates :password, length: { maximum: 20 }
  # validates :password, length: { is: 12 }
  # validates :password, length: { in: 8..20 }
end

Numericality

This helper validates that your attributes have only numeric values. We can also add ":only_integer" option with this helper.

class Food < ApplicationRecord
  validates :price, numericality: { greater_than_or_equal_to: 0.01 }
end

class LineItem < ApplicationRecord
  validates :quantity, numericality: { only_integer: true }
end

Some other options we can use:

  • :greater_than and :greater_than_or_equal_to
  • :equal_to
  • :less_than and :less_than_or_equal_to
  • :other_than
  • :odd and :even

Acceptance

This helper validates that a checkbox on the user interface was checked when a form is submitted. This is typically used for requiring user to agree to app's terms of service.

class Person < ApplicationRecord
  validates :terms_of_service, acceptance: true
end

Validates Associated

We use this validation when our model has associations with other models and we want them to be validated too.

# This is a standard presence validation helper
class Order < ApplicationRecord
  has_many :line_items
  validates_associated :line_items
end

Confirmation

We use this validation when we have two text fields that should receive exactly the same content. This validation creates a virtual attribute whose name is the name of the field that has to be confirmed with "_confirmation" appended.

# This is a standard presence validation helper
class Order < ApplicationRecord
  validates :email, confirmation: true
end

Exclusion

This helper validates that the attributes' values are not included in a given set of enumerable object.

class Account < ApplicationRecord
  validates :subdomain, exclusion: { in: %w(www us ca jp),
    message: "%{value} is reserved." }
end

Inclusion

This helper validates that the attributes' values are included in a given set of enumerable object.

class Coffee < ApplicationRecord
  validates :size, inclusion: { in: %w(small medium large) }
end

Absence

This helper validates that the specified attributes are absent.

class Person < ApplicationRecord
  validates :name, :login, :email, absence: true
end

# We can also use this validation
# to validate the absence of associated model
class LineItem < ApplicationRecord
  belongs_to :order
  validates :order, absence: true
end

# To validate associated records whose absence is required
# we can use ":inverse_of"
class Order < ApplicationRecord
  has_many :line_items, inverse_of: :order
end

Validates With

Even with so many built in validations available, sometimes we want to build our own custom validation. For this, we need to create a custom validator class and call it using "validates_with".

class DiscountValidator < ActiveModel::Validator
  def validate(record)
    record.total_price > record.discount
  end
end

class Order < ApplicationRecord
  validates_with DiscountValidator
end

Validates Each

This helper validates attributes against a block. It does not have a predefined validation function, we should create one using a block.

class Person < ApplicationRecord
  validates_each :name, :surname do |record, attr, value|
    record.errors.add(attr, 'must start with upper case') if value =~ /\A[[:lower:]]/
  end
end

Callbacks

Can You Guess...

We actually have used callbacks in our app before. Can you tell me what is a callback?

What are Callbacks?

Callbacks are methods that get called at certain moments of an object's life cycle.

Object Life Cycle

This picture shows you the life cycle of an object in Rails and available callbacks for each state.

We Have Used Callbacks

class Food < ApplicationRecord
  # -cut-
  before_destroy :ensure_not_referenced_by_any_line_item
  
  # -cut-
  private
    def ensure_not_referenced_by_any_line_item
      unless line_items.empty?
        errors.add(:base, 'Line Items present')
        throw :abort
      end
    end
end

Exercise (1)

It's your product manager again! Your app has gained a traction and he now thinks about making it big. He gets approval to give vouchers to your app users.

Exercise (2)

Here are the requirements:

  • Make a feature that enables administrators to manage (create, read, update, and delete) vouchers.
  • A voucher a code, with the format of all capital string. However, administrators sometimes forget to input with all capital letters, so you have to make sure that whatever input it gets, it will be transformed to all capital letters format.
  • A voucher has a "valid from" attribute, indicating the beginning of valid period for the voucher.
  • A voucher has a "valid through" attribute, indicating the end of valid period for the voucher.

Exercise (3)

Here are the requirements:

  • Make a feature that enables administrators to manage (create, read, update, and delete) vouchers.
  • A voucher has an "amount" attribute, a positive float value indicating the amount of discount given.
  • A voucher has an "unit" attribute that can only be one of two values: "percent" or "rupiah".
  • A voucher with an amount of "15.0" and unit of "percent" means that an order gets a 15.0 % discount.

Exercise (4)

Here are the requirements:

  • A voucher has a "max amount" attribute, a positive float value indicating the maximum amount of discount an order can get in rupiah. So, if the max amount of a voucher is IDR 10,000, an order with total price of IDR 100,000 can only get IDR 10,000 discount even if the voucher's value is 15.0 %.
  • An order can only have maximum one voucher (so it can also have no voucher). If an order has a voucher, it can only be saved if it has a valid voucher.

Exercise (5)

Based on those requirements:

  1. Create the voucher feature in TDD manners.
  2. Write any necessary changes that you see fit for any existing specs, models, controllers, or views.

Rails in Depth - Active Record

By qblfrb

Rails in Depth - Active Record

  • 311