• 自由地引入不是 ActiveRecord 的模型类。

  • 模型的命名应有意义(但简短)且不含缩写。

  • 如果需要模型类有与 ActiveRecord 类似的行为(如验证),但又不想有 ActiveRecord 的数据库功能,应使用 ActiveAttr 这个 gem。

    1. class Message
    2. include ActiveAttr::Model
    3. attribute :name
    4. attribute :email
    5. attribute :content
    6. attribute :priority
    7. attr_accessible :name, :email, :content
    8. validates_presence_of :name
    9. validates_format_of :email, :with => /\A[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}\z/i
    10. validates_length_of :content, :maximum => 500
    11. end

    更完整的示例请参考 RailsCast on the subject。

ActiveRecord

  • 避免改动缺省的 ActiveRecord 惯例(表的名字、主键等),除非你有一个充分的理由(比如,不受你控制的数据库)。

    1. # 差 - 如果你能更改数据库的 schema,那就不要这样写
    2. class Transaction < ActiveRecord::Base
    3. self.table_name = 'order'
    4. ...
    5. end
  • 把宏风格的方法调用(has_many, validates 等)放在类定义语句的最前面。

    1. class User < ActiveRecord::Base
    2. # 默认的 scope 放在最前(如果有的话)
    3. default_scope { where(active: true) }
    4. # 接下来是常量初始化
    5. COLORS = %w(red green blue)
    6. # 然后是 attr 相关的宏
    7. attr_accessor :formatted_date_of_birth
    8. attr_accessible :login, :first_name, :last_name, :email, :password
    9. # 紧接着是与关联有关的宏
    10. belongs_to :country
    11. has_many :authentications, dependent: :destroy
    12. # 以及与验证有关的宏
    13. validates :email, presence: true
    14. validates :username, presence: true
    15. validates :username, uniqueness: { case_sensitive: false }
    16. validates :username, format: { with: /\A[A-Za-z][A-Za-z0-9._-]{2,19}\z/ }
    17. validates :password, format: { with: /\A\S{8,128}\z/, allow_nil: true}
    18. # 下面是回调方法
    19. before_save :cook
    20. before_save :update_username_lower
    21. # 其它的宏(如 devise)应放在回调方法之后
    22. ...
    23. end
  • has_many :through 优于 has_and_belongs_to_many。 使用 has_many :through 允许 join 模型有附加的属性及验证。

    1. # 不太好 - 使用 has_and_belongs_to_many
    2. class User < ActiveRecord::Base
    3. has_and_belongs_to_many :groups
    4. end
    5. class Group < ActiveRecord::Base
    6. has_and_belongs_to_many :users
    7. end
    8. # 更好 - 使用 has_many :through
    9. class User < ActiveRecord::Base
    10. has_many :memberships
    11. has_many :groups, through: :memberships
    12. end
    13. class Membership < ActiveRecord::Base
    14. belongs_to :user
    15. belongs_to :group
    16. end
    17. class Group < ActiveRecord::Base
    18. has_many :memberships
    19. has_many :users, through: :memberships
    20. end
  • self[:attribute]read_attribute(:attribute) 更好。

    1. # 差
    2. def amount
    3. read_attribute(:amount) * 100
    4. end
    5. # 好
    6. def amount
    7. self[:amount] * 100
    8. end
  • self[:attribute] = value 优于 write_attribute(:attribute, value)

    1. # 差
    2. def amount
    3. write_attribute(:amount, 100)
    4. end
    5. # 好
    6. def amount
    7. self[:amount] = 100
    8. end
  • 总是使用新式的 “sexy”验证。

    1. # 差
    2. validates_presence_of :email
    3. validates_length_of :email, maximum: 100
    4. # 好
    5. validates :email, presence: true, length: { maximum: 100 }
  • 当一个自定义的验证规则使用次数超过一次时,或该验证规则是基于正则表达式时,应该创建一个自定义的验证规则文件。

    1. # 差
    2. class Person
    3. validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i }
    4. end
    5. # 好
    6. class EmailValidator < ActiveModel::EachValidator
    7. def validate_each(record, attribute, value)
    8. record.errors[attribute] << (options[:message] || 'is not a valid email') unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
    9. end
    10. end
    11. class Person
    12. validates :email, email: true
    13. end
  • 自定义验证规则应放在 app/validators 目录下。

  • 如果你在维护数个相关的应用,或验证规则本身足够通用,可以考虑将自定义的验证规则抽象为一个共用的 gem。

  • 自由地使用命名 scope。

    1. class User < ActiveRecord::Base
    2. scope :active, -> { where(active: true) }
    3. scope :inactive, -> { where(active: false) }
    4. scope :with_orders, -> { joins(:orders).select('distinct(users.id)') }
    5. end
  • 当一个由 lambda 和参数定义的命名 scope 太过复杂时,
    更好的方式是创建一个具有同样用途并返回 ActiveRecord::Relation 对象的类方法。这很可能让 scope 更加精简。

    1. class User < ActiveRecord::Base
    2. def self.with_orders
    3. joins(:orders).select('distinct(users.id)')
    4. end
    5. end

    注意这种方式不允许命名 scope 那样的链式调用。例如:

    1. # 不能链式调用
    2. class User < ActiveRecord::Base
    3. def User.old
    4. where('age > ?', 80)
    5. end
    6. def User.heavy
    7. where('weight > ?', 200)
    8. end
    9. end

    这种方式下 oldheavy 可以单独工作,但不能执行 User.old.heavy
    若要链式调用,请使用下面的代码:

    1. # 可以链式调用
    2. class User < ActiveRecord::Base
    3. scope :old, -> { where('age > 60') }
    4. scope :heavy, -> { where('weight > 200') }
    5. end
  • 注意 update_attribute 方法的行为。它不运行模型验证(与update_attributes 不同),因此可能弄乱模型的状态。

  • 应使用对用户友好的 URL。URL 中应显示模型的一些具有描述性的属性,而不是仅仅显示 id。有多种方法可以达到这个目的:

    • 重写模型的 to_param 方法。Rails 使用该方法为对象创建 URL。该方法默认会以字符串形式返回记录的 id 项。
      可以重写该方法以包含其它可读性强的属性。

      1. class Person
      2. def to_param
      3. "#{id} #{name}".parameterize
      4. end
      5. end

    为了将结果转换为一个 URL 友好的值,字符串应该调用 parameterize 方法。
    对象的 id 属性值需要位于 URL 的开头,以便使用 ActiveRecord 的 find 方法查找对象。

    • 使用 friendly_id 这个 gem。它允许使用对象的一些描述性属性而非 id 来创建可读性强的 URL。

      1. class Person
      2. extend FriendlyId
      3. friendly_id :name, use: :slugged
      4. end

    查看 gem documentation 以获得更多 friendly_id 的使用信息。

  • 应使用 find_each 来迭代一系列 ActiveRecord 对象。用循环来处理数据库中的记录集(如 all 方法)是非常低效率的,因为循环试图一次性得到所有对象。而批处理方法允许一批批地处理记录,因此需要占用的内存大幅减少。

    1. # 差
    2. Person.all.each do |person|
    3. person.do_awesome_stuff
    4. end
    5. Person.where('age > 21').each do |person|
    6. person.party_all_night!
    7. end
    8. # 好
    9. Person.find_each do |person|
    10. person.do_awesome_stuff
    11. end
    12. Person.where('age > 21').find_each do |person|
    13. person.party_all_night!
    14. end
  • 因为 Rails 为有依赖关系的关联添加了回调方法,应总是调用
    before_destroy 回调方法,调用该方法并启用 prepend: true 选项会执行验证。

    1. # 差——即使 super_admin 返回 true,roles 也会自动删除
    2. has_many :roles, dependent: :destroy
    3. before_destroy :ensure_deletable
    4. def ensure_deletable
    5. fail "Cannot delete super admin." if super_admin?
    6. end
    7. # 好
    8. has_many :roles, dependent: :destroy
    9. before_destroy :ensure_deletable, prepend: true
    10. def ensure_deletable
    11. fail "Cannot delete super admin." if super_admin?
    12. end

