Поиск без учета регистра в модели Rails



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



 Product.first
=> #<Product id: 10, name: "Blue jeans" >


теперь я импортирую некоторые параметры продукта из другого набора данных, но есть несоответствия в написании имен. Например, в другом наборе данных, Blue jeans может быть прописано Blue Jeans.



хотел Product.find_or_create_by_name("Blue Jeans"), но это создаст новый продукт, почти идентичный первому. Каковы мои варианты, если я хочу найти и сравнить имя в нижнем регистре.



проблем с производительностью нет действительно важно здесь: есть только 100-200 продуктов, и я хочу запустить это как миграцию, которая импортирует данные.



какие идеи?

790   17  

17 ответов:

вам, вероятно, придется быть более подробным здесь

name = "Blue Jeans"
model = Product.where('lower(name) = ?', name.downcase).first 
model ||= Product.create(:name => name)

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

вы можете использовать следующие:

validates_uniqueness_of :name, :case_sensitive => false

обратите внимание, что по умолчанию параметр :case_sensitive => false, поэтому вам даже не нужно писать эту опцию, если вы не изменили другие способы.

найти более: http://api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html#method-i-validates_uniqueness_of

Если вы используете 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">

в postgres:

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

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

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

вот пример Arel поиска без учета регистра:

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

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

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

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

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

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

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

поиск без учета регистра поставляется встроенный с рельсами. Он учитывает различия в реализациях баз данных. Использовать встроенная библиотека Arel, или драгоценный камень, как Squeel.

есть много хороших ответов здесь, особенно @Ома по. Но одна вещь, вы могли бы попробовать использовать пользовательскую сериализацию колонки. Если вы не возражаете, что все хранится в нижнем регистре в вашей БД, то вы можете создать:

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

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

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

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

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

предполагая, что вы используете mysql, вы могли бы использовать поля, которые не чувствительны к регистру:http://dev.mysql.com/doc/refman/5.0/en/case-sensitivity.html

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

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

до сих пор я сделал решение с помощью 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 :)

Comments

    Ничего не найдено.