Rails自定义模型设置器和查询

时间:2016-01-20 23:26:43

标签: ruby-on-rails activerecord

假设在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是推荐的这种情景方法吗?

3 个答案:

答案 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_byfind_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)

另见