Chapter 3. Models

This chapter explains refactoring techniques and cautionary points in Model.

Initial value

initialize

If you want to set a default value to Model, you can write as follows. This is a function of Ruby itself. (super is for overriding ActiveRecord.)

def initialize(args = {})
  super
  self.name = 'New user'
end

after_initialize is also called when the registered record initialized, but I think this is little or no used. Similar method after_find is little or no used too.

* It doesn't mean you mustn't use it.

DB default

It can be used default option in create_table block to set default values to DB values as follows. This will be the initial value when Model.new.

create_table :users do |t|
  t.string :name, default: 'New user'
  t.integer :status, default: 1
end

You can use this for the value that are unlikely to change in the future.

Be careful of Callbacks using

Callbacks before_save and after_commit etc, are automatically called at save. Since Callbacks can easily become complicated and will be called at unintended times, it is necessary to consider whether it should really be used.

In major methods, callbacks are called at these times.

  • create, create!
  • save, save!
  • update, update!
  • destroy, destroy!

And basically, these methods should be used when INSERT, UPDATE and DETETE. If other methods are used, Validations and Callbacks may not be called. Therefore, it is necessary to check if they are not needed.

For example, if you want to use update_all, make sure to check if you really can skip Validations and Callbacks.

Checking input values should bring together to Validations.

Validations are used to ensure that invalid values are not INSERT/UPDATE-ed into the DB. Validations are automatically called by the methods introduced in the previous section, but can also be called manually by these methods.

  • valid?, invalid?

Validations are a quite good standard feature and well documented.

Rails Guides - Active Record Validations

Not that not table column values can be checked by other methods (such as private methods in Service class).

Base Validations

class User < ApplicationRecord
  validates :name, presence: true
end

Various value checks can be used, such as null check, numbers, in range, case, etc.

Automatic generation Validation code

Validations can be generated automatically by using gems such as schema_validations and pretty_validation.

This gems are a little old, but they are for development so you don't care too much about them. If you are concerned about them, you can remove them from Gemfile after use.

Needless to say, you should check the generated code.

Custom Validations

Use custom validations for complex value checks that are not possible with normal validations.

class User < ApplicationRecord
  validates :name, foo: true
end

This will call FooValidator. This is a standard feature in Rails. It can be put in app/validators and separate them from app/models. If it is difficult to create a new directory in app, you can create a directory app/models/{table name} and put it there.

  • To validate single column,
    • Use validates in the Model
    • Create Activemodel::EachValidator class and define validate_each method in it.
  • To validate multiple columns or multiple child models,
    • Use validates_with in the Model
    • Create Activemodel::Validator class and define validate method in it.
  • If you doun't separate file,
    • Use validate and define the method in the Model.

Avoiding validations

save(validate: false) can be used to skip Validations but, it should basically not be used except during development or special operations on the admin system.

The first thing to keep in mind is that neither Rails nor MySQL are good at handling subqueries. So unless you have a reason to do so, do not use subqueries, but rather execute SQL multiple times to retrieve data. Of course, the records should be narrowed down as much as possible by index and WHERE conditions.

all

Model.all class is ActiveRecord_Relation (so-called scope), not Array. It is not yet instantiated and no SQL has been executed.

ActiveRecord_Relation

It is convenient and Rails-like to use this class. It can be used Array methods. SQL will be executed and instantiated when Array method is used.

select

It tends not to be used very often except when using GROUP. It tends not to be used much except when using GROUP. In fact, it may be better to have it, but it is often omitted. For APIs, use a serializer to pick columns.

where

Basically, written in the following two ways.

  • Placeholder where("created_at > ?" , date)
  • Hash where(created_at: date)

Direct embedding of user-input strings as follows is not secure because it is not escaped.

  • where("created_at > #{params[:date]}")

group

Returns as a hash. It is not an intuitive(simple) method, so you can want to consider other ways.

* It does not mean you must not use it.

order

Use order(id: :desc) for descending order.

default_scope

It will include SQL such as where and order by default. It is also the default value for new when it is used for where.

If you are concerned about unintended behavior, you can use only the default order for convenience.

merge

It is a convenience method to cleanly merge scopes that has already been defined in joining Models.

Example.

class Post < ApplicationRecord
  has_many :comments
end

class Comment < ApplicationRecord
  belongs_to :post
  scope :draft, -> { where(status: :draft) }
end

Post.joins(:comments).merge(Comment.draft).

Result SQL.

SELECT (omitted) WHERE comments.status = 'draft'

Checking SQL

It can be used to_sql, but it does not execute SQL and just output a SQL string. So, I recommend to use first or to_a that will execute SQL, and check its logs.

For checking the effectiveness of index, it can be used .explain.

How to use joins, includes, preload, eager_load

To keep it simple,

  • For INNER JOIN, use joins.
  • For LEFT JOIN, use preload.
    • If have a problem with it, use eager_load

To be precise, Preloads are not execute LEFT JOIN, but rather execute another SQL.

It is not prohibited to use includes, but it requires a deeper understanding of the behavior and it may be a little difficult to use.

About the N+1 problem

The N+1 is a performance problem that can occur in Rails applications. There is no detailed explanation here, so if you don't know it, please Google it and learn it.

Since it is difficult to detect this problem perfectly by oneself, it is better to use the gem bullet to detect them automatically.

Github - bullet

arel_table

It will be able to create OR and complex SQL using only ActiveRecord functions, but it tends not to be recommended in terms of code readability.

I have seen some unreadable code using this, so I don't think it is necessary to use it. I recommend using String, Heredoc or another SQL instead of arel_table.

Some people may think "writing SQL in String is not a clean", but it is fine to recognize "writing SQL in String tends to make it not clean". Don't have to deny it strongly.

Enumeration

Use enum to have numerical status values, such as a status column. Also, String value can be used for DB values so that they can be easily recognized.

This feature since Rails 4.1, so older systems may have other implementation or use the gem enumerize.

How to use

Define with _prefix: true is recommended like the following.

class User < ApplicationRecord
  enum status: { draft: 0, public: 1, private: 2 }, _prefix: true
end

In this case, user.status to get 'draft' but it is stored 0 as a number in DB.

Also, these methods will also be available.

  • user.status_draft? check the status.
  • user.status_draft! update the status.
  • User.statuses get the statuses. (Class method)

Concerning to organize code

Module#concerning can be used to organize code and I recommend this for refactoring.

Ruby on Rails API - Module::Concerning

This is same as including the module. Writing within a file prevents excessive file splitting, and by making groups of functions into blocks, readability is increased.

It can be used anywhere, not only in Models. Note that it is not related to models/concerns directly.

Namespacing for Model

In some cases, it is wanted to create a namespace to organize what the model files are related to.

When model name ≠ table name, the following can be used.

  • table_name_prefix for the Models under the namespace.
  • table_name for a specific Model.

Ruby on Rails API - table_name_prefix=

Using Namespace is a little difficult to use

So, I don't use it, but it does not mean that you must not use Namespace. Using well, it may be possible to organize the Models nicely. If you feel stuck to implement, you can implement without Namespace first, and then try to add it later.

Devises to manage Namespace

  1. Prefix child table names with the parent table name

Example.

If there are three tables related to the company,

  • Companies
  • Company's employees
  • Employees' jobs

then

  • companies - company_staffs - company_staff_jobs

could be good. This is easier to understand in terms of relevance than

  • companies - employees - jobs

In some cases, it may be better not to use this rule because it makes it difficult to understand the entity (what the table manages), but basically, first consider possible to use this rule.

  1. Alias for associations

For the following definition in company.rb,

has_many :company_staffs

and add the following.

has_many :staffs, class_name: :company_staffs

By doing so, the redundant description company.company_staffs can be written as company.staffs. Note that there may be difficult to read in some cases, so be careful not to just add aliases without thinking.

How to use concerns directory in app/models

Putting all Model's modules into app/models/concerns/ will become unmanageable. So, it is needed to put only common functions into it.

For modules used only for a specific model, it is recommended to create the app/models/{table name}/ directory and place them there instead of concerns. For example, use app/models/users/ to seperate the functions from the User Model.

Some people don't want to use concerns, but this rule is fine to use it.

Naming with able suffix

Module name

Defining the module name with able makes it easier to understand what kind of behavior is being added.

It is more intuitive include Taggable than include TagModule. This is not Ruby or Rails coding convention, but a convention. If you have more intuitive and understandable, you can arrange it.

Method and column names

Also, it can be used for method and column names.

It may be more intuitive taggable? than can_tag? or can_use_tag?.

* ? can't be used in table column name.

Introduction convenience some methods for checking Model status

  • model.new_record? check if new record.
  • model.persisted? check if exists in the DB.
  • model.changed? check if record values changed.
  • model.{column name}_changed? check if a specific column changed.
  • model.changes get before and after values as a Hash.
  • model.{column name}_was get a value before the change.