ActiveRecord 查询

  • 不要在查询中使用字符串插值,它会使你的代码有被 SQL 注入攻击的风险。

    1. # 差——插值的参数不会被转义
    2. Client.where("orders_count = #{params[:orders]}")
    3. # 好——参数会被适当转义
    4. Client.where('orders_count = ?', params[:orders])
  • 当查询中有超过 1 个占位符时,应考虑使用名称占位符,而非位置占位符。

    1. # 一般般
    2. Client.where(
    3. 'created_at >= ? AND created_at <= ?',
    4. params[:start_date], params[:end_date]
    5. )
    6. # 好
    7. Client.where(
    8. 'created_at >= :start_date AND created_at <= :end_date',
    9. start_date: params[:start_date], end_date: params[:end_date]
    10. )
  • 当只需要通过 id 查询单个记录时,优先使用 find 而不是 where

    1. # 差
    2. User.where(id: id).take
    3. # 好
    4. User.find(id)
  • 当只需要通过属性查询单个记录时,优先使用 find_by 而不是 where

    1. # 差
    2. User.where(first_name: 'Bruce', last_name: 'Wayne').first
    3. # 好
    4. User.find_by(first_name: 'Bruce', last_name: 'Wayne')
  • 当需要处理多条记录时,应使用 find_each

    1. # 差——一次性加载所有记录
    2. # 当 users 表有成千上万条记录时,非常低效
    3. User.all.each do |user|
    4. NewsMailer.weekly(user).deliver_now
    5. end
    6. # 好——分批检索记录
    7. User.find_each do |user|
    8. NewsMailer.weekly(user).deliver_now
    9. end
  • where.not 比书写 SQL 更好。

    1. # 差
    2. User.where("id != ?", id)
    3. # 好
    4. User.where.not(id: id)