Поиск без учета регистра в модели Rails


211

Моя модель продукта содержит некоторые элементы

 Product.first
 => #<Product id: 10, name: "Blue jeans" >

Сейчас я импортирую некоторые параметры продукта из другого набора данных, но есть несоответствия в написании имен. Например, в другом наборе данных Blue jeansможет быть написано Blue Jeans.

Я хотел Product.find_or_create_by_name("Blue Jeans"), но это создаст новый продукт, практически идентичный первому. Каковы мои варианты, если я хочу найти и сравнить имя в нижнем регистре.

Проблемы с производительностью на самом деле здесь не важны: есть только 100-200 продуктов, и я хочу запустить их как миграцию, которая импортирует данные.

Любые идеи?

Ответы:


368

Вы, вероятно, должны быть более многословными здесь

name = "Blue Jeans"
model = Product.where('lower(name) = ?', name.downcase).first 
model ||= Product.create(:name => name)

5
Комментарий @ botbot не относится к строкам из пользовательского ввода. "# $$" - это малоизвестный ярлык для экранирования глобальных переменных с помощью интерполяции строк Ruby. Это эквивалентно "# {$$}". Но интерполяция строк не происходит со строками, вводимыми пользователем. Попробуйте это в Irb, чтобы увидеть разницу: "$##"и '$##'. Первый интерполируется (двойные кавычки). Второго нет. Пользовательский ввод никогда не интерполируется.
— Брайан Морарти

5
Просто чтобы заметить, что find(:first)это устарело, и вариант теперь использовать #first. Таким образом,Product.first(conditions: [ "lower(name) = ?", name.downcase ])
— Luís Ramalho

2
Вам не нужно делать всю эту работу. Используйте встроенную библиотеку Arel или Squeel
— Dogweather

17
В Rails 4 теперь можно делатьmodel = Product.where('lower(name) = ?', name.downcase).first_or_create
— Дерек Лукас

1
@DerekLucas, хотя это возможно сделать в Rails 4, этот метод может вызвать неожиданное поведение. Предположим, у нас есть after_createобратный вызов в Productмодели и внутри обратного вызова, у нас есть whereпредложение, например products = Product.where(country: 'us'). В этом случае whereпредложения объединяются в цепочку при выполнении обратных вызовов в контексте области. Просто к вашему сведению.
— Elquimista

100

Это полная настройка в 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 с открытым исходным кодом, мы не должны жаловаться - мы можем реализовать это сами и отправить запрос на извлечение.


6
Спасибо за заслугу в создании без учета регистра в PostgreSQL. Благодарим вас за то, что вы показали, как использовать его в Rails! Еще одно примечание: если вы используете стандартный искатель, например, find_by_name, он все равно точно соответствует. Вы должны написать пользовательские искатели, аналогичные приведенной выше строке «запрос», если вы хотите, чтобы в поиске не учитывался регистр.
— Марк Берри

Учитывая, что find(:first, ...)это устарело, я думаю, что это самый правильный ответ.
— пользователь

name.downcase необходимо? Кажется, работает сProduct.where("lower(name) = ?", name).first
— Джордан

1
@ Джордан, ты пробовал это с именами, имеющими заглавные буквы?
— Ома

1
@ Иордания, возможно, не слишком важно, но мы должны стремиться к точности в SO, поскольку мы помогаем другим :)
— oma

28

Если вы используете 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">

21

Возможно, вы захотите использовать следующее:

validates_uniqueness_of :name, :case_sensitive => false

Обратите внимание, что по умолчанию установлено значение case_sensitive => false, поэтому вам даже не нужно писать эту опцию, если вы не изменили другие способы.

Узнайте больше по адресу: http://api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html#method-i-validates_uniqueness_of


5
По моему опыту, в отличие от документации, case_sensitive по умолчанию имеет значение true. Я видел, что поведение в postgresql и других сообщали то же самое в mysql.
— Трой

1
так что я пытаюсь это с postgres, и это не работает. find_by_x чувствителен к регистру независимо от ...
— Луи Сэйерс

