ruk·si

🛤️ Ruby on Rails
Models

Updated at 2016-01-06 22:43

Related to Ruby Guide and Ruby on Rails Guide.

Keep model names short. Name the models with meaningful but short names without abbreviations.

Use model generators. More about migrations in Ruby on Rails - Migration notes.

# Creates model, migration, tests and fixtures (test sample data).
rails generate model User name:text email:text
rake db:migrate # you can edit the migration before this

# Removes User model and creates remove migration.
rake db:rollback # if you already migrated
rails destroy model User

# Also creates controller, views, styles, js, routes and JSON output `.json`.
rails generate scaffold User name:text email:text
rake db:migrate # you can edit the migration before this

# Removes User scaffold files and creates remove migration.
rake db:rollback # if you already migrated
rails destroy scaffold User

Introduce non-ActiveRecord model classes freely. They are not stored in the database, they just make code easier to reason about while implementing the interface you would expect from a model.

# in rails 4
class Contact
  include ActiveModel::Model
  attr_accessor :email, :message
  validates :email, presence: true, length: { :in => 2..255 }
end

Optionally also use ActiveAttr. If you need model objects that support ActiveRecord behavior like validation without the ActiveRecord database functionality use the ActiveAttr gem. It's like extended ActiveModel.

class Message
  include ActiveAttr::Model

  attribute :name
  attribute :email
  attribute :content
  attribute :priority, :default => "High"

  attr_accessible :name, :email, :content

  validates :name, presence: true
  validates :email, format: {
    :with => /\A[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}\z/i
  }
  validates :content, length: { :maximum => 500 }
end

Everything related to database should be in the model. A good rule of thumb is that only models can have save method calls. It forces you to make more context aware methods for saving your data. Figure out more domain specific name for the action you are doing.

Models should have more code than controllers. Pulling functionality from controllers to models helps to reduce duplication.

Use form models. Create a non-database model for forms that knows how to save itself, usually including calls to multiple other models.

ActiveRecord

Avoid altering ActiveRecord defaults. Like table names or primary key unless you have a very good reason like a database that's not under your control.

# bad
class Transaction < ActiveRecord::Base
  self.table_name = 'order'
  # ...
end

Use reload to refresh values from the database.

@user.reload
@user.reload(lock: true) # paramater is a hash passed to `find`

Group macro-style methods in the beginning of the class definition. Like has_many and validates.

class User < ActiveRecord::Base
  # scopes
  default_scope { where(active: true) }

  # constants
  COLORS = %w(red green blue)

  # attr related macros
  attr_accessor :formatted_date_of_birth
  attr_accessible :login, :first_name, :last_name, :email, :password

  # associations
  belongs_to :country
  has_many :authentications, dependent: :destroy

  # validations
  validates :email, presence: true
  validates :username, presence: true
  validates :username, uniqueness: { case_sensitive: false }
  validates :username, format: { with: /\A[A-Za-z][A-Za-z0-9._-]{2,19}\z/ }
  validates :password, format: { with: /\A\S{8,128}\z/, allow_nil: true}

  # callbacks
  before_save :cook
  before_save :update_username_lower

  # other macros (like devise's)

  # methods and all the other stuff
end

Use model level relations.

class Client < ActiveRecord::Base
  has_one :address
  has_many :orders
  has_and_belongs_to_many :roles
end

class Address < ActiveRecord::Base
  belongs_to :client
end

class Order < ActiveRecord::Base
  belongs_to :client, counter_cache: true
end

class Role < ActiveRecord::Base
  has_and_belongs_to_many :clients
end

__Prefer has_many :through to has_and_belongs_to_many. __ Using has_many :through allows additional attributes and validations on the join model.

# bad
class User < ActiveRecord::Base
  has_and_belongs_to_many :groups
end

class Group < ActiveRecord::Base
  has_and_belongs_to_many :users
end

# good
class User < ActiveRecord::Base
  has_many :memberships
  has_many :groups, through: :memberships
end

class Membership < ActiveRecord::Base
  belongs_to :user
  belongs_to :group
end

class Group < ActiveRecord::Base
  has_many :memberships
  has_many :users, through: :memberships
end

Prefer self[:attribute] over read_attribute(:attribute).

# bad
def amount
  read_attribute(:amount) * 100
end

# good
def amount
  self[:amount] * 100
end

Prefer self[:attribute] = value to write_attribute(:attribute, value).

# bad
def amount
  write_attribute(:amount, 100)
end

# good
def amount
  self[:amount] = 100
end

Single-Table Inheritance

STI allows using a single table as a backend for multiple model types. Possible by adding type text column to the table that tells which type the row is. You should only use STI if you have inheritance in the ActiveRecord models, both classes have very similar behaviour and the column data is very similar. Too different behaviour results in messy classes and too different data results in messy database with a lot of nulls.

# in the users table migration...
t.string :type

# /config/application.rb
config.autoload_paths += %W(#{config.root}/app/models/users)

# /app/models/users/user.rb
class User < ActiveRecord::Base
end

# /app/models/users/requester.rb
class Requester < User
end

# /app/models/users/requestee.rb
class Requestee < User
end

Abstract Base Class

ABC is similar to STI, but data is stored in separate tables. Shared base behaviour with the option to have very different data. Other option to share behaviour is using concerns.

class Place < ActiveRecord::Base
  self.abstract_class = true
end
# Now this class doesn't have a db table so extending models
# can be kept in their own tables.

URLs

Use user-friendly URLs. Show some descriptive attribute of the model in the URL rather than its id. There is more than one way to achieve this.

  • Override the to_param method of the model. This method is used by Rails for constructing a URL to the object. The default implementation returns the id of the record as a String. It could be overridden to include another human-readable attribute.
    class Person
      def to_param
        "#{id} #{name}".parameterize
      end
    end
    
  • Use the friendly_id gem. It allows creation of human-readable URLs by using some descriptive attribute of the model instead of its id.
    class Person
      extend FriendlyId
      friendly_id :name, use: :slugged
    end
    

Callbacks

Define before_destroy with prepend: true. Always call before_destroy callbacks that perform validation with prepend: true.

# bad - roles will be deleted automatically even if super_admin? is true
has_many :roles, dependent: :destroy

before_destroy :ensure_deletable

def ensure_deletable
  fail "Cannot delete super admin." if super_admin?
end

# good
has_many :roles, dependent: :destroy

before_destroy :ensure_deletable, prepend: true

def ensure_deletable
  fail "Cannot delete super admin." if super_admin?
end

Saving

Utilize read-only models when possible.

# disable creation, edit and delete
after_initialize :readonly!

# allow creation, disable edit and delete
def readonly?
  !new_record?
end

Use conditional creation. Great with user inputted tags.

Tag.find_or_create_by(name: 'News')
# => creates the user if it doesn't exist
Tag.find_or_create_by!(name: 'News')
# => ... also raises exception if the created object is invalid

Optimistic locking strategy is most applicable to high-volume systems. You don't expect many collisions in database writes and reads. Has better performance.

c = Client.find(1)
c.first_name = "Michael"
c.save

Pessimistic locking strategy has better data integrity. Requires you to be careful with your application design to avoid deadlocks.

# default locking
Item.transaction do
  i = Item.lock.first
  i.name = 'Jones'
  i.save!
end

item = Item.first
item.with_lock do
  item.increment!(:views)
end

Sources