## 模型
* <a name="model-classes"></a>
自由地引入不是 ActiveRecord 的模型类。
<sup>[[link](#model-classes)]</sup>
* <a name="meaningful-model-names"></a>
模型的命名应有意义(但简短)且不含缩写。
<sup>[[link](#meaningful-model-names)]</sup>
* <a name="activeattr-gem"></a>
如果需要模型类有与 ActiveRecord 类似的行为(如验证),但又不想有 ActiveRecord 的数据库功能,应使用 [ActiveAttr](https://github.com/cgriego/active_attr) 这个 gem。
<sup>[[link](#activeattr-gem)]</sup>
```Ruby
class Message
include ActiveAttr::Model
attribute :name
attribute :email
attribute :content
attribute :priority
attr_accessible :name, :email, :content
validates_presence_of :name
validates_format_of :email, :with => /\A[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}\z/i
validates_length_of :content, :maximum => 500
end
```
更完整的示例请参考 [RailsCast on the subject](http://railscasts.com/episodes/326-activeattr)。
### ActiveRecord
* <a name="keep-ar-defaults"></a>
避免改动缺省的 ActiveRecord 惯例(表的名字、主键等),除非你有一个充分的理由(比如,不受你控制的数据库)。
<sup>[[link](#keep-ar-defaults)]</sup>
```Ruby
# 差 - 如果你能更改数据库的 schema,那就不要这样写
class Transaction < ActiveRecord::Base
self.table_name = 'order'
...
end
```
* <a name="macro-style-methods"></a>
把宏风格的方法调用(`has_many`, `validates` 等)放在类定义语句的最前面。
<sup>[[link](#macro-style-methods)]</sup>
```Ruby
class User < ActiveRecord::Base
# 默认的 scope 放在最前(如果有的话)
default_scope { where(active: true) }
# 接下来是常量初始化
COLORS = %w(red green blue)
# 然后是 attr 相关的宏
attr_accessor :formatted_date_of_birth
attr_accessible :login, :first_name, :last_name, :email, :password
# 紧接着是与关联有关的宏
belongs_to :country
has_many :authentications, dependent: :destroy
# 以及与验证有关的宏
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}
# 下面是回调方法
before_save :cook
before_save :update_username_lower
# 其它的宏(如 devise)应放在回调方法之后
...
end
```
* <a name="has-many-through"></a>
`has_many :through` 优于 `has_and_belongs_to_many`。 使用 `has_many :through` 允许 join 模型有附加的属性及验证。
<sup>[[link](#has-many-through)]</sup>
```Ruby
# 不太好 - 使用 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
# 更好 - 使用 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
```
* <a name="read-attribute"></a>
`self[:attribute]` 比 `read_attribute(:attribute)` 更好。
<sup>[[link](#read-attribute)]</sup>
```Ruby
# 差
def amount
read_attribute(:amount) * 100
end
# 好
def amount
self[:amount] * 100
end
```
* <a name="write-attribute"></a>
`self[:attribute] = value` 优于 `write_attribute(:attribute, value)`。
<sup>[[link](#write-attribute)]</sup>
```Ruby
# 差
def amount
write_attribute(:amount, 100)
end
# 好
def amount
self[:amount] = 100
end
```
* <a name="sexy-validations"></a>
总是使用新式的 ["sexy"
验证](http://thelucid.com/2010/01/08/sexy-validation-in-edge-rails-rails-3/)。
<sup>[[link](#sexy-validations)]</sup>
```Ruby
# 差
validates_presence_of :email
validates_length_of :email, maximum: 100
# 好
validates :email, presence: true, length: { maximum: 100 }
```
* <a name="custom-validator-file"></a>
当一个自定义的验证规则使用次数超过一次时,或该验证规则是基于正则表达式时,应该创建一个自定义的验证规则文件。
<sup>[[link](#custom-validator-file)]</sup>
```Ruby
# 差
class Person
validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i }
end
# 好
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
```
* <a name="app-validators"></a>
自定义验证规则应放在 `app/validators` 目录下。
<sup>[[link](#app-validators)]</sup>
* <a name="custom-validators-gem"></a>
如果你在维护数个相关的应用,或验证规则本身足够通用,可以考虑将自定义的验证规则抽象为一个共用的 gem。
<sup>[[link](#custom-validators-gem)]</sup>
* <a name="named-scopes"></a>
自由地使用命名 scope。
<sup>[[link](#named-scopes)]</sup>
```Ruby
class User < ActiveRecord::Base
scope :active, -> { where(active: true) }
scope :inactive, -> { where(active: false) }
scope :with_orders, -> { joins(:orders).select('distinct(users.id)') }
end
```
* <a name="named-scope-class"></a>
当一个由 lambda 和参数定义的命名 scope 太过复杂时,
更好的方式是创建一个具有同样用途并返回 `ActiveRecord::Relation` 对象的类方法。这很可能让 scope 更加精简。
<sup>[[link](#named-scope-class)]</sup>
```Ruby
class User < ActiveRecord::Base
def self.with_orders
joins(:orders).select('distinct(users.id)')
end
end
```
注意这种方式不允许命名 socpe 那样的链式调用。例如:
```Ruby
# 不能链式调用
class User < ActiveRecord::Base
def User.old
where('age > ?', 80)
end
def User.heavy
where('weight > ?', 200)
end
end
```
这种方式下 `old` 和 `heavy` 可以单独工作,但不能执行 `User.old.heavy`。
若要链式调用,请使用下面的代码:
```Ruby
# 可以链式调用
class User < ActiveRecord::Base
scope :old, -> { where('age > 60') }
scope :heavy, -> { where('weight > 200') }
end
```
* <a name="beware-update-attribute"></a>
注意
[`update_attribute`](http://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update_attribute)
方法的行为。它不运行模型验证(与 `update_attributes` 不同),因此可能弄乱模型的状态。
<sup>[[link](#beware-update-attribute)]</sup>
* <a name="user-friendly-urls"></a>
应使用对用户友好的 URL。URL 中应显示模型的一些具有描述性的属性,而不是仅仅显示 `id`。有多种方法可以达到这个目的:
<sup>[[link](#user-friendly-urls)]</sup>
* 重写模型的 `to_param` 方法。Rails 使用该方法为对象创建 URL。该方法默认会以字符串形式返回记录的 `id` 项。
可以重写该方法以包含其它可读性强的属性。
```Ruby
class Person
def to_param
"#{id} #{name}".parameterize
end
end
```
为了将结果转换为一个 URL 友好的值,字符串应该调用 `parameterize` 方法。
对象的 `id` 属性值需要位于 URL 的开头,以便使用 ActiveRecord 的 `find` 方法查找对象。
* 使用 `friendly_id` 这个 gem。它允许使用对象的一些描述性属性而非 `id` 来创建可读性强的 URL。
```Ruby
class Person
extend FriendlyId
friendly_id :name, use: :slugged
end
```
查看 [gem documentation](https://github.com/norman/friendly_id) 以获得更多 `friendly_id` 的使用信息。
* <a name="find-each"></a>
应使用 `find_each` 来迭代一系列 ActiveRecord 对象。用循环来处理数据库中的记录集(如 `all` 方法)是非常低效率的,因为循环试图一次性得到所有对象。而批处理方法允许一批批地处理记录,因此需要占用的内存大幅减少。
<sup>[[link](#find-each)]</sup>
```Ruby
# 差
Person.all.each do |person|
person.do_awesome_stuff
end
Person.where('age > 21').each do |person|
person.party_all_night!
end
# 好
Person.find_each do |person|
person.do_awesome_stuff
end
Person.where('age > 21').find_each do |person|
person.party_all_night!
end
```
* <a name="before_destroy"></a>
因为 [Rails 为有依赖关系的关联添加了回调方法](https://github.com/rails/rails/issues/3458),应总是调用
`before_destroy` 回调方法,调用该方法并启用 `prepend: true` 选项会执行验证。
<sup>[[link](#before_destroy)]</sup>
```Ruby
# 差——即使 super_admin 返回 true,roles 也会自动删除
has_many :roles, dependent: :destroy
before_destroy :ensure_deletable
def ensure_deletable
fail "Cannot delete super admin." if super_admin?
end
# 好
has_many :roles, dependent: :destroy
before_destroy :ensure_deletable, prepend: true
def ensure_deletable
fail "Cannot delete super admin." if super_admin?
end
```
### ActiveRecord 查询
* <a name="avoid-interpolation"></a>
不要在查询中使用字符串插值,它会使你的代码有被 SQL 注入攻击的风险。
<sup>[[link](#avoid-interpolation)]</sup>
```Ruby
# 差——插值的参数不会被转义
Client.where("orders_count = #{params[:orders]}")
# 好——参数会被适当转义
Client.where('orders_count = ?', params[:orders])
```
* <a name="named-placeholder"></a>
当查询中有超过 1 个占位符时,应考虑使用名称占位符,而非位置占位符。
<sup>[[link](#named-placeholder)]</sup>
```Ruby
# 一般般
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]
)
```
* <a name="find"></a>
当只需要通过 id 查询单个记录时,优先使用 `find` 而不是 `where`。
<sup>[[link](#find)]</sup>
```Ruby
# 差
User.where(id: id).take
# 好
User.find(id)
```
* <a name="find_by"></a>
当只需要通过属性查询单个记录时,优先使用 `find_by` 而不是 `where`。
<sup>[[link](#find_by)]</sup>
```Ruby
# 差
User.where(first_name: 'Bruce', last_name: 'Wayne').first
# 好
User.find_by(first_name: 'Bruce', last_name: 'Wayne')
```
* <a name="find_each"></a>
当需要处理多条记录时,应使用 `find_each`。
<sup>[[link](#find_each)]</sup>
```Ruby
# 差——一次性加载所有记录
# 当 users 表有成千上万条记录时,非常低效
User.all.each do |user|
NewsMailer.weekly(user).deliver_now
end
# 好——分批检索记录
User.find_each do |user|
NewsMailer.weekly(user).deliver_now
end
```
* <a name="where-not"></a>
`where.not` 比书写 SQL 更好。
<sup>[[link](#where-not)]</sup>
```Ruby
# 差
User.where("id != ?", id)
# 好
User.where.not(id: id)
```