Нечувствительный к регистру поиск в модели Rails

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

 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)

Ответ 2

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

Ответ 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

Ответ 5

В postgres:

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

Ответ 6

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

Ответ 7

Несколько комментариев относятся к Arel, не приводя пример.

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

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

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

Ответ 8

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

Ответ 9

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

Ответ 10

Find_or_create теперь устарел, вы должны использовать AR-отношение вместо first_or_create, например:

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

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

Ответ 12

Здесь есть много замечательных ответов, особенно @oma's. Но еще одна вещь, которую вы могли бы попробовать, - использовать пользовательскую сериализацию столбцов. Если вы не против того, чтобы все было записано в нижнем регистре в вашем db, вы можете создать:

# 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) = ? в ваших запросах.

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

Ответ 13

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

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

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

Ответ 15

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

Ответ 16

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

Ответ 17

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

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

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

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

Ответ 18

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

c = Product.find_by("LOWER(name)= ?", name.downcase)

Ответ 19

До сих пор я решил использовать 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 :)