Rails modelinde büyük / küçük harfe duyarlı olmayan arama


211

Ürün modelim bazı öğeler içeriyor

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

Şimdi başka bir veri kümesinden bazı ürün parametrelerini içe aktarıyorum, ancak adların yazımında tutarsızlıklar var. Örneğin, diğer veri kümesinde Blue jeanshecelenebilir Blue Jeans.

İstedim Product.find_or_create_by_name("Blue Jeans"), ama bu yeni bir ürün yaratacak, neredeyse ilkiyle aynı. Küçük harfli adı bulmak ve karşılaştırmak istersem seçeneklerim nelerdir?

Performans sorunları burada gerçekten önemli değil: Sadece 100-200 ürün var ve bunu verileri içe aktaran bir göç olarak çalıştırmak istiyorum.

Herhangi bir fikir?

Yanıtlar:


368

Muhtemelen burada daha ayrıntılı olmalısın

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

5
@ botbot'un yorumu kullanıcı girişindeki dizeler için geçerli değildir. "# $$" Ruby string enterpolasyonu ile global değişkenlerden kaçmak için az bilinen bir kısayoldur. "# {$$}" ile eşdeğerdir. Ancak dize enterpolasyonu kullanıcı girdi dizelerinde gerçekleşmez. Farkı görmek için Irb'de bunları deneyin: "$##"ve '$##'. Birincisi enterpole edilir (çift tırnak). İkincisi değil. Kullanıcı girişi asla enterpolasyona girmez.
Brian Morearty

5
Sadece find(:first)kullanımdan kaldırıldığını ve şimdi seçenek kullanmak olduğunu unutmayın #first. Böylece,Product.first(conditions: [ "lower(name) = ?", name.downcase ])
Luís Ramalho

2
Tüm bu işleri yapmanıza gerek yok. Kullanım dahili Arel kütüphane ya Squeel
Dogweather

17
Rails 4'te artık yapabilirsinizmodel = Product.where('lower(name) = ?', name.downcase).first_or_create
Derek Lucas

1
@DerekLucas Rails 4'te bunu yapmak mümkün olsa da, bu yöntem beklenmedik bir davranışa neden olabilir. Diyelim ki modelde after_creategeri arama Productvar ve geri aramanın içinde, whereyan tümcemiz var, örn products = Product.where(country: 'us'). Bu durumda, wherekapsam kapsamında geri çağrılar yürütülürken yan tümceler zincirlenir. Sadece FYI.
elquimista

100

Bu, kendi referansım için Rails'te eksiksiz bir kurulum. Sana da yardım ederse mutluyum.

sorgu:

Product.where("lower(name) = ?", name.downcase).first

doğrulayıcı:

validates :name, presence: true, uniqueness: {case_sensitive: false}

