Wyszukiwanie bez rozróżniania wielkości liter w modelu Rails


211

Mój model produktu zawiera niektóre elementy

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

Teraz importuję niektóre parametry produktu z innego zestawu danych, ale w pisowni nazw występują niespójności. Na przykład w innym zbiorze danych Blue jeansmożna przeliterować Blue Jeans.

Chciałem Product.find_or_create_by_name("Blue Jeans"), ale stworzy to nowy produkt, prawie identyczny z pierwszym. Jakie są moje opcje, jeśli chcę znaleźć i porównać małą literę.

Problemy z wydajnością nie są tu tak naprawdę ważne: jest tylko 100-200 produktów i chcę uruchomić to jako migrację, która importuje dane.

Jakieś pomysły?

Odpowiedzi:


368

Prawdopodobnie będziesz musiał być bardziej gadatliwy

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

5
Komentarz @ botbota nie dotyczy ciągów znaków wprowadzanych przez użytkownika. „# $$” to mało znany skrót do zmiany zmiennych globalnych za pomocą interpolacji ciągów Ruby. Jest to odpowiednik „# {$$}”. Ale interpolacja ciągów nie zachodzi w przypadku ciągów wprowadzanych przez użytkownika. Wypróbuj je w Irb, aby zobaczyć różnicę: "$##"i '$##'. Pierwszy jest interpolowany (podwójne cudzysłowy). Drugi nie jest. Wprowadzane przez użytkownika dane nigdy nie są interpolowane.
Brian Morearty

5
Wystarczy zauważyć, że find(:first)jest przestarzałe, a teraz można skorzystać z tej opcji #first. Tak więcProduct.first(conditions: [ "lower(name) = ?", name.downcase ])
Luís Ramalho

2
Nie musisz wykonywać całej tej pracy. Skorzystaj z wbudowanej biblioteki Arel lub Squeel
Dogweather

17
W Rails 4 możesz teraz zrobićmodel = Product.where('lower(name) = ?', name.downcase).first_or_create
Derek Lucas

1
@DerekLucas, chociaż jest to możliwe w Rails 4, ta metoda może spowodować nieoczekiwane zachowanie. Załóżmy, że after_createw Productmodelu mamy wywołanie zwrotne, a wewnątrz wywołania zwrotnego mamy whereklauzulę, np products = Product.where(country: 'us'). W takim przypadku whereklauzule są łączone łańcuchowo, gdy wywołania zwrotne są wykonywane w kontekście zakresu. Po prostu dla ciebie.
elquimista

100

To jest kompletna konfiguracja w Railsach, dla mojego własnego odniesienia. Cieszę się, że ci to pomaga.

Zapytanie:

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

walidator:

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

indeks (odpowiedź z unikatowego dla wielkości liter unikalnego indeksu w Rails / ActiveRecord? ):

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

Żałuję, że nie było piękniejszego sposobu na zrobienie pierwszego i ostatniego, ale z drugiej strony Rails i ActiveRecord są oprogramowaniem typu open source, nie powinniśmy narzekać - możemy to zaimplementować i wysłać prośbę pull.


6
Dzięki za stworzenie indeksu bez rozróżniania wielkości liter w PostgreSQL. Podziękowania dla Ciebie za pokazanie, jak używać go w Railsach! Jedna dodatkowa uwaga: jeśli używasz standardowej wyszukiwarki, np. Find_by_name, nadal dokładnie pasuje. Musisz napisać niestandardowe wyszukiwarki, podobne do powyższego wiersza „zapytanie”, jeśli chcesz, aby wyszukiwanie nie uwzględniało wielkości liter.
Mark Berry

Biorąc pod uwagę, że find(:first, ...)obecnie jest on przestarzały, uważam, że jest to najbardziej właściwa odpowiedź.
użytkownik

czy nazwa.downcase jest potrzebna? Wygląda na to, że współpracuje zProduct.where("lower(name) = ?", name).first
Jordan

1
@Jordan, próbowałeś tego z nazwiskami wielkimi literami?
oma

1
@Jordan, być może nie jest to zbyt ważne, ale powinniśmy dążyć do dokładności SO, ponieważ pomagamy innym :)
oma

28

Jeśli korzystasz z Postegres i Rails 4+, masz opcję użycia typu kolumny CITEXT, który pozwoli na zapytania bez rozróżniania wielkości liter bez konieczności zapisywania logiki zapytań.

Migracja:

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

Aby to przetestować, należy spodziewać się:

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

Możesz użyć następujących:

validates_uniqueness_of :name, :case_sensitive => false

Pamiętaj, że domyślnie jest to ustawienie: case_sensitive => false, więc nie musisz nawet pisać tej opcji, jeśli nie zmieniłeś innych sposobów.

Znajdź więcej na: http://api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html#method-i-validates_uniqueness_of


5
Z mojego doświadczenia wynika, że ​​w przeciwieństwie do dokumentacji, case_sensitive jest domyślnie prawdziwy. Widziałem takie zachowanie w postgresql i inni zgłaszali to samo w mysql.
Troy,

1
więc próbuję tego z postgres i to nie działa. find_by_x rozróżnia małe i wielkie litery bez względu na ...
Louis Sayers,

Ta walidacja ma miejsce tylko podczas tworzenia modelu. Jeśli więc masz w bazie danych „HAML” i próbujesz dodać „haml”, nie przejdzie on weryfikacji.
Dudo

14

W postgresie:

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

1
Szyny na Heroku, więc korzystanie z Postgres… ILIKE jest genialny. Dziękuję Ci!
FeifanZ

Zdecydowanie używając ILIKE na PostgreSQL.
Dom,

12

Kilka komentarzy odnosi się do Arel, bez podania przykładu.

Oto przykład Arel wyszukiwania bez rozróżniania wielkości liter:

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

Zaletą tego rodzaju rozwiązania jest to, że jest niezależne od bazy danych - użyje poprawnych poleceń SQL dla bieżącego adaptera ( matchesbędzie używał ILIKEPostgres i LIKEwszystkich innych).


9

Cytowanie z dokumentacji SQLite :

Każdy inny znak pasuje do siebie lub jego odpowiednika z małymi / dużymi literami (tzn. Dopasowanie bez rozróżniania wielkości liter)

... czego nie wiedziałem, ale to działa:

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

Więc możesz zrobić coś takiego:

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

Nie #find_or_create, wiem, i może nie jest zbyt przyjazny dla wielu baz danych, ale warto na to spojrzeć?


1
Podobnie jest w przypadku mysql rozróżniana jest wielkość liter, ale nie w postgresql. Nie jestem pewien co do Oracle lub DB2. Chodzi o to, że nie możesz na to liczyć, a jeśli go użyjesz, a szef zmieni bazową bazę danych, zaczniesz mieć „brakujące” rekordy bez wyraźnego powodu. Niższa sugestia @ neutrino (prawdopodobnie) jest prawdopodobnie najlepszym sposobem na rozwiązanie tego problemu.
masukomi

6

Innym podejściem, o którym nikt nie wspominał, jest dodawanie wyszukiwarek bez rozróżniania wielkości liter do ActiveRecord :: Base. Szczegóły można znaleźć tutaj . Zaletą tego podejścia jest to, że nie trzeba modyfikować każdego modelu i nie trzeba dodawać lower()klauzuli do wszystkich zapytań bez rozróżniania wielkości liter, wystarczy użyć innej metody wyszukiwania.


Twoja strona, do której prowadzi łącze, umiera, podobnie jak Twoja odpowiedź.
Anthony

Jak przepowiedziała @Anthony, tak się stało. Link martwy.
XP84

3
@ XP84 Nie wiem, jak to jest istotne, ale naprawiłem link.
Alex Korban

6

Wielkie i małe litery różnią się tylko jednym bitem. Najbardziej efektywnym sposobem na ich przeszukanie jest zignorowanie tego bitu, a nie konwersja dolnej lub górnej itp. Zobacz słowa kluczowe COLLATIONdla MSSQL, sprawdź, NLS_SORT=BINARY_CIczy używasz Oracle itp.


4

Find_or_create jest teraz przestarzałe, powinieneś użyć Relacji AR zamiast plus first_or_create, tak jak poniżej:

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

Zwróci pierwszy dopasowany obiekt lub utworzy go dla ciebie, jeśli nie istnieje.



2

Jest tu wiele świetnych odpowiedzi, szczególnie @ oma's. Ale możesz jeszcze spróbować użyć niestandardowej serializacji kolumn. Jeśli nie przeszkadza ci to, że wszystko jest przechowywane małymi literami w bazie danych, możesz utworzyć:

# 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

Następnie w swoim modelu:

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

Zaletą tego podejścia jest to, że nadal możesz korzystać ze wszystkich zwykłych wyszukiwarek (w tym find_or_create_by) bez korzystania z niestandardowych zakresów, funkcji lub posiadanialower(name) = ? posiadania zapytań.

Minusem jest to, że tracisz informacje o obudowie w bazie danych.


2

Podobne do Andrewsa, który jest nr 1:

Coś, co zadziałało dla mnie to:

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

Eliminuje to trzeba zrobić #wherei #firstw tej samej kwerendy. Mam nadzieję że to pomoże!


1

Możesz również użyć takich zakresów poniżej, aby wzbudzić ich obawy i uwzględnić w modelach, które mogą ich potrzebować:

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

Następnie użyj takiego: Model.ci_find('column', 'value')



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

TypeError: Cannot visit Regexp
Dorian

@shilovk dzięki. Właśnie tego szukałem. I wyglądało to lepiej niż zaakceptowana odpowiedź stackoverflow.com/a/2220595/1380867
MZaragoza

Podoba mi się to rozwiązanie, ale jak udało Ci się ominąć błąd „Nie można odwiedzić Regexp”? Ja też to widzę.
Gayle,

0

Niektóre osoby używają LIKE lub ILIKE, ale pozwalają wyszukiwać wyrażenia regularne. Poza tym nie musisz wstawiać w Ruby małej litery. Możesz pozwolić, aby baza danych zrobiła to za Ciebie. Myślę, że może być szybciej. Również first_or_createmogą być stosowane po 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

Do tej pory zrobiłem rozwiązanie za pomocą Ruby. Umieść to w modelu produktu:

  #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

To da mi pierwszy produkt, w którym pasują nazwy. Lub zero.

>> 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
Jest to wyjątkowo nieefektywne w przypadku większego zestawu danych, ponieważ musi on załadować całą rzecz do pamięci. Chociaż nie jest to dla Ciebie problemem z zaledwie kilkoma wpisami, nie jest to dobra praktyka.
lambshaanxy
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.