Rails Coding Conventions (v2) - (The Rails Style)
White

pdkproitf viết ngày 20/06/2017

Welcome back, Before reading this chapter, let make sure that you already read through on chapter Use Ruby Style on Rails. In previous chapter I already talk about how to Use Ruby Style On Rails Framework. Now, I'm going to discuss about Rails convention.

In this article, I won't discuss full of the conventions. I just focus to some main styles that I think any Rails developer usually meet.

  • Model
  • Migration
  • Mailers
  • Routing
  • Time

Model

  • Name the models with meaningful (but short) names without abbreviations
  • If you need model objects that support Actirecod behavior (like validation) without the ActiveRecord database functionality use the ActiveAttr gem.
class Message
  include ActiveAttr::Model

  attribute :name
  attribute :email
  attribute :content
  attribute :priority

  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

ActiveRecord

  • Avoid altering ActiveRecord defaults (table names, primary key, etc) unless you have a very good reason (like a database that's not under your control).
# bad - don't do this if you can modify the schema
class Transaction < ActiveRecord::Base
  self.table_name = 'order'
  ...
end
  • Group macro-style methods (has_many, validates, etc) in the beginning of the class definition.
class User < ActiveRecord::Base
  # keep the default scope first (if any)
  default_scope { where(active: true) }

  # constants come up next
  COLORS = %w(red green blue)

  # afterwards we put attr related macros
  attr_accessor :formatted_date_of_birth

  attr_accessible :login, :first_name, :last_name, :email, :password

  # Rails4+ enums after attr macros, prefer the hash syntax
  enum gender: { female: 0, male: 1 }

  # followed by association macros
  belongs_to :country

  has_many :authentications, dependent: :destroy

  # and validation macros
  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 }

  # next we have callbacks
  before_save :cook
  before_save :update_username_lower

  # other macros (like devise's) should be placed after the callbacks
  ...
end
  • Prefer has_many :through to has_and_belongs_to_many. Using has_many :through allows additional attributes and validations on the join model.

# not so good - using has_and_belongs_to_many
class User < ActiveRecord::Base
  has_and_belongs_to_many :groups
end

class Group < ActiveRecord::Base
  has_and_belongs_to_many :users
end

# preferred way - using has_many :through
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) and self[:attribute] = value over write_attribute(:attribute, value)
# bad
def amount
  write_attribute(:amount, 100)
  read_attribute(:amount) * 100
end

# good
def amount
  self[:amount] = 100
  self[:amount] * 100
end
  • Always use the new "sexy" validations.
# bad
validates_presence_of :email
validates_length_of :email, maximum: 100

# good
validates :email, presence: true, length: { maximum: 100 }
  • When a custom validation is used more than once or the validation is some regular expression mapping, create a custom validator file.
# bad
class Person
  validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i }
end

# good
class EmailValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    record.errors[attribute] << (options[:message] || 'is not a valid email') unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
  end
end

class Person
  validates :email, email: true
end
  • Keep custom validators under app/validators.
  • Use named scopes freely.
class User < ActiveRecord::Base
  scope :active, -> { where(active: true) }
  scope :inactive, -> { where(active: false) }

  scope :with_orders, -> { joins(:orders).select('distinct(users.id)') }
end
  • When a named scope defined with a lambda and parameters becomes too complicated, it is preferable to make a class method instead which serves the same purpose of the named scope and returns an ActiveRecord::Relation object. Arguably you can define even simpler scopes like this.
class User < ActiveRecord::Base
  def self.with_orders
    joins(:orders).select('distinct(users.id)')
  end
end
  • Beware of the behavior of the following methods. They do not run the model validations and could easily corrupt the model state.
# bad
Article.first.decrement!(:view_count)
DiscussionBoard.decrement_counter(:post_count, 5)
Article.first.increment!(:view_count)
DiscussionBoard.increment_counter(:post_count, 5)
person.toggle :active
product.touch
Billing.update_all("category = 'authorized', author = 'David'")
user.update_attribute(:website, 'example.com')
user.update_columns(last_request_at: Time.current)
Post.update_counters 5, comment_count: -1, action_count: 1

# good
user.update_attributes(website: 'example.com')
  • Use find_each to iterate over a collection of AR objects. Looping through a collection of records from the database (using the all method, for example) is very inefficient since it will try to instantiate all the objects at once. In that case, batch processing methods allow you to work with the records in batches, thereby greatly reducing memory consumption.
# bad
Person.all.each do |person|
  person.do_awesome_stuff
end

Person.where('age > 21').each do |person|
  person.party_all_night!
end

# good
Person.find_each do |person|
  person.do_awesome_stuff
end

Person.where('age > 21').find_each do |person|
  person.party_all_night!
end
  • Since Rails creates callbacks for dependent associations, always call before_destroy callbacks that perform validation with prepend: true. [link]
# 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
  • Define the dependent option to the has_many and has_one associations. [link]
# bad
class Post < ActiveRecord::Base
  has_many :comments
end

# good
class Post < ActiveRecord::Base
  has_many :comments, dependent: :destroy
end
  • When persisting AR objects always use the exception raising bang! method or handle the method return value. This applies to create, save, update, destroy, first_or_create and find_or_create_by. [link]
# bad
user.create(name: 'Bruce')

# good
user.create!(name: 'Bruce')
# or
bruce = user.create(name: 'Bruce')
if bruce.persisted?
  ...
else
  ...
end

ActiveRecord Queries

  • Avoid string interpolation in queries, as it will make your code susceptible to SQL injection attacks.
# bad - param will be interpolated unescaped
Client.where("orders_count = #{params[:orders]}")

# good - param will be properly escaped
Client.where('orders_count = ?', params[:orders]) 

  • Consider using named placeholders instead of positional placeholders when you have more than 1 placeholder in your query.
# okish
Client.where(
  'created_at >= ? AND created_at <= ?',
  params[:start_date], params[:end_date]
)

# good
Client.where(
  'created_at >= :start_date AND created_at <= :end_date',
  start_date: params[:start_date], end_date: params[:end_date]
)
  • Favor the use of find over where or find_by over where and find_by_attribute when you need to retrieve a single record by id.
# bad
User.where(id: id).take

# good
User.find(id)

# bad
User.where(first_name: 'Bruce', last_name: 'Wayne').first

# bad
User.find_by_first_name_and_last_name('Bruce', 'Wayne')

# good
User.find_by(first_name: 'Bruce', last_name: 'Wayne')
  • Favor the use of where.not over SQL
# bad
User.where("id != ?", id)

# good
User.where.not(id: id)

Migrations

  • Keep the schema.rb (or structure.sql) under version control.
  • Use rake db:schema:load instead of rake db:migrate to initialize an empty database.
  • Enforce default values in the migrations themselves instead of in the application layer. [link]
# bad - application enforced default value
class Product < ActiveRecord::Base
  def amount
    self[:amount] || 0
  end
end

# good - database enforced
class AddDefaultAmountToProducts < ActiveRecord::Migration
  def change
    change_column_default :products, :amount, 0
  end
end
  • When writing constructive migrations (adding tables or columns), use the change method instead of up and down methods.
# the old way
class AddNameToPeople < ActiveRecord::Migration
  def up
    add_column :people, :name, :string
  end

  def down
    remove_column :people, :name
  end
end

# the new preferred way
class AddNameToPeople < ActiveRecord::Migration
  def change
    add_column :people, :name, :string
  end
end
  • If you have to use models in migrations, make sure you define them so that you don't end up with broken migrations in the future
# db/migrate/<migration_file_name>.rb
# frozen_string_literal: true

# bad
class ModifyDefaultStatusForProducts < ActiveRecord::Migration
  def change
    old_status = 'pending_manual_approval'
    new_status = 'pending_approval'

    reversible do |dir|
      dir.up do
        Product.where(status: old_status).update_all(status: new_status)
        change_column :products, :status, :string, default: new_status
      end

      dir.down do
        Product.where(status: new_status).update_all(status: old_status)
        change_column :products, :status, :string, default: old_status
      end
    end
  end
end

# good
# Define `table_name` in a custom named class to make sure that
# you run on the same table you had during the creation of the migration.
# In future if you override the `Product` class
# and change the `table_name`, it won't break
# the migration or cause serious data corruption.
class MigrationProduct < ActiveRecord::Base
  self.table_name = :products
end

class ModifyDefaultStatusForProducts < ActiveRecord::Migration
  def change
    old_status = 'pending_manual_approval'
    new_status = 'pending_approval'

    reversible do |dir|
      dir.up do
        MigrationProduct.where(status: old_status).update_all(status: new_status)
        change_column :products, :status, :string, default: new_status
      end

      dir.down do
        MigrationProduct.where(status: new_status).update_all(status: old_status)
        change_column :products, :status, :string, default: old_status
      end
    end
  end
end
  • Name your foreign keys explicitly instead of relying on Rails auto-generated FK names.
# bad
class AddFkArticlesToAuthors < ActiveRecord::Migration
  def change
    add_foreign_key :articles, :authors
  end
end

# good
class AddFkArticlesToAuthors < ActiveRecord::Migration
  def change
    add_foreign_key :articles, :authors, name: :articles_author_id_fk
  end
end
  • Don't use non-reversible migration commands in the change method. Reversible migration commands are listed below. ActiveRecord::Migration::CommandRecorder.
# bad
class DropUsers < ActiveRecord::Migration
  def change
    drop_table :users
  end
end

# good
class DropUsers < ActiveRecord::Migration
  def up
    drop_table :users
  end

  def down
    create_table :users do |t|
      t.string :name
    end
  end
end

# good
# In this case, block will be used by create_table in rollback
# http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters.html#method-i-drop_table
class DropUsers < ActiveRecord::Migration
  def change
    drop_table :users do |t|
      t.string :name
    end
  end
end

Mailers

  • Name the mailers SomethingMailer. Without the Mailer suffix it isn't immediately apparent what's a mailer and which views are related to the mailer.
  • Provide both HTML and plain-text view templates.
  • Enable errors raised on failed mail delivery in your development environment. The errors are disabled by default.
# config/environments/development.rb

config.action_mailer.raise_delivery_errors = true
  • If you need to use a link to your site in an email, always use the _url, not _path methods. The _url methods include the host name and the _path methods don't.
# bad
You can always find more info about this course
<%= link_to 'here', course_path(@course) %>

# good
You can always find more info about this course
<%= link_to 'here', course_url(@course) %>

Routing

  • When you need to add more actions to a RESTful resource (do you really need them at all?) use member and collection routes.
# bad
get 'subscriptions/:id/unsubscribe'
resources :subscriptions

# good
resources :subscriptions do
  get 'unsubscribe', on: :member
end

# good
resources :subscriptions do
  member do
    get 'unsubscribe'
    # more routes
  end
end

# bad
get 'photos/search'
resources :photos

# good
resources :photos do
  get 'search', on: :collection
end

# good
resources :photos do
  collection do
    get 'search'
    # more routes
  end
end
  • Use nested routes to express better the relationship between ActiveRecord models. [link]
class Post < ActiveRecord::Base
  has_many :comments
end

class Comments < ActiveRecord::Base
  belongs_to :post
end

# routes.rb
resources :posts do
  resources :comments
end
  • Use namespaced routes to group related actions
namespace :admin do
  # Directs /admin/products/* to Admin::ProductsController
  # (app/controllers/admin/products_controller.rb)
  resources :products
end
  • Never use the legacy wild controller route. This route will make all actions in every controller accessible via GET requests.
# very bad
match ':controller(/:action(/:id(.:format)))'
  • Don't use match to define any routes unless there is need to map multiple request types among [:get, :post, :patch, :put, :delete] to a single action using :via option.

Time

  • Config your timezone accordingly in application.rb.
config.time_zone = 'Eastern European Time'
# optional - note it can be only :utc or :local (default is :utc)
config.active_record.default_timezone = :local
  • Don't use Time.parse.
# bad
Time.parse('2015-03-02 19:05:37') # => Will assume time string given is in the system's time zone.

# good
Time.zone.parse('2015-03-02 19:05:37') # => Mon, 02 Mar 2015 19:05:37 EET +02:00
  • Don't use Time.now.
# bad
Time.now # => Returns system time and ignores your configured time zone.

# good
Time.zone.now # => Fri, 12 Mar 2014 22:04:47 EET +02:00
Time.current # Same thing but shorter.

Thanks ,

Reference https://github.com/bbatsov/rails-style-guide
pdkproitf 20-06-2017

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

pdkproitf

3 bài viết.
0 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
1 0
This article will discuss about the basic styles are used common on Rails include: How to use Ruby convention on Rails and Rails convention. Why c...
pdkproitf viết hơn 1 năm trước
1 0
White
1 0
This article will discuss about the difference between Rails 4 and Rails 5, what new in Rails 5. Before come to the details let take a quick look ...
pdkproitf viết hơn 1 năm trước
1 0
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
3 bài viết.
0 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!