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의 설명은 사용자 입력의 문자열에는 적용되지 않습니다. "# $$"는 루비 문자열 보간으로 전역 변수를 이스케이프 처리하기위한 잘 알려진 바로 가기입니다. "# {$$}"와 같습니다. 그러나 문자열 보간은 사용자 입력 문자열에는 발생하지 않습니다. 차이를 볼 수 IRB에서 이러한 시도 : "$##"'$##'. 첫 번째는 보간됩니다 (큰 따옴표). 두 번째는 아닙니다. 사용자 입력은 보간되지 않습니다.
Brian Morearty

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
Derek Lucas

1
@DerekLucas Rails 4에서는 가능하지만이 방법으로 예기치 않은 동작이 발생할 수 있습니다. 우리가 가지고 있다고 가정after_createProduct모델에 콜백이 있고 콜백 내에 콜백이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)를 사용하는 경우에도 여전히 정확히 일치합니다. 검색시 대소 문자를 구분하지 않으려면 위의 "쿼리"줄과 유사한 사용자 정의 파인더를 작성해야합니다.
Mark Berry

즉 고려 find(:first, ...)이제 사용되지 않습니다, 나는 이것이 가장 적절한 답이라고 생각합니다.
사용자

소문자가 필요합니까? 그것은 작동하는 것 같습니다Product.where("lower(name) = ?", name).first
Jordan

1
@ Jordan은 대문자로 된 이름으로 시도해 보셨습니까?
oma

1
@Jordan, 아마도 그렇게 중요하지는 않지만, 우리는 다른 사람들을 돕고 있으므로 SO에 대한 정확성을 위해 노력해야합니다.)
oma

28

Postegres and 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에서도 동일한 동작을보고하는 것을 보았습니다.
Troy

1
postgres로 시도하고 있는데 작동하지 않습니다. find_by_x는 대소 문자를 구분합니다 ...
Louis Sayers

이 유효성 검사는 모델을 만들 때만 가능합니다. 따라서 데이터베이스에 'HAML'이 있고 'haml'을 추가하려고하면 유효성 검사를 통과하지 못합니다.
Dudo

14

postgres에서 :

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

1
Heroku의 Rails, Postgres 사용… ILIKE는 훌륭합니다. 감사합니다!
FeifanZ

PostgreSQL에서 ILIKE를 확실히 사용합니다.
Dom

12

몇 가지 의견은 예제를 제공하지 않고 Arel을 나타냅니다.

대소 문자를 구분하지 않는 검색의 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를 변경하면 분명한 이유없이 레코드가 누락되기 시작합니다. @neutrino의 더 낮은 제안은 아마도 이것을 해결하는 가장 좋은 방법 일 것입니다.
masukomi

6

아무도 언급하지 않은 또 다른 방법은 대소 문자를 구분하지 않는 파인더를 ActiveRecord :: Base에 추가하는 것입니다. 자세한 내용은 여기를 참조하십시오 . 이 방법의 장점은 모든 모델을 수정할 필요가없고 lower()대소 문자를 구분하지 않는 모든 쿼리에 절을 추가 할 필요가 없으며 다른 파인더 메소드를 사용하는 것입니다.


링크 한 페이지가 죽으면 대답도 사라집니다.
Anthony

@Anthony가 예언 한대로 지나갔습니다. 연결이 끊어졌습니다.
XP84

3
@ XP84 이것이 더 이상 관련성이 있는지 모르겠지만 링크를 수정했습니다.
Alex Korban

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에 대한 훌륭한 답변이 많이 있습니다. 그러나 시도 할 수있는 또 다른 방법은 사용자 정의 열 직렬화를 사용하는 것입니다. 모든 것이 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) = ? 쿼리에 입니다.

단점은 데이터베이스에서 케이싱 정보가 손실된다는 것입니다.


2

Andrews와 비슷한 # 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
Dorian

@shilovk 감사합니다. 이것이 바로 내가 찾던 것입니다. 그리고 그것은 받아 들여지는 답변보다 좋아 보였습니다. stackoverflow.com/a/2220595/1380867
MZaragoza

이 솔루션이 마음에 들지만 "Regexp를 방문 할 수 없습니다"오류를 어떻게 극복 했습니까? 나도 그것을보고있다.
Gayle

0

어떤 사람들은 LIKE 또는 ILIKE를 사용하여 보여 주지만 정규 표현식 검색을 허용합니다. 또한 루비로 소문자를 옮길 필요가 없습니다. 데이터베이스가 자동으로 처리하도록 할 수 있습니다. 나는 그것이 더 빠를 것이라고 생각합니다. 또한 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
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.