In general, the term “model” refers to a representation of an object, a person, or a system.
In Rails, it's the root of the Model-View-Controller, or MVC, the architectural pattern generally used to develop user interfaces that's also used by the framework.
Their elemental function is to represent the data structure and business logic of an application while being able to access data stored in the database.
But that's not the only role they play in Rails. Let's see what are their main responsibilities:
Rails models responsibilities
Even if the function of models can be supplemented by other objects in some design patterns, in Rails, models have a couple of distinct responsibilities:
Data representation and persistence
Typically, models correspond to tables in the database following Rails conventions.
Each instance of the model represents a row in the table that, by convention, receives the name of the model but pluralized: The User
model corresponds to the users
table.
Mainly, Rails models inherit behavior from ApplicationRecord
which implements the Active Record ORM (Object-Relational Mapping) to facilitate the relation with the data stored in the database.
Active Record provides an easy-to-use interface for:
- Object mapping: in charge of mapping database tables to Ruby objects: database records become Ruby objects that have attributes that map to the db tables.
- Performing CRUD operations: creating, reading, updating and deleting records.
- Entity relationships: AR provides an easy way to define how data relates to each other.
- Validations: ensuring data is valid before it's saved to the database. This can be performed at the database level, but Rails offers an extra way of achieving the same outcome.
- Callbacks: lifecycle hooks that we can use to execute code at specific points of the object's life: before or after validations are performed or the object is saved to the database.
- Scopes: provide a way to retrieve collections of records with specific requirements using the ORM, Arel or plain SQL. ### Business Logic Besides object mapping and accessing the database, Rails models are used to encapsulate our application's business logic, which is the code that's specific to our application or business domain.
If we're building a SaaS app, we might need logic to see if a User
is subscribed or on a trial using our specific requirements.
It represents the rules, processes, and constraints that define how our application operates based on the requirements or our user's needs.
When using models, the logic is usually implemented using tools like custom methods, validations, associations, callbacks, etc.
We're also encouraged to use Ruby classes, also known as Plain-Ol'-Ruby-Objects, to represent entities in our application's domain that are not necessarily backed by a database.
Data relationships
Models are also in charge of the way data is connected by using the relational nature of SQL, which enforces those relationships using primary and foreign keys.
In Rails, we can define associations using the following methods:
-
has_many
: indicates a one-to-many relationship. For example, anAuthor
can have many books represented by theBook
model. -
belongs_to
: is the inverse relationship. A book belongs to an author via a foreign key which, by convention, should be anauthor_id
defined in theBook
model. -
has_one
: indicates a one-to-one relationship. Like a user who has aProfile
. -
has_and_belongs_to_many
: indicates a many-to-many relationship where we have an intermediate table, but we don't define a model. For example, aPost
has many tags and, at the same time, aTag
can belong to many posts. To achieve this, we need a table calledpost_tags
by convention and define thehas_and_belongs_to_many
in both models. -
has_many :through
: indicates a many-to-many relationship, but we have a model to represent it. For example, we could have aPostTag
orPostTagging
model, where we would add abelongs_to
association to both posts and tags. Meanwhile, we would declare thehas_many :through
in both models, providing us with access to the list of tags for a given post or a list of posts where a given tag was used. ## Model feature implementation The following are common ways of implementing the Rails model features explained above: ### Validations They allow us to ensure data integrity in the application itself, and they should be accompanied by database constraints to avoid data corruption:
class User < ApplicationRecord
validates :email, presence: true, uniqueness: true
validates :username, presence: true, length: {minimum: 6, maximum: 50}
end
The validations above require an email
and username
to be present to persist a user to the database, but they also require the username to be unique and the length of the username
to be at least 6 and at most 50 characters.
Rails includes many validations by default, but we can also define a custom validation using the validate
method:
class User < ApplicationRecord
validate :minimum_age
private
def minimum_age
return if date_of_birth.blank?
if date_of_birth > 18.years.ago.to_date
errors.add(:date_of_birth, "You must be at least 18 years old")
end
end
end
Callbacks
They allow us to execute code between the object's lifecycle:
class Article < ApplicationRecord
before_save :normalize_title
private
def normalize_title
self.title = title.downcase.titleize
end
end
The code above makes sure our article's title is normalized before being saved to the database.
Rails provides us with many callbacks that allow us to execute code when an object is being created, updated, destroyed or associated with other objects via associations.
Scopes
They are a way to retrieve a subset of records or sort them using a given criterion.
We can use Active Record, SQL, or even Arel to achieve our goal:
class Product < ApplicationRecord
scope :recent, -> { order(created_at: :desc) }
scope :by_price_ascending, -> { order(price_cents: :asc) }
scope :active, -> { where(active: true) }
end
We use the order
method to sort records and where
to query our database.
We can also use raw SQL if it suits us:
class User < ApplicationRecord
scope :active_last_week, -> { where("last_login_at >= ?", 1.week.ago) }
end
Or Arel which is a helper library that helps Rails build SQL queries that we can also use:
class User < ApplicationRecord
scope :active_last_week, -> {
where(Arel::Table.new(:users)[:last_login_at].gteq(1.week.ago))
}
end
Rails models best practices
Even though models are a special type of object that inherits from ApplicationRecord
, most of the OOP design advice applies to them.
The following is advice that's usually given when talking about models:
-
Single responsibility principle: models should focus on a single entity and the aspects that are related to it. For example: a
User
should not be too worried about subscription or payment logic. Otherwise, it usually means we're missing an object to model those other entities. - Fat models and skinny controllers: this is a common motto that was frequently used when Rails came out. It means that, given the choice, we should prefer to put business logic in models and not in controllers. There are many other design choices that can improve this simplified vision of application design, but it's a great starting point, especially when compared to alternatives like doing most work in controllers.
- DRY or Don't Repeat Yourself: reusing code is encouraged. The DRY principle centers around the fact that duplicate code can become an issue in the future, so it promotes the use of reusable code. In Rails, we can use concerns or—more rarely—inheritance to achieve this goal.
- Unit and integration testing: adding tests to make sure our models work like intended is desirable. Automated tests might seem like a drag at first but they pay for themselves almost invariably in the long term.