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:
From your experience in the past two weeks, can you guess:
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 provides us a lot of features. These are some of its most important ability:
By default, Active Record uses some naming conventions to find out how the mapping between models and database tables should be created.
There are also some optional column names that will add additional features to Active Record instances.
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
Given your current Go Food app, try to:
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
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'
},
])
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:
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
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
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.
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')
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)
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
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.
The following methods trigger validations, and will save the object to the database only if the object is valid:
The following methods skip validations, and therefore should be used with caution:
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
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}]
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?
Some validations that we have used so far:
Some new validations that we will learn:
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] }
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
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
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
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:
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
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
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
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
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
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
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
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
We actually have used callbacks in our app before. Can you tell me what is a callback?
Callbacks are methods that get called at certain moments of an object's life cycle.
This picture shows you the life cycle of an object in Rails and available callbacks for each state.
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
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.
Here are the requirements:
Here are the requirements:
Here are the requirements:
Based on those requirements: