Sequel-Model

Sequel::Model Mass Assignment

大多数的Model方法接受一个包含一系列key和value的哈希作为参数,这些方法包括:Model.new, Model.create, Model#set 和 Model#update。当把hash参数传递给这些方法时,每个key后面都会添加一个=(setter方法),如果存在setter方法且能够访问,Sequel会调用setter方法,setter的参数就是hash的value值。默认的,有两种setter方法是被限制的。第一种是类似typecast_on_assignment= 和 ==的方法,这些方法不影响列,所以不能用于mass assignment。第二种是主键的setter方法。为了使用主键的setter方法,必须为那个model调用unrestrict_primary_key:

Post.unrestrict_primary_key

默认情况下,mass assignment允许设置除了主键外的所有列,在某些时候,这会存在安全风险。Sequel有多种方式来保证mass assignment的安全性,第一种是使用set_allowed_columns:

Post.set_allowed_columns :title, :body, :category

这就明确指定了哪些方法是允许的(title=, body=, and category=),其他方法都不允许调用。在所有场景被访问的列都相同的简单应用中,这种方法很有效,但是在不同场景需要访问不同列时,这种方法就不适用了(比如,admin access vs. user access)。为了应对这种情况,可以使用set_only 或 update_only:

# user case
post.set_only(params[:post], :title, :body)
# admin case
post.set_only(params[:post], :title, :body, :deleted)

上面的例子中,在mass assignment时,对普通用户来说,只有title= 和 body=方法允许被访问;对管理员来说,title=, body=, 和 deleted=允许被访问。
默认情况下,非法访问一个setter方法时,Sequel会抛出一个Sequel::Error异常,通过如下方式,你可以设置不抛出异常:

# Global default
Sequel::Model.strict_param_setting = false
# Class level
Post.strict_param_setting = false
# Instance level
post.strict_param_setting = false

除了set_only和update_only方法,Sequel还支持set_fileds和update_fileds方法,对大多数用户来说,这些方法是mass assignment更好的选择。这两个方法需要两个参数,第一个参数是属性hash,第二个参数是包含有效字段名的数组:

post.set_fields(params[:post], [:title, :body])

set_fields/update_fields和set_only/update_only方法在实现上不同。set_only/update_only会遍历hash,检查每个尝试调用的方法是否合法;set_fields/update_fields会遍历数组,然后在hash中查找值,并调用对应的setter方法。(一个是遍历第一个参数,一个是遍历第二个参数)。
set_fields/update_fields被设计用于从输入中获取指定字段并忽略其他字段的场景,适用于HTML表单这种数据固定的情况。set_only/update_only被设计用于不知道输入中会包含哪些字段,但是又要确保某些setter方法能够被调用的场景,适用于灵活变动的API。
上面的含义是,set_only/update_only对第一个hash参数进行遍历,用户可能提供了title、body,也可能没提供;set_fields/update_fields对第二个数组参数进行遍历,这些参数在第一个hash参数中应该是要包含的,如果没包含,怎么办,看下面。
可以为set_fields/update_fields提供一个可选的hash参数,当前用于处理:missing选项。

  • 当hash是:missing=>:skip时,set_fields/update_fields跳过未提供的值,这就允许这两个方法能够像set_only/update_only一样被使用了。
  • 当hash是:missing=>:raise时,set_fields/update_fields会在第一个hash参数不包括指定值时抛出异常,而不是将其值设置为nil或者hash的默认值。这允许更严格的检查,类似默认mass assignment方法的:strict_param_checking设置。你可以使用Model.default_set_fields_options=方法在全局或为某个model的set_fields/update_fields设置默认的选项。
    在所有的mass assignment场景中,以set开头的方法仅设置属性但不保存,以update开始的方法设置属性的同时会保存。

Model校验

概述

本指南介绍如何使用Sequel::Model的校验支持。我们会解释Sequel的校验支持是如何工作的,校验是用来干什么的,以及如何使用validation_helpers 插件来为你的models增加特定类型的校验。

为什么要校验

Validations are primarily useful for associating error messages to display to the user with specific attributes on the model. 也可以用他们来保证模型实例的数据完整性,但并不推荐这么做,除非模型实例是修改数据库的唯一方式,或者你有复杂的数据完整性要求,以至无法通过数据库级别的限制来实现。

