Поиск без учета регистра в модели 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)
alex.zherdev
источник
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 с открытым исходным кодом, мы не должны жаловаться - мы можем реализовать это сами и отправить запрос на извлечение.

OMA
источник
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">
Viet
источник
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"])
tomekfranek
источник
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)

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

superluminary
источник
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')

theterminalguy
источник
0
user = Product.where(email: /^#{email}$/i).first
shilovk
источник
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"> 
6 футов Дан
источник
0

Альтернативой может быть

c = Product.find_by("LOWER(name)= ?", name.downcase)
Дэвид Барриентос
источник
-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