In my previous post I have noted that Rails encourages application developers to rely on feral mechanisms, such as validations and associations, to ensure application integrity. In this post, I first explore various feral mechanisms in Rails, and how they are being used by some sample applications. Next, I will throw some light on how Rails actually enforces these feral mechanisms.
ORM and Active Record
Quoting from Ruby on Rails guide:
Object Relational Mapping: Object-Relational Mapping, commonly referred to as its abbreviation ORM, is a technique that connects the rich objects of an application to tables in a relational database management system. Using ORM, the properties and relationships of the objects in an application can be easily stored and retrieved from a database without writing SQL statements directly and with less overall database access code.
Active Record as an ORM Framework:
ActiveRecord is Rails’s implementation of ORM. Active Record gives
us several mechanisms, the most important being the ability to:
- Represent models and their data.
- Represent associations between these models.
- Represent inheritance hierarchies through related models.
- Validate models before they get persisted to the database.
- Perform database operations in an object-oriented fashion.
Active Record Models:
A class that extends
ActiveRecord is an Active Record model, or
simply a model. Each model has a corresponding table in the underlying
database. For instance, defining the
class Product < ActiveRecord::Base end
will create a table with the following schema:
CREATE TABLE products ( id int(11) NOT NULL auto_increment, name varchar(255), PRIMARY KEY (id) );
To write a record to the the database, we first create an Active
Record object (note:
arg: denotes an argument named arg. Think of
Ruby methods as functions accepting named variants):
product = Product.create(name: "Milk")
product = Product.new do |p| p.name = "Milk" end
ActiveRecord offers save method:
Reading can be done via multiple querying methods. Example:
all_prods = Product.all milk = Product.find_by(name: "Milk").order(id: :asc)
Active record querying webpage has more details.
Updatation can be done via read-modify-save sequence:
milks = Product.find_by(name: "Milk") milks.each do |m| m.name = "Whole Milk" m.save end
Note: Active Record uses SQL INSERT for new record saves, and SQL
UPDATE for updatations. It uses
new_record? flag to keep track of new
Active Record objects that are not yet saved into the database
(source: Active Record Callbacks guide).
Deletion is done using
milks = Product.find_by(name: "Milk") milks.each do |m| m.destroy end
The corresponding records will be deleted from the database.
Associations are useful to keep track of semantic associations between
various models. For instance, using a
belongs_to association, the
developer can let Rails know that every order in an eCommerce
application belongs to utmost one customer (Note: the
denotes a symbol called name. Meta-level functions accept symbols
and generate new code, which would then be spliced into original code
(somewhat like MetaOCaml and TemplateHaskell). Ruby, being a dynamic
language, does not treat meta-level functions specially.).
class Order < ActiveRecord::Base belongs_to :customer end
The result is that the
order table will now have a column named
customer_id that is expected to refer to
id of the
table. Order objects will also have a field named
referring to the Customer object to which the order belongs to
(note: the field can be
nil). When an order object is saved, the
id of its customer object will be saved to the
of the corresponding record in the database.
The other end of
belongs_to association is usually a
has_one association. Using a
the developer can specify that a customer can have one or many orders:
class Customer < ActiveRecord::Base has_many :orders end
Other than adding a field named
orders to a customer object,
has_many association will have no tangible effect;
there is no change in Rails’s interaction with
tables. However, we can qualify the
has_many association with an
dependent argument set to
:destroy that asks Rails to
destroy all orders placed by a customer, when the customer is deleted.
class Customer < ActiveRecord::Base has_many :orders, dependent: :destroy end
This behaviour corresponds to
ON DELETE CASCADE behaviour offered by
SQL for foreign key dependents.
belongs_to association from
has_many association from
Order, with a
:delete qualification, we have something close to, but not exactly, a
foreign key constraint from the order table to the customer table. To
effectively enforce a foreign key constraint, we need a guarantee that
an order cannot exist without the corresponding customer existing in
the customer table. Currently, this invariant can be violated if, for
save an order object referring to a customer object, whose
corresponding record was already destroyed. The effective foreign key
enforcement that prevents this scenario (theoretically, at least) can
be achieved using Model Validations, described in the next section.
It is also possible to specify many-to-many relationships between
models in a similar way to how such relationships are defined in
relational databases: via an intermediary model. For example, in a
microblogging application, a user can follow many users, and can
have many users as followers. This many-to-many relationship between
users can be specified via a third model called, say,
relationships and follower
relationships. Each relationship
belongs_to two users: user who is
being followed, and the user who is following. This scenario is
described in the picture below (source: Michael Hartl’s
book. Released under MIT License):
Observe that there is a transitive
has_many relationship between
users, which can be made explicit via
through: argument to
has_many association: a user
has_many followers through:
has_many followed_users through: relationships.
has_one, Rails defines few
other associations. They are described here.
Quoting from Rails guide on validations: Validations are used to ensure that only valid data is saved into your database. For example, it may be important to your application to ensure that every user provides a valid email address and mailing address. Model-level validations are the best way to ensure that only valid data is saved into your database.
Validations are primarily declared using
method. For example, to ensure that user provides an
email address during during a new account creation (i.e.,
nil), and to ensure that there are no existing accounts
associated with this email, we can perform
uniqueness validations, respectively.
class Account < ActiveRecord::Base validates :email, uniqueness: true, presence: true end
Rails also provides variants of
validates specialized for common
class Account < ActiveRecord::Base validates_presence_of :email validates_uniqueness_of :email end
validates_uniqueness_of is clearly useful to ensure primary key
property. Besides being useful to preempt
validates_presence_of is also useful to enforce foreign key
constraints, when used in conjunction with associations. For instance,
in the eCommerce example, the foreign key constraint from
Customer can be enforced by validating the presence of
class Order < ActiveRecord::Base belongs_to :customer validates_presence_of :customer end class Customer < ActiveRecord::Base has_many :orders, dependent: :destroy end
Another useful validation is
validates_associated. You should use
this helper when your model has associations with other models and
they also need to be validated. When you try to save your object,
valid? will be called upon each one of the associated objects. For
instance, when the
Customer class validates associated orders:
class Customer < ActiveRecord::Base has_many :orders, dependent: :destroy validates_associated :orders end
All the orders associated with the customer are validated when the
customer object is being
save‘d (in this case, this entails a
validation for the presence of the customer object owning orders,
which is trivially true).
validates_associated works with all of the association types.
Although it is by default turned off for most associations, it is by
default on for
has_many association. So, for the above example we
needn’t explicitly specify
Note: Ruby has a concept of virtual attributes, which, in the
ActiveRecord are attributes that do not get recorded in
the database. Validations can also be defined over virtual attributes.
Validations, such as
validates_presence_of, are built into Rails.
Sometimes, built-in validations are not sufficient. Rails allows
developers to define custom validations for this purpose. Custom
validations can be defined as methods, which are used once locally, or
they can be defined by implementing Rails’s
interface. Examples of custom validators include Spree’s
AvailabilityValidator, which checks whether an eCommerce inventory
as sufficient stock available to fulfill an order. More on custom
When are validations enforced?
When an Active Record object is being persisted to the database
save), although this is not entirely accurate:
Validations are typically run before these commands (INSERT/UPDATE) are sent to the database. If any validations fail, the object will be marked as invalid and Active Record will not perform the INSERT or UPDATE operation. This helps to avoid storing an invalid object in the database. You can choose to have specific validations run when an object is created, saved, or updated.
There are many ways to change the state of an object in the database. Some methods will trigger validations, but some will not. This means that it’s possible to save an object in the database in an invalid state if you aren’t careful. - ActiveRecord validations callbacks guide
Validations and any callbacks registered on the state changes of the model object are queued for execution. This queue will include all your model’s validations, the registered callbacks, and the database operation to be executed. The whole callback chain is wrapped in a transaction. If any validation/callback method returns false or raises an exception the execution chain gets halted and a ROLLBACK is issued (source).
We will now examine some sample applications to understand how validations and associations are being used.
The app defines 3 models -
with following associations and validations:
belongs_toa user, and validates user’s presence.
- A user has an email address; validates its presence and uniqueness.
- A user
has_manyfollower relationships, and
has_manyfollowers. Follower relationships need to be destroyed if this user is deleted.
- A user
has_manyfollowing relationships, and
has_manyfollowed users. Following relationships need to be destroyed if this user is deleted.
- A relationship jointly
belongs_toa follower user and a followed user, and validates their presence.
Following are some interesting operations that the app defines:
Adding a User: Adds a user after validating that the email is present and unique.
addUser(u) = transaction do assert(u.email != nil); dups := SQL "SELECT * FROM users WHERE email = `u.email` LIMIT 1" assert (dups == ); SQL "INSERT INTO users VALUES (`freshId()`,`u.email`,`u.name`)"; followUser(thisUser, thisUser); /* One must follow oneself */
Follow a User: Makes the current user follow another user.
followUser(thisUser,thatUser) = transaction do usr1 := SQL "SELECT * FROM users WHERE id = `thisUser.id` LIMIT 1"; assert (usr1 != ); usr2 := SQL "SELECT * FROM users WHERE id = `thatUser.id` LIMIT 1"; assert (usr2 != ); SQL "INSERT INTO Relationships VALUES (`thisUser.id`,`thatUser.id`)";
Unfollow a User: Makes the current user unfollow other user.
unfollowUser(thisUser,thatUser) = transaction do SQL "DELETE FROM Relationships WHERE follower_id = `thisUser.id` AND followed_id = `thisUser.id`";
getFollowers: Returns the list of users following the current user.
/* Implementation 1 : from the original sample app */ getFollowers(thisUser) = SQL "SELECT * FROM users INNER JOIN relationships ON users.id = relationships.follower_id WHERE relationships.followed_id = `thisUser.id`" /* Implementation 2: If the data store does not support joins (e.g: Cassandra) */ getFollowers(thisUser) = transaction do rels := SQL "SELECT * FROM relationships WHERE followed_id = `thisUser.id`"; followers := ; rels.each |rel| do /* The following pattern-match must always pass because: 1. Relationship model validates presence of follower before persisting. 2. When a user is deleted, all the dependent relationships are deleted as well. 3. User model validates uniqueness when persisiting. */ [follower] = SQL "SELECT * FROM users WHERE id = `rel.follower_id`"; followers := follower::followers; return followers;
getFollowedUsers: Returns the list of users that the current user
is following. Implementation similar to
postBlog: Posts a microblog on behalf of the current user. Validates the presence of current user record before persisiting the microblog.
postBlog(thisUser,blog) = transaction do usr = SQL "SELECT * FROM users WHERE id = `thisUser.id` LIMIT 1"; assert(usr != ); SQL "INSERT INTO microposts (content, user_id) VALUES (`blog.content`, `thisUser.id`)";
getTimeLine: Get a list of microposts by a user (the call is:
getTimeLine(user) = transaction do posts := SQL "SELECT * FROM microposts WHERE user_id = `user.id`"; posts.each |post| do post.user := (SQL "SELECT * FROM users WHERE id = post.user_id LIMIT 1").first; return posts;
The SQL call to
users table never returns an empty collection because:
- A micropost
belongs_toa user, and checks the presence of the user before persisting, and
- When a user is deleted, the dependent microposts are also deleted.
.first on the collection returned by SQL is always valid.
getFeed: Get a list of microposts by users being followed by the current user.
getFeed(user) = transaction do followed_ids := SQL "SELECT followed_id FROM relationships WHERE follower_id = `user.id`"; uids_of_interest := user.id :: followed_ids ; posts := SQL "SELECT * FROM microposts WHERE user_id IN `uids_of_interest`"; posts.each |post| do post.user := (SQL "SELECT * FROM users WHERE id = post.user_id LIMIT 1").first; return posts;
deleteUser: Delete the current user. It has to enforce
:destroy on microposts and relationships.
deleteUser(thisUser) = transaction do /* First, delete all microposts by this user */ SQL "DELETE FROM microposts WHERE user_id = `thisUser.id`"; /* Next, delete all relationships on this user */ SQL "DELETE FROM relationships WHERE follower_id = `thisUser.id` OR followed_id = `thisUser.id`"; /* Finally, delete the user */ SQL "DELETE FROM users WHERE id = `thisUser.id`";