假设在name
列上有一个带有custom setter/accessor和唯一性约束的Rails模型:
class Person < ActiveRecord::Base
validates :name, presence: true, uniqueness: true
def name=(name)
# Example transformation only.
# Could be substituted for a more complex operation/transformation.
title_cased = name.titleize
self[:name] = title_cased
end
end
现在,请考虑以下事项:
Person.create! name: "John Citizen"
Person.find_or_create_by! name: "john citizen" # Error: validation fails
find
操作将找不到任何结果,因为没有与“john citizen”匹配的条目。然后,create!
操作将抛出错误,因为已存在条目“John Citizen”(create!
创建新记录并在验证失败时引发异常)。
你如何优雅防止此类错误发生?对于松散耦合和封装目的,在执行find_or_create_by!
之类的操作或其他操作(如find_by
之前)是否可以不转换名称(在本例中为titlecase)?< / p>
编辑: 正如@harimohanraj所暗示的那样,问题似乎是等价的。模型是否应该透明地处理对其简化的规范状态的理解/转换输入。或者这应该是班级/模型的消费者的责任吗?
此外,active record callbacks是推荐的这种情景方法吗?
答案 0 :(得分:2)
如果您已定义了自定义setter方法,则您所做的隐式决策是:name
属性的值,无论它们以何种形式出现(例如,用户在其中的输入)文本字段),应在数据库中以标题形式处理。如果是这样,那么find_or_create_by! name: 'john citizen'
失败就有意义了!换句话说,您的自定义setter方法代表您决定&#34; John Citizen&#34;和#34;约翰公民&#34;是一回事。
如果您发现自己想要在数据库中存储John Citizen
和 john citizen
,那么我会重新考虑您创建自定义setter方法的决定。实现&#34;松散耦合的一种很酷的方法&#34;是将所有清理数据的逻辑(例如来自用户填写表单的数据)放入一个单独的Ruby对象中。
问题上没有太多的背景,所以这里有一个抽象的例子来证明我的意思。
# A class to house the logic of sanitizing your parameters
class PersonParamsSanitizer
# It is initialized with dirty user parameters
def initialize(params)
@params = params
end
# It spits out neat, titleized params
def sanitized_params
{
name: @params[:name].titleize
}
end
end
class PersonController < ApplicationController
def create
# Use your sanitizer object to convert dirty user parameters into neat
# titleized params for your new perons
sanitized_params = UserParamsSanitizer.new(params).sanitized_params
person = Person.new(sanitized_params)
if person.save
redirect_to person
else
render :new
end
end
end
这样,您就不会覆盖用户模型中的setter方法,如果您愿意,可以无所畏惧地使用find_or_create_by!
!
答案 1 :(得分:0)
问题是find_or_create_by
和类似的方法已经没有改变名称...正如你所说没有记录“john citizen”但是要正常工作你需要为find_or_create_by
,find_or_create_by!
或find_by
(您不需要find
的此解决方案,因为它只按主键检索记录)
所以...
def self.find_or_create_by(options)
super(rectify_options(options))
end
def self.find_or_create_by!(options)
super(rectify_options(options))
end
def self.find_by(options)
super(rectify_options(options))
end
private
def self.rectify_options(options)
options[:name] = (new.name = options[:name]) if options[:name]
options
end
答案 2 :(得分:0)
您可以使用以下命令将验证设置为不区分大小写:
class Person < ActiveRecord::Base
validates :name,
presence: true,
uniqueness: { case_sensitive: false }
end
但是,从just using a validation in Rails will lead to race conditions开始,您还需要一个不区分大小写的数据库索引支持它。如何实现这一点取决于RBDMS。
这留下了查询的问题。执行密集搜索的经典方法是WHERE LOWER(name) = LOWER(?)
。虽然Postgres允许您使用WHERE ILIKE name = ?
。
如果你想将它封装到模型中,这是个好主意,你可以创建一个范围:
class Person
scope :find_by_name, lambda{ |name| where('LOWER(name) = LOWER(?)', name) }
end
但是,在这种情况下,您不能使用.find_or_create_by!
作为查询而不仅仅是哈希。相反,您可以拨打.first_or_create
。
Person.find_by_name("John Citizen").first_or_create(attrs)