数据完整性

数据库完整性最好由数据库本身来保证。例如,如果你有一个数据列不能包含NULL,在定义数据库时可以通过指定NOT NULL来限制。如果你有一个数据列的值只能是1-10,你可以增加一个CHECK约束来保证。如果你有一个VARCHAR列,要求长度是2-255,你可以设置VARCHAR列的长度是255,并增加一个CHECK约束来保证所有的值至少包含2个字符。
不幸的是,并不是所有的场景都可以像上面说的那样解决。比如,你使用Mysql并且没有数据库配置的权限,你可能尝试将一个长度为300的字符串插入到定义为VARCHAR(255)的列,此时Mysql只是截断了字符串,并不会抛出一个错误。这种情况下,增加一个model校验来保证完整性就显得很必要了。
另外,有些场景下你的数据库完整性要求可能很难通过数据库约束来实现,特别是你面对多种数据库类型时。
最后,校验比数据库约束更好写,如果时间完整性不是非常的重要,通过校验来提供最小的数据完整性可能更好。

用法

不管你使用校验来验证数据完成性还是提供错误消息,用法都是相同的。当你尝试保存一个model实例时,在发送INSERT或UPDATE请求给数据库之前,Sequel::Model会尝试调用validate来验证。如果validate方法没有为实例添加任何错误消息,就认为其实有效的,valid?方法会返回true。如果validate想对象添加了错误信息,valid?会返回false,保存操作就会抛出Sequel::ValidationFailed(默认的异常)异常,挥着返回Nil(如果raise_on_save_failure 设置为false)。
在发送数据库请求前进行校验,Sequel尝试保证无效的对象不会被保存到数据库中。但是,如果你在数据库中不增加同样的校验,那么非法数据可能会通过其他的方式被添加到数据库中。这样就会导致很奇怪的现象,比如,你从数据库检索出一条数据,没有做任何修改,然后尝试保存,就会抛出一个异常。

跳过校验

Sequel::Model使用save方法保存model对象,这就意味着所有model对象的保存都会进行校验操作。唯一跳过校验的方式是在调用save时传递一个:validate => false选项,如果使用了该选项,save方法在保存是不会再进行校验。
注意,可能经常出现通过dataset更新数据而不是使用save的情况,校验只会在你调用save或者其他的model方法调用save时才会进行。例如,create方法实例化一个model实例,然后调用save,所以通过create能够创建一个合法的对象。但是insert类方法只是一个dataset方法,仅仅将原始的hash数据插入到数据库中,并不对数据进行校验。

valid?he validate

Sequel::Model使用valid?方法检查model实例是否合法,该方法不应该被重写,但是validate方法应该被重写用以向model中添加校验:

class Album < Sequel::Model
  def validate
    super
    errors.add(:name, 'cannot be empty') if !name || name.empty?
  end
end

Album.new.valid? # false
Album.new(:name=>'').valid? # false
Album.new(:name=>'RF').valid? # true

如果valid?方法返回false,你可以通过errors方法获取描述model错误信息的Sequel::Model::Errors实例:

a = Album.new
# => #<Album @values={}>
a.valid?
# => false
a.errors
# => {:name=>["cannot be empty"]}

Sequel::Model::Errors是Hash类的子类,所以上面返回的是一个hash。
注意在调用valid?方法前调用errors,会导致errors返回空:

Album.new.errors
# => {}

所以在调用valid?前不应该调用errors方法。
Sequel::Model::Errors有一些帮助方法用来方便的获取实例的所有错误信息或者检查指定属性的错误。后面会介绍这些方法。

validation_helpers

Sequel::Model没有提供一个校验框架,没有内置任何你可以调用的校验帮助方法。所以,Sequel附带一个叫做validation_helpers的插件来处理大多数基础的校验。所以你不需要像下面这样定义validate方法:

class Album < Sequel::Model
  def validate
    super
    errors.add(:name, 'cannot be empty') if !name || name.empty?
    errors.add(:name, 'is already taken') if name && new? && Album[:name=>name]
    errors.add(:website, 'cannot be empty') if !website || website.empty?
    errors.add(:website, 'is not a valid URL') unless website =~ /\Ahttps?:\/\//
  end
end

你可以像下面这样调用:

