Rails 模型中不区分大小写的搜索
Posted
技术标签:
【中文标题】Rails 模型中不区分大小写的搜索【英文标题】:Case-insensitive search in Rails model 【发布时间】:2011-01-14 07:02:45 【问题描述】:我的产品模型包含一些项目
Product.first
=> #<Product id: 10, name: "Blue jeans" >
我现在正在从另一个数据集中导入一些产品参数,但名称的拼写不一致。例如,在另一个数据集中,Blue jeans
可以拼写为Blue Jeans
。
我想Product.find_or_create_by_name("Blue Jeans")
,但这将创建一个新产品,几乎与第一个相同。如果我想查找和比较小写名称,我有什么选择。
性能问题在这里并不重要:只有 100-200 种产品,我想将其作为导入数据的迁移来运行。
有什么想法吗?
【问题讨论】:
【参考方案1】:你可能不得不在这里更冗长
name = "Blue Jeans"
model = Product.where('lower(name) = ?', name.downcase).first
model ||= Product.create(:name => name)
【讨论】:
@botbot 的评论不适用于用户输入的字符串。 "#$$" 是一个鲜为人知的使用 Ruby 字符串插值转义全局变量的快捷方式。它相当于“#$$”。但是字符串插值不会发生在用户输入的字符串上。在 Irb 中尝试这些以查看区别:"$##"
和 '$##'
。第一个是插值(双引号)。第二个不是。用户输入永远不会被插值。
请注意find(:first)
已弃用,现在的选项是使用#first
。因此,Product.first(conditions: [ "lower(name) = ?", name.downcase ])
你不需要做所有这些工作。使用the built-in Arel library or Squeel
在 Rails 4 中你现在可以做model = Product.where('lower(name) = ?', name.downcase).first_or_create
@DerekLucas 尽管在 Rails 4 中可以这样做,但这种方法可能会导致意外行为。假设我们在Product
模型中有after_create
回调,在回调内部,我们有where
子句,例如products = Product.where(country: 'us')
。在这种情况下,where
子句被链接起来,因为回调在作用域的上下文中执行。仅供参考。【参考方案2】:
这是 Rails 中的完整设置,供我自己参考。如果对你也有帮助,我很高兴。
查询:
Product.where("lower(name) = ?", name.downcase).first
验证者:
validates :name, presence: true, uniqueness: case_sensitive: false
索引(来自Case-insensitive unique index in Rails/ActiveRecord?的回答):
execute "CREATE UNIQUE INDEX index_products_on_lower_name ON products USING btree (lower(name));"
我希望有一种更漂亮的方式来完成第一个和最后一个,但话说回来,Rails 和 ActiveRecord 是开源的,我们不应该抱怨 - 我们可以自己实现它并发送拉取请求。
【讨论】:
感谢您在 PostgreSQL 中创建不区分大小写的索引。感谢您展示如何在 Rails 中使用它!附加说明:如果您使用标准取景器,例如find_by_name,它仍然完全匹配。如果您希望搜索不区分大小写,则必须编写自定义查找器,类似于上面的“查询”行。 考虑到find(:first, ...)
现在已被弃用,我认为这是最合适的答案。
是否需要 name.downcase?它似乎适用于Product.where("lower(name) = ?", name).first
@Jordan 你试过用大写字母的名字吗?
@Jordan,也许不是太重要,但我们应该在帮助他人的同时争取 SO 的准确性 :)【参考方案3】:
如果您使用 Postegres 和 Rails 4+,那么您可以选择使用列类型 CITEXT,这将允许不区分大小写的查询,而无需写出查询逻辑。
迁移:
def change
enable_extension :citext
change_column :products, :name, :citext
add_index :products, :name, unique: true # If you want to index the product names
end
要对其进行测试,您应该期待以下内容:
Product.create! name: 'jOgGers'
=> #<Product id: 1, name: "jOgGers">
Product.find_by(name: 'joggers')
=> #<Product id: 1, name: "jOgGers">
Product.find_by(name: 'JOGGERS')
=> #<Product id: 1, name: "jOgGers">
【讨论】:
【参考方案4】:您可能想要使用以下内容:
validates_uniqueness_of :name, :case_sensitive => false
请注意,默认设置为:case_sensitive => false,所以如果你没有改变其他方式,你甚至不需要写这个选项。
了解更多信息: http://api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html#method-i-validates_uniqueness_of
【讨论】:
根据我的经验,与文档相比,case_sensitive 默认为 true。我已经在 postgresql 中看到了这种行为,而其他人在 mysql 中也报告了相同的行为。 所以我正在用 postgres 尝试这个,但它不起作用。 find_by_x 是区分大小写的,无论如何... 此验证仅在创建模型时进行。因此,如果您的数据库中有“HAML”,并且您尝试添加“haml”,它将无法通过验证。【参考方案5】:几个cmets指的是Arel,没有举例。
这是一个不区分大小写搜索的 Arel 示例:
Product.where(Product.arel_table[:name].matches('Blue Jeans'))
这种类型的解决方案的优点是它与数据库无关 - 它会为您当前的适配器使用正确的 SQL 命令(matches
将使用 ILIKE
用于 Postgres,LIKE
用于其他一切)。
【讨论】:
确保正确处理_
、%
以及是否有任何转义字符。在 MySQL 中默认转义为 \
,但在 oracle 中没有默认转义,您需要将其作为第二个参数添加到 #matches
。【参考方案6】:
在 postgres 中:
user = User.find(:first, :conditions => ['username ~* ?', "regedarek"])
【讨论】:
Rails on Heroku,所以使用 Postgres...ILIKE 非常棒。谢谢! 绝对在 PostgreSQL 上使用 ILIKE。【参考方案7】:引用SQLite documentation:
任何其他字符匹配自己或 它的小写/大写等效项(即 不区分大小写的匹配)
...我不知道。但它有效:
sqlite> create table products (name string);
sqlite> insert into products values ("Blue jeans");
sqlite> select * from products where name = 'Blue Jeans';
sqlite> select * from products where name like 'Blue Jeans';
Blue jeans
所以你可以这样做:
name = 'Blue jeans'
if prod = Product.find(:conditions => ['name LIKE ?', name])
# update product or whatever
else
prod = Product.create(:name => name)
end
不是#find_or_create
,我知道,而且它可能不太适合跨数据库,但值得一看?
【讨论】:
like 在 mysql 中区分大小写,但在 postgresql 中不区分大小写。我不确定 Oracle 或 DB2。关键是,您不能指望它,如果您使用它并且您的老板更改了您的基础数据库,您将开始“丢失”记录而没有明显的原因。 @neutrino 的 lower(name) 建议可能是解决这个问题的最佳方法。【参考方案8】:另一种没有人提到的方法是将不区分大小写的查找器添加到 ActiveRecord::Base 中。详情请见here。这种方法的优点是您不必修改每个模型,也不必将lower()
子句添加到所有不区分大小写的查询中,而只需使用不同的查找器方法。
【讨论】:
当您链接的页面消失时,您的答案也会消失。 正如@Anthony 所预言的那样,它已经实现了。链接失效。 @XP84 我不知道这有多相关,但我已经修复了链接。【参考方案9】:大小写字母仅相差一位。搜索它们最有效的方法是忽略该位,而不是转换低或高等。MSSQL 见关键字COLLATION
,如果使用 Oracle 等,见NLS_SORT=BINARY_CI
。
【讨论】:
【参考方案10】:Find_or_create 现在已弃用,您应该使用 AR 关系加上 first_or_create,如下所示:
TombolaEntry.where("lower(name) = ?", self.name.downcase).first_or_create(name: self.name)
这将返回第一个匹配的对象,如果不存在,则为您创建一个。
【讨论】:
【参考方案11】:类似于#1的安德鲁斯:
对我有用的是:
name = "Blue Jeans"
Product.find_by("lower(name) = ?", name.downcase)
这消除了在同一查询中执行#where
和#first
的需要。希望这会有所帮助!
【讨论】:
【参考方案12】:Rails 内置了不区分大小写的搜索。它解释了数据库实现的差异。使用the built-in Arel library, or a gem like Squeel。
【讨论】:
【参考方案13】:这里有很多很棒的答案,尤其是@oma 的。但是您可以尝试的另一件事是使用自定义列序列化。如果您不介意所有内容都以小写形式存储在您的数据库中,那么您可以创建:
# lib/serializers/downcasing_string_serializer.rb
module Serializers
class DowncasingStringSerializer
def self.load(value)
value
end
def self.dump(value)
value.downcase
end
end
end
然后在你的模型中:
# app/models/my_model.rb
serialize :name, Serializers::DowncasingStringSerializer
validates_uniqueness_of :name, :case_sensitive => false
这种方法的好处是您仍然可以使用所有常规查找器(包括 find_or_create_by
),而无需使用自定义范围、函数或在查询中包含 lower(name) = ?
。
缺点是您会丢失数据库中的外壳信息。
【讨论】:
【参考方案14】:另一种可能是
c = Product.find_by("LOWER(name)= ?", name.downcase)
【讨论】:
【参考方案15】:您也可以使用下面这样的范围并将它们放在关注点中并包含在您可能需要它们的模型中:
scope :ci_find, lambda |column, value| where("lower(#column) = ?", value.downcase).first
然后像这样使用:
Model.ci_find('column', 'value')
【讨论】:
【参考方案16】:假设你使用mysql,你可以使用不区分大小写的字段:http://dev.mysql.com/doc/refman/5.0/en/case-sensitivity.html
【讨论】:
【参考方案17】:user = Product.where(email: /^#email$/i).first
【讨论】:
TypeError: Cannot visit Regexp
@shilovk 谢谢。这正是我一直在寻找的。它看起来比接受的答案***.com/a/2220595/1380867
我喜欢这个解决方案,但您是如何解决“无法访问正则表达式”错误的?我也看到了。【参考方案18】:
有些人使用 LIKE 或 ILIKE 显示,但那些允许正则表达式搜索。此外,您不需要在 Ruby 中进行小写。你可以让数据库为你做这件事。我认为它可能会更快。在where
之后也可以使用first_or_create
。
# app/models/product.rb
class Product < ActiveRecord::Base
# case insensitive name
def self.ci_name(text)
where("lower(name) = lower(?)", text)
end
end
# first_or_create can be used after a where clause
Product.ci_name("Blue Jeans").first_or_create
# Product Load (1.2ms) SELECT "products".* FROM "products" WHERE (lower(name) = lower('Blue Jeans')) ORDER BY "products"."id" ASC LIMIT 1
# => #<Product id: 1, name: "Blue jeans", created_at: "2016-03-27 01:41:45", updated_at: "2016-03-27 01:41:45">
【讨论】:
【参考方案19】:到目前为止,我使用 Ruby 制作了一个解决方案。将其放在 Product 模型中:
#return first of matching products (id only to minimize memory consumption)
def self.custom_find_by_name(product_name)
@@product_names ||= Product.all(:select=>'id, name')
@@product_names.select|p| p.name.downcase == product_name.downcase.first
end
#remember a way to flush finder cache in case you run this from console
def self.flush_custom_finder_cache!
@@product_names = nil
end
这将为我提供第一个名称匹配的产品。或无。
>> Product.create(:name => "Blue jeans")
=> #<Product id: 303, name: "Blue jeans">
>> Product.custom_find_by_name("Blue Jeans")
=> nil
>> Product.flush_custom_finder_cache!
=> nil
>> Product.custom_find_by_name("Blue Jeans")
=> #<Product id: 303, name: "Blue jeans">
>>
>> #SUCCESS! I found you :)
【讨论】:
这对于更大的数据集来说效率极低,因为它必须将整个数据加载到内存中。虽然只有几百个条目对您来说不是问题,但这不是一个好习惯。以上是关于Rails 模型中不区分大小写的搜索的主要内容,如果未能解决你的问题,请参考以下文章