🛤️ Ruby on Rails - Models
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