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文字列補間でグローバル変数をエスケープするためのあまり知られていないショートカットです。「#{$$}」に相当します。しかし、文字列補間はユーザー入力文字列には起こりません。違いを見るためにIrbの中にこれらを試してみてください"$##"'$##'。最初は補間されます(二重引用符)。2番目は違います。ユーザー入力は補間されません。
Brian Morearty、2013年

5
これfind(:first)は非推奨であり、オプションはを使用すること#firstです。したがって、Product.first(conditions: [ "lower(name) = ?", name.downcase ])
ルイスRamalho

2
このすべての作業を行う必要はありません。組み込みのArelライブラリまたはSqueel
Dogweatherを

17
Rails 4でできることmodel = Product.where('lower(name) = ?', name.downcase).first_or_create
Derek Lucas

1
@DerekLucas Rails 4では可能ですが、このメソッドは予期しない動作を引き起こす可能性があります。我々が持っていると仮定after_createして、コールバックをProductモデルとコールバックの内側に、我々が持っているwhereなど、句をproducts = Product.where(country: 'us')。この場合、これらのwhere句は、スコープのコンテキスト内でコールバックが実行されるときにチェーンされます。参考までに。
elquimista 2016年

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, ...)が現在廃止されていることを考えると、これが最も適切な答えだと思います。
ユーザー

name.downcaseは必要ですか?動作するようですProduct.where("lower(name) = ?", name).first
ヨルダン

1
@ジョーダンは大文字を含む名前でそれを試しましたか?
oma 2014

1
@ジョーダン、あまり重要ではないかもしれませんが、私たちは他の人を助けているので、SOの正確性に努めるべきです:)
oma

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はデフォルトで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 2013

間違いなくPostgreSQLでILIKEを使用しています。
Dom

12

いくつかのコメントは例を提供せずにArelを参照しています。

次に、大文字と小文字を区別しない検索のArelの例を示します。

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

このタイプのソリューションの利点は、データベースにとらわれないことです。現在のアダプタに適切なSQLコマンドを使用します(Postgresやその他すべてにmatches使用ILIKELIKEます)。


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
likeは、mysqlでは大文字と小文字を区別しますが、postgresqlでは区別しません。OracleかDB2かわかりません。つまり、それを当てにすることはできません。それを使用して上司が基礎となるデータベースを変更すると、明白な理由なしにレコードが「失われる」ようになります。@neutrinoのlower(name)の提案は、おそらくこれに対処するための最良の方法です。
ますこみ

6

誰も言及していない別のアプローチは、大文字と小文字を区別しないファインダーをActiveRecord :: Baseに追加することです。詳細はこちら。このアプローチの利点は、すべてのモデルを変更する必要がなくlower()、大文字と小文字を区別しないすべてのクエリに句を追加する必要がないことです。代わりに別のファインダーメソッドを使用するだけです。


あなたがリンクしたページが死ぬとき、あなたの答えも死にます。
Anthony

@Anthonyが予言したように、それが通過するようになりました。リンクが死んでいる。
XP84

3
@ XP84これがどれだけ関連しているかはわかりませんが、リンクを修正しました。
Alex Korban

6

大文字と小文字の違いは1ビットだけです。それらを検索する最も効率的な方法は、このビットを無視し、下位または上位を変換しないことなどCOLLATIONです。MSSQLのキーワードを参照しNLS_SORT=BINARY_CI、Oracleを使用しているかどうかなどを確認してください。


4

Find_or_createは廃止されました。次のように、first_or_createに加えてARリレーションを使用する必要があります。

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

これにより、最初に一致したオブジェクトが返されるか、存在しない場合は作成されます。



2

ここにはたくさんのすばらしい答えがあります。特に@omaです。しかし、もう1つ試すことができるのは、カスタム列のシリアル化を使用することです。すべて小文字で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

アンドリュースに似ています#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にアクセスできません」エラーをどのように回避しましたか?私もそれを見ています。
Gayle

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を使用してソリューションを作成しました。これをProductモデル内に配置します。

  #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.