Rails在持久化时忽略验证

时间:2018-04-01 20:37:49

标签: ruby-on-rails ruby activerecord

我遇到一个问题,我的City模型正在创建多个重复项,并以某种方式忽略了我的验证。

我的控制器正在实例化Family记录并按如下方式分配城市:

f = Family.new 
f.assign_city('Seattle', 'Washington') # put in literals as examples

然后,在我的Family模型中,我有assign_city方法:

def assign_city(city_name, state_name)
  raise(GeoException, 'Both city and state names must be present') 
  unless city_name.present? && state_name.present?
  existing_city = City.query_by_name_and_state(city_name, 
    state_name).first
  if existing_city
    self.city = existing_city
  else
    self.city = City.create! name: city_name, state: state_name, 
      country: 'USA', description: ''
  end
end 

我的City模型包含以下条目:

validates :name, presence: true, uniqueness: {scope: :state}
validates :state, presence: true, numericality: false

scope :query_by_name_and_state, (->(city, state) {
  if city.present? && state.present?
    where('LOWER(name) LIKE ? AND LOWER(state) LIKE ?', city.downcase, 
     state.downcase)
  else
    where '' # to prevent exception to be raised if city, state are nil
  end
})

我很困惑有两个原因:

  1. 该程序甚至不应该进入ELSE流程来创建新城市,因为已经有一个具有相同名称和州的城市。
  2. 进入ELSE流程后,它不应该成功创建记录,因为您无法从验证中获取,正如您所看到的那样。
  3. 那就是说,我不知何故有17个具有相同名称和州的重复城市!

    PS-如果它也有帮助,我做了一个City.where('Seattle', 'Washington').map(&:is_valid?)并得到一个[false, false, false,...]的数组

    任何建议都将不胜感激。谢谢!

1 个答案:

答案 0 :(得分:1)

您不需要执行此条件逻辑,您可以使用find_or_create_by

def assign_city(city_name, state_name)
  raise(GeoException, 'Both city and state names must be present') unless city_name.present? && state_name.present?

  self.city = City.find_or_create_by name: city_name, state: state_name, country: 'USA', description: ''
end 

很难知道为什么你没有得到模型验证错误。我怀疑它是因为您在Family的未加载实例上运行。但是如果你想要安全,你可以在条件逻辑中执行city.valid?。但是,如果要确保数据完整性,最佳做法是包括数据库级别验证,因为有多种方法可以绕过/覆盖rails中的模型验证。您的迁移可能如下所示:

class ChangeCity < ActiveRecord::Migration
  def change
     add_index :cities, [:city_name, :state_name],  unique: true
  end
end

这样会引发不依赖于模型验证的数据库级错误。您还需要先删除欺骗,否则此迁移将失败。

更新:我测试了你的城市范围方法,发现它可能不太可靠,因为你可以看到这里,如果我们传递空字符串的城市和阶段看看会发生什么:

simple_soundcloud_app(main)> existing_city = City.query_by_name_and_state('', '').first
  City Load (0.1ms)  SELECT  "cities".* FROM "cities" ORDER BY "cities"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<City:0x007f84790c2308
 id: 1,
 name: "New York",
 state: "NY",
 created_at: Sun, 01 Apr 2018 22:58:35 UTC +00:00,
 updated_at: Sun, 01 Apr 2018 22:58:35 UTC +00:00>

这并没有回答为什么您的验证没有按预期启动的问题但是我们无法确定您使用的输入数据或首先如何创建重复项。我建议为所有方法编写单元测试并测试所有边缘情况。

更新2,让我们修复你的范围方法,因为它有很多错误,特别是如上所述的黑客攻击。你的范围需要参数,所以不要在没有反模式的情况下调用它。您可能应该使用像https://github.com/loureirorg/city-state之类的宝石。但是,如果您要管理数据,请将其标准化为包含所有数据。然后这将工作:

class City < ApplicationRecord
  has_many :families
  validates :name, presence: true, uniqueness: {scope: :state}
  validates :state, presence: true, numericality: false

  scope :query_by_name_and_state, -> (city, state) {
      where(name: city.downcase, state: state.downcase)
  }

  before_save :normalize_data

  def normalize_data
    self.name.downcase!
    self.state.downcase!
  end
end


simple_soundcloud_app(main)> existing_city = City.query_by_name_and_state('Boston', 'MA').first
  City Load (0.1ms)  SELECT  "cities".* FROM "cities" WHERE "cities"."name" = ? AND "cities"."state" = ? ORDER BY "cities"."id" ASC LIMIT ?  [["name", "boston"], ["state", "ma"], ["LIMIT", 1]]
=> #<City:0x007fce6a34e5c8
 id: 3,
 name: "boston",
 state: "ma",
 created_at: Sun, 08 Apr 2018 01:29:43 UTC +00:00,
 updated_at: Sun, 08 Apr 2018 01:29:43 UTC +00:00>
 simple_soundcloud_app(main)> existing_city = City.query_by_name_and_state('', '').first
  City Load (0.1ms)  SELECT  "cities".* FROM "cities" WHERE "cities"."name" = ? AND "cities"."state" = ? ORDER BY "cities"."id" ASC LIMIT ?  [["name", ""], ["state", ""], ["LIMIT", 1]]
=> nil