class Album < Sequel::Model
  plugin :validation_helpers
  def validate
    super
    validates_presence [:name, :website]
    validates_unique :name
    validates_format /\Ahttps?:\/\//, :website, :message=>'is not a valid URL'
  end
end

除了validates_unique有自己的API以外,validation_helpers定义的方法有下面两个API的其中一个:

  • (atts, opts={}): For methods such as validates_presence, which do not take an additional argument.
  • (arg, atts, opts={}): For methods such as validates_format, which take an additional argument.
    对这两个API来说,atts是一个列符号,或者一个列符号数组;opts是一个可选的hash。
    下面的方法是由validation_helpers提供的:

validates_presence

这可能是最常用的帮助方法,用于检查指定的属性不能为空;如果一个对象定义了blank?,validates_presence会调用该方法来检查对象是不是空;否则nil,空字符串或者只包含空白符的字符串都被作为空,定义了empty?并返回true的对象也会被当做空。其他的情况validates_presence都会作为non-blank。这意味着,对定义为boolean的列来说,你可以使用validates_presence来检查提供的是true或false,而不是NULL。

class Album < Sequel::Model
  def validate
    super
    validates_presence [:name, :website, :debut_album]
  end
end

validates_not_null

和validates_presence类似,但是只检查NULL/nil值,允许其他的空对象,例如空字符串或者只包含空白符的字符串。

validates_format

validates_format用来保证指定的属性值满足正则表达式的要求。 It's useful for checking that fields such as email addresses, URLs, UPC codes, ISBN codes, and the like, are in a specific format.也可用于校验字符串中只能使用指定的字符。

