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?

Jesper Rønn-Jensen
źródło

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)
alex.zherdev
źródło
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.

oma
źródło
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">
Wietnam
źródło
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

Sohan
źródło
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"])
tomekfranek
źródło
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).

Brad Werth
źródło
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ć?

Mike Woodhouse
źródło
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.

Alex Korban
źródło
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.

Dean Radcliffe
źródło
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.

superluminarny
źródło
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.

Nate Murray
źródło
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!

Jonathan Fairbanks
źródło
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')

theterminalguy
źródło
0
user = Product.where(email: /^#{email}$/i).first
Shilovk
źródło
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"> 
6 stóp Dan
źródło
0

Alternatywą może być

c = Product.find_by("LOWER(name)= ?", name.downcase)
David Barrientos
źródło
-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 :)
Jesper Rønn-Jensen
źródło
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