Эта проверка только при создании модели. Так что, если у вас есть «HAML» в вашей базе данных, и вы пытаетесь добавить «haml», он не пройдет валидацию.
— Дудо

14

В postgres:

 user = User.find(:first, :conditions => ['username ~* ?', "regedarek"])

1
Рельсы на Heroku, так что с использованием Postgres ... ILIKE блестяще. Спасибо!
— FeifanZ

Определенно используя ILIKE на PostgreSQL.
— Дом

12

Несколько комментариев относятся к Арелу, без предоставления примера.

Вот пример Arel для поиска без учета регистра:

Product.where(Product.arel_table[:name].matches('Blue Jeans'))

Преимущество этого типа решения заключается в том, что оно не зависит от базы данных - оно будет использовать правильные команды SQL для вашего текущего адаптера ( matchesбудет использоваться ILIKEдля Postgres и LIKEдля всего остального).


9

Цитирование из документации 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, я знаю, и это может быть не очень дружелюбно к базе данных, но стоит посмотреть?


1
like чувствителен к регистру в mysql, но не в postgresql. Я не уверен насчет Oracle или DB2. Дело в том, что вы не можете рассчитывать на это, и если вы используете его и ваш босс изменит ваш базовый БД, у вас начнутся «пропущенные» записи без очевидной причины. Нижнее (имя) предложение @ нейтрино, вероятно, является лучшим способом решения этой проблемы.
— Масукоми

6

Другой подход, о котором никто не упомянул, заключается в добавлении нечувствительных к регистру искателей в ActiveRecord :: Base. Подробности можно найти здесь . Преимущество этого подхода состоит в том, что вам не нужно изменять каждую модель, и вам не нужно добавлять lower()предложение ко всем вашим запросам без учета регистра, вместо этого вы просто используете другой метод поиска.


когда умирает страница, на которую вы ссылаетесь, ваш ответ.
— Энтони

Как пророчествовал @ Энтони, так и произошло. Ссылка мертвая.
— XP84

3
@ XP84 Я не знаю, насколько это актуально, но я исправил ссылку.
— Алекс Корбан

6

Прописные и строчные буквы отличаются только на один бит. Наиболее эффективный способ их поиска - игнорировать этот бит, не преобразовывать нижний или верхний и т. Д. См. Ключевые слова COLLATIONдля MSSQL, посмотрите NLS_SORT=BINARY_CI, используете ли Oracle, и т. Д.


4

Find_or_create теперь устарела, вместо этого вы должны использовать AR Relation плюс first_or_create, вот так:

TombolaEntry.where("lower(name) = ?", self.name.downcase).first_or_create(name: self.name)

Это вернет первый соответствующий объект или создаст его для вас, если его не существует.



2

Здесь много хороших ответов, особенно @ 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) = ? в ваших запросах.

Недостатком является то, что вы теряете информацию об корпусе в базе данных.


2

Подобно Эндрюсу, который является # 1:

Что-то, что сработало для меня:

name = "Blue Jeans"
Product.find_by("lower(name) = ?", name.downcase)

Это избавляет от необходимости делать один #whereи #firstтот же запрос. Надеюсь это поможет!


1

Вы также можете использовать такие области, как это ниже, и поставить их под вопрос и включить в модели, которые могут вам понадобиться:

scope :ci_find, lambda { |column, value| where("lower(#{column}) = ?", value.downcase).first }

Затем используйте как это: Model.ci_find('column', 'value')



0
user = Product.where(email: /^#{email}$/i).first

TypeError: Cannot visit Regexp
— Дориан

@shilovk спасибо. Это именно то, что я искал. И это выглядело лучше, чем принятый ответ stackoverflow.com/a/2220595/1380867
— MZaragoza

Мне нравится это решение, но как вы преодолели ошибку «Не удается посетить регулярное выражение»? Я тоже это вижу.
— Гейл

0

Некоторые люди показывают, используя 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"> 


-9

Пока что я сделал решение, используя 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 :)

2
Это крайне неэффективно для большого набора данных, поскольку он должен загружать все это в память. Хотя это не проблема для вас с несколькими сотнями записей, это не очень хорошая практика.
— lambshaanxy
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.