class Album < Sequel::Model
  def validate
    super
    validates_format /\A\d\d\d-\d-\d{7}-\d-\d\z/, :isbn
    validates_format /\A[0-9a-zA-Z:' ]+\z/, :name
  end
end

validates_exact_length, validates_min_length, validates_max_length, validates_length_range

  • validates_exact_length:属性为指定长度。
  • validates_min_length:属性最小长度。
  • validates_max_length: 属性最大长度。
  • validates_length_range:属性长度范围。
class Album < Sequel::Model
  def validate
    super
    validates_exact_length 17, :isbn
    validates_min_length 3, :name
    validates_max_length 100, :name
    validates_length_range 3..100, :name
  end
end

validates_length_range的参数是一个range或者是定义了include?方法的对象。

validates_integer, validates_numeric

validates_integer使用Kernel.Integet校验,validates_numeric使用Kernel.Float校验。如果Kernel方法抛出异常,则校验失败,其他情况下校验成功。

class Album < Sequel::Model
  def validate
    super
    validates_integer :copies_sold
    validates_numeric :replaygain
  end
end

validates_includes

用以检查属性的值是否在指定范围内,可以是一个数组,也可以是其他的任何定义了include?方法的对象。

class Album < Sequel::Model
  def validate
    super
    validates_includes [1, 2, 3, 4, 5], :rating
  end
end

validates_operator

validates_operator检查属性指定的值调用operator操作是否返回真值,一般情况下,该方法用于大小检查,比如>,>=等,但是属性值能调用的任何接受一个参数并返回真值的方法都可以作为operator。

class Album < Sequel::Model
  def validate
    super
    validates_operator(:>, 3, :tracks)
  end
end

validates_type

用于检查属性值是否为指定类型的实例。第一个参数可以是类名本身、和类名相同的字符串或符号,或者是类的数组:

class Album < Sequel::Model
  def validate
    super
    validates_type String, [:name, :website]
    validates_type :Artist, :artist
    validates_type [String, Integer], :foo
  end
end

validates_schema_types

validates_unique

validates_unique和其他validation_helpers方法有不一样的API,接受任意数量的参数,参数必须是列符号名或者列符号名数组。如果参数是一个符号,Sequel为该列检查值是否唯一,如果参数是一个符号数组,Sequel为该组合检查值是否唯一:

validates_unique(:name, :artist_id)

Will set up a 2 separate uniqueness validations. It will make it so that no two albums can have the same name, and that each artist can only be associated with one album. In general, that's probably not what you want. You probably want it so that two albums can have the same name, unless they are by the same artist. To do that, you need to use an array:

validates_unique([:name, :artist_id])

That sets up a single uniqueness validation for the combination of the fields.
You can mix and match the two approaches. For example, if all albums should have a unique UPC, and no artist can have duplicate album names:

validates_unique(:upc, [:name, :artist_id])

上面例子的含义是,upc不能重复,[:name, :artist_id]的组合不能重复。
validates_unique还可以接收一个块,这个块指定了约束的范围,例如:

validates_unique(:name){|ds| ds.where(:active)}

where条件筛选出来的记录才需要满足校验条件。
If you provide a block, it is called with the dataset to use for the uniqueness check, which you can then filter to scope the uniqueness validation to a subset of the model's dataset.
You can also include an options hash as the last argument. Unlike the other validations, the options hash for validates_unique only recognizes for these options:

  • :dataset The base dataset to use for the unique query, defaults to the model's dataset

  • :message The message to use

  • :only_if_modified Only check the uniqueness if the object is new or one of the columns has been modified.

  • :where A callable object where call takes three arguments, a dataset, the current object, and an array of columns, and should return a modified dataset that is filtered to include only rows with the same values as the current object for each column in the array. This is useful any time the unique constraints are derived from the columns and not the columns themselves (such as unique constraints on lower(column)).

validates_unique is the only method in validation_helpers that checks with the database. Attempting to validate uniqueness outside of the database suffers from a race condition, so any time you want to add a uniqueness validation, you should make sure to add a uniqueness constraint or unique index on the underlying database table. See the “Migrations and Schema Modification” guide for details on how to do that.

validation_helpers选项

除了validates_unique以外的其他validation_helpers方法都接受以下的选项:

:message

最常用的选项,用于覆盖默认的校验消息。可以是一个字符串,也可以是一个Proc对象。如果是一个字符串,则可以被直接使用,如果是一个Proc对象,则该Proc要能被调用并返回一个字符串。如果校验方法在属性数组前还有一个参数,则这个参数会被传递给Proc对象。The exception is the validates_not_string method, which doesn't take an argument, but passes the schema type symbol as the argument to the proc.

class Album < Sequel::Model
  def validate
    super
    validates_presence :copies_sold, :message=>'was not given'
    validates_min_length 3, :name, :message=>proc{|s| "should be more than #{s} characters"}
  end
end

:allow_nil

设置了:allow_nil选项后,如果属性值是nil或者属性没有提供时会跳过校验。该属性经常用在为一个属性设置了validates_presence 方法,同时又设置其他校验方法时,对nil值场景不想收到多个错误信息时:

class Album < Sequel::Model
  def validate
    super
    validates_presence :copies_sold
    validates_integer :copies_sold, :allow_nil=>true
  end
end

validates_integer不配置:allow_nil 选项时,如果copies_sold的值是nil,你会得到两个错误信息,而不是一个校验错误信息。

:allow_blank

和:allow_nil类似,但是该选项会跳过所有的空值,而不是仅跳过nil值。例如,艺术家可以有一个网站,如果有的话,应该是URL格式的;如果没有的话,应该是nil或者空字符串。

class Album < Sequel::Model
  def validate
    super
    validates_format /\Ahttps?:\/\//, :website, :allow_blank=>true
  end
end
a = Album.new
a.website = ''
a.valid? # true

如果要使用:allow_blank选项,你要保证所有的对象都能够响应blank?方法。Sequel附带提供了一个扩展:

Sequel.extension :blank

:allow_missing

该选项和:allow_nil选项不同,不是检查属性的值是否为nil,而是检查属性在model实例的值hash表中是否存在。当属性的值是nil或者没有提供值时:allow_nil会跳过校验。:alow_missing仅会在没有提供值时才跳过校验。如果提供的值是nil,:allow_missing不会跳过校验。

条件验证

由于Sequel使用validate实例方法处理校验,使得条件验证很简单。例如,在创建对象时对属性进行校验:

validates_presence :name if new?

对已存在的对象更新时进行校验:

validates_integer :copies_sold unless new?

根据对象的状态进行校验:

validates_presence :name if status_id > 1
validates_integer :copies_sold if status_id > 3

你可以使用所有的ruby条件表达式,比如case:

case status_id
when 1
  validates_presence :name
when 2
  validates_presence [:name, :artist_id]
when 3
  validates_presence [:name, :artist_id, :copies_sold]
end

你还可以指定依赖其他属性:

validates_min_length(status_id > 2 ? 5 : 10, [:name])
validates_presence(status_id < 2 ? :name : [:name, :artist_id])

基本上,条件验证不需要特殊的语法,和其他ruby代码中一样使用即可。

默认的错误消息

:exact_length
is not #{arg} characters

:format
is invalid

:includes
is not in range or set: #{arg.inspect}

:integer
is not a number

:length_range
is too short or too long

:max_length
is longer than #{arg} characters

:min_length
is shorter than #{arg} characters

:not_null
is not present

:numeric
is not a number

:schema_types
is not a valid #{schema_type}

:type
is not a #{arg}

:presence
is not present

:unique
is already taken

修改默认选项

可以很方便的修改validation_helpers使用的默认选项。所有的默认选项都保存在Sequel::Plugins::ValidationHelpers::DEFAULT_OPTIONS hash列表中。所以你只需要修改这个hash列表来改变默认选项。一个更新这个hash的方式是使用merge!:

Sequel::Plugins::ValidationHelpers::DEFAULT_OPTIONS.merge!(
 :presence=>{:message=>'cannot be empty'},
 :includes=>{:message=>'invalid option', :allow_nil=>true},
 :max_length=>{:message=>lambda{|i| "cannot be more than #{i} characters"}, :allow_nil=>true},
 :format=>{:message=>'contains invalid characters', :allow_nil=>true})

上面的例子会设置presence, includes, max_length, and format校验的错误消息,并且把includes, max_length, and format校验的:allow_nil选项默认值设置为true。

自定义验证

在validate方法内部,你可以定义自己的校验,在校验不过时,通过errors.add添加错误信息:

class Album < Sequel::Model
  def validate
    super
    errors.add(:release_date, 'cannot be before record date') if release_date < record_date
  end
end

就像条件校验,在自定义的校验中你可以使用标准的ruby条件判断,当检测到非法值时,通过调用errors.add添加错误信息。errors.add的第一个参数是列符号名,第二个参数是错误消息。
你可以把自定义的校验封装在一个方法中,并通过在validate中调用来打到复用的目的。For example, if there is a common need to validate that one column in the model comes before another column:

class Sequel::Model
  def validates_after(col1, col2)
    errors.add(col1, "cannot be before #{col2}") if send(col1) < send(col2)
  end
end
class Album < Sequel::Model
  def validate
    super
    validates_after(:release_date, :record_date)
  end
end

为所有Model设置校验

比如说,你想为你所有的model类添加默认的校验。通过重写Sequel::Model的validate方法可以很容易的做到,把校验代码放到其中,在你的model类中重写validate方法时,记得调用super:

class Sequel::Model
  def self.string_columns
    @string_columns ||= columns.reject{|c| db_schema[c][:type] != :string}
  end

  def validate
    super
    validates_format(/\A[^\x00-\x08\x0e-\x1f\x7f\x81\x8d\x8f\x90\x9d]*\z/n,
     model.string_columns,
     :message=>"contains invalid characters")
  end
end

这可以对所有的字符串列进行校验,以保证不包含所有的非法字符。但是要记得,如果你在某个model类中重写了validate方法,记得调用super:

class Album < Sequel::Model
  def validate
    super # Important!
    validates_presence :name
  end
end

如果你忘记了调用super,定义在Sequel::Model中的校验就不会被调用。在你重写任何一个Sequel::Model方法是都记得调用super,除非你明确不需要默认的行为。

Sequel::Model::Errors

Sequel::Model::Errors是Hash的一个子类,该类的常用方法如下:

add

为给定的列添加错误消息,第一个参数是列的符号,第二个参数是错误消息:

errors.add(:name, 'is not valid')

on

on经常在校验结束后被调用,来获取指定属性的需哦无消息,入参是列符号名,返回错误消息数组或nil:
errors.on(:name)
如果你基于一个属性的校验结果来对其他属性进行校验,你可能会用到on方法:
validates_integer(:release_date) if errors.on(:record_date)
这里,如果校验record data出现错误时,你就不关心release date了。

full_messages

full_messages为对象返回一个错误消息数组:

album.errors
# => {:name=>["cannot be empty"]}
album.errors.full_messages
# => ["name cannot be empty"]

count

返回错误消息个数:
album.errors.count # => 1

posted @ 2017-02-25 20:10  崔咩咩  阅读(583)  评论(0编辑  收藏  举报