Поиск без учета регистра в модели 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.