自由地引入不是 ActiveRecord 的模型类。
模型的命名应有意义(但简短)且不含缩写。
如果需要模型类有与 ActiveRecord 类似的行为(如验证),但又不想有 ActiveRecord 的数据库功能,应使用 ActiveAttr 这个 gem。
class Messageinclude ActiveAttr::Modelattribute :nameattribute :emailattribute :contentattribute :priorityattr_accessible :name, :email, :contentvalidates_presence_of :namevalidates_format_of :email, :with => /\A[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}\z/ivalidates_length_of :content, :maximum => 500end
更完整的示例请参考 RailsCast on the subject。
ActiveRecord
避免改动缺省的 ActiveRecord 惯例(表的名字、主键等),除非你有一个充分的理由(比如,不受你控制的数据库)。
# 差 - 如果你能更改数据库的 schema,那就不要这样写class Transaction < ActiveRecord::Baseself.table_name = 'order'...end
把宏风格的方法调用(
has_many,validates等)放在类定义语句的最前面。class User < ActiveRecord::Base# 默认的 scope 放在最前(如果有的话)default_scope { where(active: true) }# 接下来是常量初始化COLORS = %w(red green blue)# 然后是 attr 相关的宏attr_accessor :formatted_date_of_birthattr_accessible :login, :first_name, :last_name, :email, :password# 紧接着是与关联有关的宏belongs_to :countryhas_many :authentications, dependent: :destroy# 以及与验证有关的宏validates :email, presence: truevalidates :username, presence: truevalidates :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}# 下面是回调方法before_save :cookbefore_save :update_username_lower# 其它的宏(如 devise)应放在回调方法之后...end
has_many :through优于has_and_belongs_to_many。 使用has_many :through允许 join 模型有附加的属性及验证。# 不太好 - 使用 has_and_belongs_to_manyclass User < ActiveRecord::Basehas_and_belongs_to_many :groupsendclass Group < ActiveRecord::Basehas_and_belongs_to_many :usersend# 更好 - 使用 has_many :throughclass User < ActiveRecord::Basehas_many :membershipshas_many :groups, through: :membershipsendclass Membership < ActiveRecord::Basebelongs_to :userbelongs_to :groupendclass Group < ActiveRecord::Basehas_many :membershipshas_many :users, through: :membershipsend
self[:attribute]比read_attribute(:attribute)更好。# 差def amountread_attribute(:amount) * 100end# 好def amountself[:amount] * 100end
self[:attribute] = value优于write_attribute(:attribute, value)。# 差def amountwrite_attribute(:amount, 100)end# 好def amountself[:amount] = 100end
总是使用新式的 “sexy”验证。
# 差validates_presence_of :emailvalidates_length_of :email, maximum: 100# 好validates :email, presence: true, length: { maximum: 100 }
当一个自定义的验证规则使用次数超过一次时,或该验证规则是基于正则表达式时,应该创建一个自定义的验证规则文件。
# 差class Personvalidates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i }end# 好class EmailValidator < ActiveModel::EachValidatordef 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/iendendclass Personvalidates :email, email: trueend
自定义验证规则应放在
app/validators目录下。如果你在维护数个相关的应用,或验证规则本身足够通用,可以考虑将自定义的验证规则抽象为一个共用的 gem。
自由地使用命名 scope。
class User < ActiveRecord::Basescope :active, -> { where(active: true) }scope :inactive, -> { where(active: false) }scope :with_orders, -> { joins(:orders).select('distinct(users.id)') }end
当一个由 lambda 和参数定义的命名 scope 太过复杂时,
更好的方式是创建一个具有同样用途并返回ActiveRecord::Relation对象的类方法。这很可能让 scope 更加精简。class User < ActiveRecord::Basedef self.with_ordersjoins(:orders).select('distinct(users.id)')endend
注意这种方式不允许命名 scope 那样的链式调用。例如:
# 不能链式调用class User < ActiveRecord::Basedef User.oldwhere('age > ?', 80)enddef User.heavywhere('weight > ?', 200)endend
这种方式下
old和heavy可以单独工作,但不能执行User.old.heavy。
若要链式调用,请使用下面的代码:# 可以链式调用class User < ActiveRecord::Basescope :old, -> { where('age > 60') }scope :heavy, -> { where('weight > 200') }end
注意
update_attribute方法的行为。它不运行模型验证(与update_attributes不同),因此可能弄乱模型的状态。应使用对用户友好的 URL。URL 中应显示模型的一些具有描述性的属性,而不是仅仅显示
id。有多种方法可以达到这个目的:重写模型的
to_param方法。Rails 使用该方法为对象创建 URL。该方法默认会以字符串形式返回记录的id项。
可以重写该方法以包含其它可读性强的属性。class Persondef to_param"#{id} #{name}".parameterizeendend
为了将结果转换为一个 URL 友好的值,字符串应该调用
parameterize方法。
对象的id属性值需要位于 URL 的开头,以便使用 ActiveRecord 的find方法查找对象。使用
friendly_id这个 gem。它允许使用对象的一些描述性属性而非id来创建可读性强的 URL。class Personextend FriendlyIdfriendly_id :name, use: :sluggedend
查看 gem documentation 以获得更多
friendly_id的使用信息。应使用
find_each来迭代一系列 ActiveRecord 对象。用循环来处理数据库中的记录集(如all方法)是非常低效率的,因为循环试图一次性得到所有对象。而批处理方法允许一批批地处理记录,因此需要占用的内存大幅减少。# 差Person.all.each do |person|person.do_awesome_stuffendPerson.where('age > 21').each do |person|person.party_all_night!end# 好Person.find_each do |person|person.do_awesome_stuffendPerson.where('age > 21').find_each do |person|person.party_all_night!end
因为 Rails 为有依赖关系的关联添加了回调方法,应总是调用
before_destroy回调方法,调用该方法并启用prepend: true选项会执行验证。# 差——即使 super_admin 返回 true,roles 也会自动删除has_many :roles, dependent: :destroybefore_destroy :ensure_deletabledef ensure_deletablefail "Cannot delete super admin." if super_admin?end# 好has_many :roles, dependent: :destroybefore_destroy :ensure_deletable, prepend: truedef ensure_deletablefail "Cannot delete super admin." if super_admin?end
ActiveRecord 查询
不要在查询中使用字符串插值,它会使你的代码有被 SQL 注入攻击的风险。
# 差——插值的参数不会被转义Client.where("orders_count = #{params[:orders]}")# 好——参数会被适当转义Client.where('orders_count = ?', params[:orders])
当查询中有超过 1 个占位符时,应考虑使用名称占位符,而非位置占位符。
# 一般般Client.where('created_at >= ? AND created_at <= ?',params[:start_date], params[:end_date])# 好Client.where('created_at >= :start_date AND created_at <= :end_date',start_date: params[:start_date], end_date: params[:end_date])
当只需要通过 id 查询单个记录时,优先使用
find而不是where。# 差User.where(id: id).take# 好User.find(id)
当只需要通过属性查询单个记录时,优先使用
find_by而不是where。# 差User.where(first_name: 'Bruce', last_name: 'Wayne').first# 好User.find_by(first_name: 'Bruce', last_name: 'Wayne')
当需要处理多条记录时,应使用
find_each。# 差——一次性加载所有记录# 当 users 表有成千上万条记录时,非常低效User.all.each do |user|NewsMailer.weekly(user).deliver_nowend# 好——分批检索记录User.find_each do |user|NewsMailer.weekly(user).deliver_nowend
where.not比书写 SQL 更好。# 差User.where("id != ?", id)# 好User.where.not(id: id)
