Поиск без учета регистра в модели Rails
моя модель продукта содержит некоторые элементы
Product.first
=> #<Product id: 10, name: "Blue jeans" >
теперь я импортирую некоторые параметры продукта из другого набора данных, но есть несоответствия в написании имен. Например, в другом наборе данных, Blue jeans может быть прописано Blue Jeans.
хотел Product.find_or_create_by_name("Blue Jeans"), но это создаст новый продукт, почти идентичный первому. Каковы мои варианты, если я хочу найти и сравнить имя в нижнем регистре.
проблем с производительностью нет действительно важно здесь: есть только 100-200 продуктов, и я хочу запустить это как миграцию, которая импортирует данные.
какие идеи?
17 ответов:
вам, вероятно, придется быть более подробным здесь
name = "Blue Jeans" model = Product.where('lower(name) = ?', name.downcase).first model ||= Product.create(:name => name)
Это полная настройка в Rails, для моей собственной справки. Я рад, если это поможет и тебе тоже.
запрос:
Product.where("lower(name) = ?", name.downcase).firstвалидатор:
validates :name, presence: true, uniqueness: {case_sensitive: false}индекс (ответ от уникальный индекс без учета регистра в Rails / ActiveRecord?):
execute "CREATE UNIQUE INDEX index_products_on_lower_name ON products USING btree (lower(name));"Я хотел бы, чтобы был более красивый способ сделать первый и последний, но опять же, Rails и ActiveRecord с открытым исходным кодом, мы не должны жаловаться - мы можем реализовать его сами и отправить запрос на вытягивание.
вы можете использовать следующие:
validates_uniqueness_of :name, :case_sensitive => falseобратите внимание, что по умолчанию параметр :case_sensitive => false, поэтому вам даже не нужно писать эту опцию, если вы не изменили другие способы.
найти более: http://api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html#method-i-validates_uniqueness_of
Если вы используете 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">
слово документация SQLite:
любой другой символ соответствует самому себе или его нижний / верхний регистр эквивалент (т. е. совпадение без учета регистра)
...чего я не знал.Но это работает:
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, Я знаю, и это может быть не очень удобно для кросс-базы данных, но стоит посмотреть?
несколько комментариев относятся к Arel, не приводя пример.
вот пример Arel поиска без учета регистра:
Product.where(Product.arel_table[:name].matches('Blue Jeans'))преимущество этого типа решения заключается в том, что он является агностиком базы данных - он будет использовать правильные команды SQL для вашего текущего адаптера (
matchesиспользоватьILIKEдля Postgres, иLIKEдля всего остального).
буквы верхнего и нижнего регистра отличаются только одним битом - самый эффективный способ их поиска-игнорировать этот бит, а не конвертировать нижний или верхний и т. д.. См. раздел параметры сортировки ключевых слов для MS SQL, см. NLS_SORT=BINARY_CI при использовании Oracle и т. д..
еще один подход, о котором никто не упоминал, - это добавление нечувствительных к регистру искателей в ActiveRecord::Base. Подробности можно найти здесь. Преимущество этого подхода заключается в том, что вам не нужно изменять каждую модель, и вам не нужно добавлять
lower()предложение для всех ваших запросов без учета регистра, вы просто используете другой метод поиска вместо этого.
Find_or_create теперь не рекомендуется, вы должны использовать отношение AR вместо плюс first_or_create, например:
TombolaEntry.where("lower(name) = ?", self.name.downcase).first_or_create(name: self.name)это вернет первый сопоставленный объект или создаст его для вас, если он не существует.
поиск без учета регистра поставляется встроенный с рельсами. Он учитывает различия в реализациях баз данных. Использовать встроенная библиотека Arel, или драгоценный камень, как Squeel.
есть много хороших ответов здесь, особенно @Ома по. Но одна вещь, вы могли бы попробовать использовать пользовательскую сериализацию колонки. Если вы не возражаете, что все хранится в нижнем регистре в вашей БД, то вы можете создать:
# 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) = ?в ваши запросы.в недостатком является то, что вы теряете информацию о корпусе в базе данных.
вы также можете использовать такие области, как показано ниже, и поместить их в проблему и включить в модели, которые вам могут понадобиться:
scope :ci_find, lambda { |column, value| where("lower(#{column}) = ?", value.downcase).first }тогда используйте вот так:
Model.ci_find('column', 'value')
предполагая, что вы используете mysql, вы могли бы использовать поля, которые не чувствительны к регистру:http://dev.mysql.com/doc/refman/5.0/en/case-sensitivity.html
некоторые люди показывают, используя LIKE или ILIKE, но те позволяют поиск регулярных выражений. Также вам не нужно опускаться в Ruby. Вы можете позволить базе данных сделать это за вас. Я думаю, что это может быть быстрее. Также
first_or_createможно использовать послеwhere.# 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">
до сих пор я сделал решение с помощью Ruby. Поместите это внутри модели продукта:
#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 :)
Comments