Нечутливий до випадку пошук у моделі 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.