Active Record验证
-
什么是ORM?
-
什么是Active Record?
-
Active Record如何使用?
-
Active Record模型的命名约定?
-
如何使用Active Record模型处理保存在关系型数据库中的数据?
1.什么是ORM?
ORM(对象关系映射):一种技术手段,把应用中的对象和关系型数据库中的数据表联系起来。应用中对象的属性和对象之间的关系可以通过一种简单的方法从数据库中获取,无需直接编写SQL语句,也不过度依赖特定的数据库种类。
2.什么是Active Record?
Active Record 是 MVC中的 M(模型),负责处理数据和业务逻辑。Active Record 负责创建和使用需要持久存入数据库中的数据。Active Record 实现了 Active Record 模式,是一种对象关系映射系统。
-
Active Record如何使用?
创建 Active Record 模型的过程很简单,只要继承 ApplicationRecord
类就行了:
class Product < ApplicationRecord end
-
Active Record模型的命名约定?
默认命名约定:默认情况下,Active Record 使用一些命名约定,查找模型和数据库表之间的映射关系。Rails 把模型的类名转换成复数,然后查找对应的数据表。例如,模型类名为 Book
,数据表就是 books
。
覆盖命名约定:ApplicationRecord
继承自 ActiveRecord::Base
,后者定义了一系列有用的方法。使用 ActiveRecord::Base.table_name=
方法可以指定要使用的表名:
class Product < ApplicationRecord
self.table_name = "my_products"
end
还可以使用 ActiveRecord::Base.primary_key=
方法指定表的主键:
class Product < ApplicationRecord
self.primary_key = "product_id"
end
如果这么做,还要调用 set_fixture_class
方法,手动指定固件(my_products.yml)的类名:
class ProductTest < ActiveSupport::TestCase
set_fixture_class my_products: Product
fixtures :my_products
...
end
模式约定:
根据字段的作用不同,Active Record 对数据库表中的字段命名也做了相应的约定:
-
外键:使用
singularized_table_name_id
形式命名,例如item_id
,order_id
。创建模型关联后,Active Record 会查找这个字段; -
主键:默认情况下,Active Record 使用整数字段
id
作为表的主键。使用 Active Record 迁移创建数据库表时,会自动创建这个字段;
还有一些可选的字段,能为 Active Record 实例添加更多的功能:
created_at:创建记录时,自动设为当前的日期和时间;
updated_at:更新记录时,自动设为当前的日期和时间;
lock_version:在模型中添加乐观锁;
type:让模型使用单表继承;
(association_name)_type:存储多态关联的类型;
(table_name)_count:缓存所关联对象的数量。比如说,一个 Article 有多个 Comment,那么 comments_count 列存储各篇文章现有的评论数量;
-
如何使用Active Record模型处理保存在关系型数据库中的数据?
CRUD(读写数据):
CURD 是四种数据操作的简称:C 表示创建,R 表示读取,U 表示更新,D 表示删除。Active Record 自动创建了处理数据表中数据的方法。
创建:
Active Record 对象可以使用散列创建,在块中创建,或者创建后手动设置属性。new
方法创建一个新对象,create
方法创建新对象,并将其存入数据库。
create
方法会创建一个新记录,并将其存入数据库:
user = User.create(name: "David", occupation: "Code Artist")
new
方法实例化一个新对象,但不保存:
user = User.new
user.name = "David"
user.occupation = "Code Artist"
调用 user.save
可以把记录存入数据库。
如果在 create
和 new
方法中使用块,会把新创建的对象拉入块中,初始化对象:
user = User.new do |u|
u.name = "David"
u.occupation = "Code Artist"
end
读取:
# 返回所有用户组成的集合
users = User.all
?
# 返回第一个用户
user = User.first
?
# 返回第一个名为 David 的用户
david = User.find_by(name: 'David')
?
# 查找所有名为 David,职业为 Code Artists 的用户,而且按照 created_at 反向排列
users = User.where(name: 'David', occupation: 'Code Artist').order(created_at: :desc)
更新:
一种是先查出来更改属性再存入数据库
user = User.find_by(name: 'David')
user.name = 'Dave'
user.save
另一种是使用散列的形式直接更新
user = User.find_by(name: 'David')
user.update(name: 'Dave')
一次更新多个属性时使用这种方法最方便。如果想批量更新多个记录,可以使用类方法 update_all
:
User.update_all "max_login_attempts = 3, must_change_password = 'true'"
删除:
user = User.find_by(name: 'David')
user.destroy
数据验证
create
create!
save
save!
update
update!
decrement!
decrement_counter
increment!
increment_counter
toggle!
touch
update_all
update_attribute
update_column
update_columns
update_counters
save(validate: false)
class Person < ApplicationRecord
validates :name, presence: true
end
Person.create(name: "John Doe").valid? # => true
Person.create(name: nil).valid? # => false
?
class Person < ApplicationRecord
validates :name, presence: true
end
>> p = Person.new
# => #
>> p.errors.messages
# => {}
>> p.valid?
# => false
>> p.errors.messages
# => {name:["can't be blank"]}
>> p = Person.create
# => #
>> p.errors.messages
# => {name:["can't be blank"]}
>> p.save
# => false
>> p.save!
# => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
>> Person.create!
# => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
?
class Person < ApplicationRecord
validates :name, presence: true
end
>> Person.new.errors[:name].any? # => false
>> Person.create.errors[:name].any? # => true
class Person < ApplicationRecord
validates :name, presence: true
end
>> person = Person.new
>> person.valid?
>> person.errors.details[:name] # => [{error: :blank}]
1.如何使用内置的数据验证辅助方法。
辅助方法:
class Person < ApplicationRecord
validates :terms_of_service, acceptance: true
end
class Person < ApplicationRecord
validates :terms_of_service, acceptance: { accept: 'yes' }
validates :eula, acceptance: { accept: ['TRUE', 'accepted'] }
end
class Library < ApplicationRecord
has_many :books
validates_associated :books
end
class Person < ApplicationRecord
validates :email, confirmation: true
end
?
在视图模板中可以这么写:
<%= text_field :person, :email %>
<%= text_field :person, :email_confirmation %>
?
只有 email_confirmation 的值不是 nil 时才会检查。所以要为确认属性加上存在性验证(后文会介绍 presence 验证)
class Person < ApplicationRecord
validates :email, confirmation: true
validates :email_confirmation, presence: true
end
?
此外,还可以使用 :case_sensitive 选项指定确认时是否区分大小写。这个选项的默认值是 true
class Person < ApplicationRecord
validates :email, confirmation: { case_sensitive: false }
end
class Account < ApplicationRecord
validates :subdomain, exclusion: { in: %w(www us ca jp),
message: "%{value} is reserved." }
end
class Coffee < ApplicationRecord
validates :size, inclusion: { in: %w(small medium large),
message: "%{value} is not a valid size" }
end
class Product < ApplicationRecord
validates :legacy_code, format: { with: /\A[a-zA-Z]+\z/,
message: "only allows letters" }
end
?
或者,使用 :without 选项,指定属性的值不能匹配正则表达式
class Person < ApplicationRecord
validates :name, length: { minimum: 2 }
validates :bio, length: { maximum: 500 }
validates :password, length: { in: 6..20 }
validates :registration_number, length: { is: 6 }
end
?
可用的长度约束选项有:
:minimum:属性的值不能比指定的长度短;
:maximum:属性的值不能比指定的长度长;
:in(或 :within):属性值的长度在指定的范围内。该选项的值必须是一个范围;
:is:属性值的长度必须等于指定值;
/\A[+-]?\d+\z/
class Player < ApplicationRecord
validates :points, numericality: true
validates :games_played, numericality: { only_integer: true }
end
:greater_than:属性值必须比指定的值大。该选项默认的错误消息是“must be greater than %{count}”;
:greater_than_or_equal_to:属性值必须大于或等于指定的值。该选项默认的错误消息是“must be greater than or equal to %{count}”;
:equal_to:属性值必须等于指定的值。该选项默认的错误消息是“must be equal to %{count}”;
:less_than:属性值必须比指定的值小。该选项默认的错误消息是“must be less than %{count}”;
:less_than_or_equal_to:属性值必须小于或等于指定的值。该选项默认的错误消息是“must be less than or equal to %{count}”;
:other_than:属性值必须与指定的值不同。该选项默认的错误消息是“must be other than %{count}”。
:odd:如果设为 true,属性值必须是奇数。该选项默认的错误消息是“must be odd”;
:even:如果设为 true,属性值必须是偶数。该选项默认的错误消息是“must be even”;
?
numericality 默认不接受 nil 值。可以使用 allow_nil: true 选项允许接受 nil
class Person < ApplicationRecord
validates :name, :login, :email, presence: true
end
validates :boolean_field_name, inclusion: { in: [true, false] }
validates :boolean_field_name, exclusion: { in: [nil] }
class Person < ApplicationRecord
validates :name, :login, :email, absence: true
end
class Account < ApplicationRecord
validates :email, uniqueness: true
end
?
注意,不管怎样设置,有些数据库查询时始终不区分大小写
class Person < ApplicationRecord
validates :name, uniqueness: { case_sensitive: false }
end
class GoodnessValidator < ActiveModel::Validator
def validate(record)
if record.first_name == "Evil"
record.errors[:base] << "This person is evil"
end
end
end
class Person < ApplicationRecord
validates_with GoodnessValidator
end
?
record.errors[:base] 中的错误针对整个对象,而不是特定的属性
validates_with 方法的参数是一个类或一组类,用来做验证。validates_with 方法没有默认的错误消息。在做验证的类中要手动把错误添加到记录的错误集合中
class GoodnessValidator < ActiveModel::Validator
def validate(record)
if options[:fields].any?{|field| record.send(field) == "Evil" }
record.errors[:base] << "This person is evil"
end
end
end
class Person < ApplicationRecord
validates_with GoodnessValidator, fields: [:first_name, :last_name]
end
?
注意,做验证的类在整个应用的生命周期内只会初始化一次,而不是每次验证时都初始化,所以使用实例变量时要特别小心
?
class Person < ApplicationRecord
validate do |person|
GoodnessValidator.new(person).validate
end
end
class GoodnessValidator
def initialize(person)
@person = person
end
def validate
if some_complex_condition_involving_ivars_and_private_methods?
@person.errors[:base] << "This person is evil"
end
end
# ...
end
class Person < ApplicationRecord
validates_each :name, :surname do |record, attr, value|
record.errors.add(attr, 'must start with upper case') if value =~ /\A[[:lower:]]/
end
end
常用的验证选项
class Coffee < ApplicationRecord
validates :size, inclusion: { in: %w(small medium large),
message: "%{value} is not a valid size" }, allow_nil: true
end
class Topic < ApplicationRecord
validates :title, length: { is: 5 }, allow_blank: true
end
Topic.create(title: "").valid? # => true
Topic.create(title: nil).valid? # => true
class Person < ApplicationRecord
# 直接写消息
validates :name, presence: { message: "must be given please" }
# 带有动态属性值的消息。%{value} 会被替换成属性的值
# 此外还可以使用 %{attribute} 和 %{model}
validates :age, numericality: { message: "%{value} seems wrong" }
# Proc
validates :username,
uniqueness: {
# object = 要验证的 person 对象
# data = { model: "Person", attribute: "Username", value: }
message: ->(object, data) do
"Hey #{object.name}!, #{data[:value]} is taken already! Try again #{Time.zone.tomorrow}"
end
}
end
class Person < ApplicationRecord
# 更新时允许电子邮件地址重复
validates :email, uniqueness: true, on: :create
# 创建记录时允许年龄不是数字
validates :age, numericality: true, on: :update
# 默认行为(创建和更新时都验证)
validates :name, presence: true
end
严格验证
class Person < ApplicationRecord
validates :name, presence: { strict: true }
end
Person.new.valid? # => ActiveModel::StrictValidationFailed: Name can't be blank
class Person < ApplicationRecord
validates :token, presence: true, uniqueness: true, strict: TokenGenerationException
end
Person.new.valid? # => TokenGenerationException: Token can't be blank
条件验证
class Order < ApplicationRecord
validates :card_number, presence: true, if: :paid_with_card?
def paid_with_card?
payment_type == "card"
end
end
class Account < ApplicationRecord
validates :password, confirmation: true,
unless: Proc.new { |a| a.password.blank? }
end
class User < ApplicationRecord
with_options if: :is_admin? do |admin|
admin.validates :password, length: { minimum: 10 }
admin.validates :email, presence: true
end
end
?
with_options 代码块中的所有验证都会使用 if: :is_admin? 这个条件
class Computer < ApplicationRecord
validates :mouse, presence: true,
if: ["market.retail?", :desktop?],
unless: Proc.new { |c| c.trackpad.present? }
end
?
只有当 :if 选项的所有条件都返回 true,且 :unless 选项中的条件返回 false 时才会做验证
2.如何使用自定义数据验证方法
自定义验证类
class MyValidator < ActiveModel::Validator
def validate(record)
unless record.name.starts_with? 'X'
record.errors[:name] << 'Need a name starting with X please!'
end
end
end
class Person
include ActiveModel::Validations
validates_with MyValidator
end
lass EmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
record.errors[attribute] << (options[:message] || "is not an email")
end
end
end
class Person < ApplicationRecord
validates :email, presence: true, email: true
end
自定义验证方法
class Invoice < ApplicationRecord
validate :expiration_date_cannot_be_in_the_past,
:discount_cannot_be_greater_than_total_value
def expiration_date_cannot_be_in_the_past
if expiration_date.present? && expiration_date < Date.today
errors.add(:expiration_date, "can't be in the past")
end
end
def discount_cannot_be_greater_than_total_value
if discount > total_value
errors.add(:discount, "can't be greater than total value")
end
end
end
class Invoice < ApplicationRecord
validate :active_customer, on: :create
def active_customer
errors.add(:customer_id, "is not active") unless customer.active?
end
end
处理验证错误
class Person < ApplicationRecord
validates :name, presence: true, length: { minimum: 3 }
end
person = Person.new
person.valid? # => false
person.errors.messages
# => {:name=>["can't be blank", "is too short (minimum is 3 characters)"]}
person = Person.new(name: "John Doe")
person.valid? # => true
person.errors.messages # => {}
class Person < ApplicationRecord
validates :name, presence: true, length: { minimum: 3 }
end
person = Person.new(name: "John Doe")
person.valid? # => true
person.errors[:name] # => []
person = Person.new(name: "JD")
person.valid? # => false
person.errors[:name] # => ["is too short (minimum is 3 characters)"]
person = Person.new
person.valid? # => false
person.errors[:name]
# => ["can't be blank", "is too short (minimum is 3 characters)"]
class Person < ApplicationRecord
def a_method_used_for_validation_purposes
errors.add(:name, "cannot contain the characters !@#%*()_-+=")
end
end
person = Person.create(name: "!@#")
person.errors[:name]
# => ["cannot contain the characters !@#%*()_-+="]
person.errors.full_messages
# => ["Name cannot contain the characters !@#%*()_-+="]
?
class Person < ApplicationRecord
def a_method_used_for_validation_purposes
errors.messages[:name] << "cannot contain the characters !@#%*()_-+="
end
end
person = Person.create(name: "!@#")
person.errors[:name]
# => ["cannot contain the characters !@#%*()_-+="]
person.errors.to_a
# => ["Name cannot contain the characters !@#%*()_-+="]
class Person < ApplicationRecord
def a_method_used_for_validation_purposes
errors.add(:name, :invalid_characters)
end
end
person = Person.create(name: "!@#")
person.errors.details[:name]
# => [{error: :invalid_characters}]
class Person < ApplicationRecord def a_method_used_for_validation_purposes errors.add(:name, :invalid_characters, not_allowed: "!@#%*()_-+=") end end person = Person.create(name: "!@#") person.errors.details[:name] # => [{error: :invalid_characters, not_allowed: "!@#%*()_-+="}]
class Person < ApplicationRecord def a_method_used_for_validation_purposes errors[:base] << "This person is invalid because ..." end end
class Person < ApplicationRecord validates :name, presence: true, length: { minimum: 3 } end person = Person.new person.valid? # => false person.errors[:name] # => ["can't be blank", "is too short (minimum is 3 characters)"] person.errors.clear person.errors.empty? # => true person.save # => false person.errors[:name] # => ["can't be blank", "is too short (minimum is 3 characters)"]
class Person < ApplicationRecord validates :name, presence: true, length: { minimum: 3 } end person = Person.new person.valid? # => false person.errors.size # => 2 person = Person.new(name: "Andrea", email: "andrea@example.com") person.valid? # => true person.errors.size # => 0
在视图中显示验证错误
<% if @article.errors.any? %><% end %><%= pluralize(@article.errors.count, "error") %> prohibited this article from being saved:
<% @article.errors.full_messages.each do |msg| %>
- <%= msg %>
<% end %>
.field_with_errors { padding: 2px; background-color: red; display: table; }