Нечутливий до випадку пошук у моделі 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 string. Це еквівалентно "# {$$}". Але інтерполяція рядків не трапляється з рядками введення користувача. Спробуйте це в Irb, щоб побачити різницю: "$##"і '$##'. Перший - інтерпольований (подвійні лапки). Другий - ні. Введення користувачів ніколи не стає інтерпольованим.
— Брайан Мореарті

5
Просто зауважте, що find(:first)це застаріле, і тепер можливим є використання #first. Таким чином,Product.first(conditions: [ "lower(name) = ?", name.downcase ])
— Луїс Рамальо

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пропозиції є ланцюговими, оскільки зворотні виклики виконуються в контексті області. Просто FYI.
— 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, ...)зараз застаріло, я вважаю, що це найбільш правильна відповідь.
— користувач

чи потрібне ім'я.запису? Здається, працює зProduct.where("lower(name) = ?", name).first
— Йорданом

1
@Jordan Ви пробували це з іменами з великими літерами?
— ома

1
@ Джордан, можливо, не надто важливий, але ми повинні прагнути до точності на роботі, оскільки ми допомагаємо іншим :)
— ома

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 є істинним за замовчуванням. Я бачив, що поведінка в postgresql та інших людей повідомляли про те саме в mysql.
— Троя

1
тому я намагаюся це робити з postgres, і це не працює. find_by_x чутливий до регістру незалежно ...
— Louis Sayers

Ця перевірка є лише при створенні моделі. Отже, якщо у вашій базі даних є "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
як, з урахуванням регістру у mysql, але не в postgresql. Я не впевнений у Oracle або DB2. Суть у тому, що ви не можете розраховувати на нього, і якщо ви використовуєте його і ваш начальник змінить ваш базовий db, ви почнете мати "відсутні" записи без очевидних причин. Нижнє (ім'я) пропозиція @ нейтрино, мабуть, найкращий спосіб вирішити це питання.
— masukomi

6

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


коли сторінка, на яку ви посилаєтесь, помирає, тож і ваша відповідь.
— Антоній

Як @Anthony пророкував, так і сталося. Посилання мертвих.
— XP84

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

6

Верхні та малі літери відрізняються лише одним бітом. Найефективніший спосіб їх пошуку - ігнорувати цей біт, не конвертувати нижній чи верхній тощо. Перегляньте ключові слова COLLATIONдля MSSQL, подивіться, NLS_SORT=BINARY_CIчи використовується Oracle тощо.


4

Find_or_create тепер застарілий, замість цього ви повинні використовувати відношення AR плюс плюс first_or_create, як-от так:

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

Це поверне перший збіглий об'єкт або створить його для вас, якщо такого не існує.



2

Тут є багато чудових відповідей, зокрема, @ oma's. Але ще одна річ, яку ви можете спробувати, - це використання спеціальної серіалізації стовпців. Якщо ви не заперечуєте, щоб все зберігалося в малому регістрі, ви можете створити:

# 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

Мені подобається це рішення, але як ти пройшов помилку "Не можу відвідати Regexp"? Я бачу і це.
— Гейл

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
Це вкрай неефективно для більшого набору даних, оскільки він повинен завантажувати всю річ у пам'ять. Хоча це не проблема для вас лише декілька сотень записів, це не є хорошою практикою.
— лямбсанксі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.