dizini ( Rails / ActiveRecord'daki büyük / küçük harfe duyarsız benzersiz dizinden gelen yanıt ? ):

execute "CREATE UNIQUE INDEX index_products_on_lower_name ON products USING btree (lower(name));"

İlk ve sonuncuyu yapmanın daha güzel bir yolu olsaydı, ama yine de, Rails ve ActiveRecord açık kaynak, şikayet etmemeliyiz - kendimiz uygulayabilir ve çekme isteği gönderebiliriz.


6
PostgreSQL'de büyük / küçük harfe duyarlı olmayan dizin oluşturma konusundaki krediniz için teşekkür ederiz. Rails'te nasıl kullanılacağını gösterdiğiniz için size teşekkür ederiz! Ek bir not: standart bir bulucu kullanıyorsanız, örneğin find_by_name, yine de tam olarak eşleşir. Aramanızın büyük / küçük harfe duyarsız olmasını istiyorsanız, yukarıdaki "sorgu" satırınıza benzer özel bulucular yazmanız gerekir.
Mark Berry

find(:first, ...)Şimdi reddedildiği düşünüldüğünde , bunun en doğru cevap olduğunu düşünüyorum.
kullanıcı

name.downcase gerekli mi? Görünüşe göreProduct.where("lower(name) = ?", name).first
Ürdün

1
@Jordan bunu büyük harfli isimlerle denediniz mi?
oma

1
@Jordan, belki de çok önemli değil, ama başkalarına yardım ettiğimiz için SO'da doğruluk için çaba göstermeliyiz :)
oma

28

Postegres ve Rails 4+ kullanıyorsanız, sorgu mantığını yazmak zorunda kalmadan büyük / küçük harfe duyarlı olmayan sorgulara izin veren CITEXT sütun türünü kullanma seçeneğiniz vardır.

Göç:

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

Ve test etmek için aşağıdakileri beklemelisiniz:

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

Aşağıdakileri kullanmak isteyebilirsiniz:

validates_uniqueness_of :name, :case_sensitive => false

Varsayılan olarak ayarın: case_sensitive => false olduğuna dikkat edin, bu nedenle başka yolları değiştirmediyseniz bu seçeneği yazmanıza bile gerek yoktur.

Daha fazla bilgi için: http://api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html#method-i-validates_uniqueness_of


5
Deneyimlerime göre, belgelerin aksine case_sensitive varsayılan olarak doğrudur. Ben postgresql ve diğerleri mysql aynı bildirdiklerini gördüm.
Troy

1
bu yüzden postgres ile deniyorum, ve çalışmıyor. find_by_x ne olursa olsun büyük / küçük harfe duyarlıdır ...
Louis Sayers

Bu doğrulama yalnızca model oluşturulurken yapılır. Veritabanınızda 'HAML' varsa ve 'haml' eklemeye çalışırsanız, doğrulama geçemez.
Dudo

14

Postgreslerde:

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

1
Heroku'da raylar var, bu yüzden Postgres kullanıyor… ILIKE mükemmel. Teşekkür ederim!
FeifanZ

Kesinlikle PostgreSQL üzerinde ILIKE kullanarak.
Dom

12

Bazı yorumlar Arel'e örnek vermeden atıfta bulunmaktadır.

Bir Arel'ye büyük / küçük harfe duyarlı olmayan bir arama örneği:

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

Bu tür bir çözümün avantajı, veritabanı-agnostik olmasıdır - mevcut adaptörünüz için doğru SQL komutlarını matcheskullanacaktır ILIKE(Postgres ve diğer LIKEher şey için kullanacaktır ).


9

SQLite belgelerinden alıntı :

Başka herhangi bir karakter kendisiyle veya küçük / büyük harf eşdeğeriyle eşleşir (örn. Büyük / küçük harf duyarsız eşleme)

... bilmiyordum ama işe yarıyor:

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

Böylece böyle bir şey yapabilirsiniz:

name = 'Blue jeans'
if prod = Product.find(:conditions => ['name LIKE ?', name])
    # update product or whatever
else
    prod = Product.create(:name => name)
end

Değil #find_or_create, biliyorum, ve veritabanları arasında çok kolay olmayabilir, ama bakmaya değer mi?


1
mysql'de büyük / küçük harf duyarlı ancak postgresql'de büyük / küçük harf duyarlı değildir. Oracle veya DB2'den emin değilim. Mesele şu ki, ona güvenemezsiniz ve eğer kullanırsanız ve patronunuz temeldeki db'nizi değiştirirse, bunun açık bir nedeni olmadan "kayıp" kayıtları almaya başlayacaksınız. @ neutrino'nun daha düşük (isim) önerisi muhtemelen bunu ele almanın en iyi yoludur.
masukomi

6

Hiç kimsenin bahsetmediği bir diğer yaklaşım, ActiveRecord :: Base'e büyük / küçük harfe duyarlı olmayan bulucular eklemektir. Ayrıntılar burada bulunabilir . Bu yaklaşımın avantajı, her modeli değiştirmek zorunda lower()kalmamanız ve tüm büyük / küçük harfe duyarlı olmayan sorgularınıza yan tümce eklemeniz gerekmiyor, bunun yerine yalnızca farklı bir bulma yöntemi kullanıyorsunuz.


bağladığınız sayfa öldüğünde, cevabınız da öyle.
Anthony

@ Anthony peygamberlik ettiği için, geçmeye başladı. Bağlantı ölü.
XP84

3
@ XP84 Bunun ne kadar alakalı olduğunu artık bilmiyorum, ancak bağlantıyı düzelttim.
Alex Korban

6

Büyük ve küçük harfler yalnızca tek bir bitle farklılık gösterir. Bunları aramak için en verimli şekilde, bu biraz gözardı etmemeye alt dönüştürmek için veya üst vb bakın anahtar olduğunu COLLATIONgörmek MSSQL için NLS_SORT=BINARY_CIeğer Oracle kullanarak, vb


4

Find_or_create artık kullanımdan kaldırıldı, bunun yerine AR İlişkisi artı first_or_create kullanmalısınız, şöyle:

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

Bu, ilk eşleşen nesneyi döndürür veya yoksa sizin için bir nesne oluşturur.



2

Burada büyük cevaplar var, özellikle @ oma. Ancak deneyebileceğiniz başka bir şey de özel sütun serileştirme kullanmaktır. Eğer db içinde küçük harf saklanan her şeyin sakıncası yoksa oluşturabilirsiniz:

# 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

Sonra modelinizde:

# app/models/my_model.rb
serialize :name, Serializers::DowncasingStringSerializer
validates_uniqueness_of :name, :case_sensitive => false

Bu yaklaşımın yararı, find_or_create_byözel kapsamları, işlevleri kullanmadan veyalower(name) = ? sorgularınızda .

Dezavantajı, veritabanındaki kasa bilgilerini kaybetmenizdir.


2

# 1 olan Andrews'a benzer:

Benim için işe yarayan bir şey:

name = "Blue Jeans"
Product.find_by("lower(name) = ?", name.downcase)

Bu, bir #whereve #firstaynı sorguda yapılması gerekliliğini ortadan kaldırır . Bu yardımcı olur umarım!


1

Ayrıca, aşağıdaki gibi kapsamları kullanabilir ve bunları bir kaygıya sokabilir ve ihtiyacınız olabilecek modellere dahil edebilirsiniz:

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

Sonra şöyle kullanın: Model.ci_find('column', 'value')



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

TypeError: Cannot visit Regexp
Dorian

@shilovk teşekkürler. Tam da aradığım şey buydu. Ve kabul edilen cevaptan daha iyi görünüyordu stackoverflow.com/a/2220595/1380867
MZaragoza

Bu çözümü beğendim, ancak "Regexp ziyaret edilemiyor" hatasını nasıl geçtiniz? Ben de görüyorum.
Gayle

0

Bazı insanlar LIKE veya ILIKE kullanarak gösterir, ancak bunlar normal ifade aramalarına izin verir. Ayrıca Ruby'de küçük harflere gerek yok. Veritabanının sizin için yapmasına izin verebilirsiniz. Bence daha hızlı olabilir. Ayrıca first_or_createsonra kullanılabilir 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

Şimdiye kadar Ruby kullanarak bir çözüm ürettim. Bunu Ürün modelinin içine yerleştirin:

  #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

Bu bana isimlerin eşleştiği ilk ürünü verecek. Veya sıfır.

>> 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
Daha büyük bir veri kümesi için bu son derece verimsizdir, çünkü her şeyi belleğe yüklemelidir. Sadece birkaç yüz girişle sizin için sorun olmasa da, bu iyi bir uygulama değildir.
lambshaanxy
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.