Suche ohne Berücksichtigung der Groß- und Kleinschreibung im Rails-Modell

211

Mein Produktmodell enthält einige Artikel

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

Ich importiere jetzt einige Produktparameter aus einem anderen Datensatz, aber es gibt Inkonsistenzen in der Schreibweise der Namen. Zum Beispiel könnte im anderen Datensatz Blue jeansgeschrieben werden Blue Jeans.

Ich wollte Product.find_or_create_by_name("Blue Jeans"), aber dadurch entsteht ein neues Produkt, das fast identisch mit dem ersten ist. Was sind meine Optionen, wenn ich den Namen in Kleinbuchstaben suchen und vergleichen möchte?

Leistungsprobleme sind hier nicht wirklich wichtig: Es gibt nur 100-200 Produkte, und ich möchte dies als Migration ausführen, die die Daten importiert.

Irgendwelche Ideen?

Jesper Rønn-Jensen
quelle

Antworten:

368

Sie müssen hier wahrscheinlich ausführlicher sein

name = "Blue Jeans"
model = Product.where('lower(name) = ?', name.downcase).first 
model ||= Product.create(:name => name)
alex.zherdev
quelle
5
Der Kommentar von @ botbot gilt nicht für Zeichenfolgen aus Benutzereingaben. "# $$" ist eine wenig bekannte Verknüpfung, um globale Variablen mit Ruby-String-Interpolation zu umgehen. Es entspricht "# {$$}". Zeichenfolgeninterpolation tritt jedoch nicht bei Zeichenfolgen auf, die vom Benutzer eingegeben werden. Versuchen Sie diese in Irb, um den Unterschied zu sehen: "$##"und '$##'. Der erste wird interpoliert (doppelte Anführungszeichen). Der zweite ist nicht. Benutzereingaben werden niemals interpoliert.
Brian Morearty
5
Nur um zu beachten, dass dies find(:first)veraltet ist und die Option jetzt verwendet werden kann #first. SoProduct.first(conditions: [ "lower(name) = ?", name.downcase ])
Luís Ramalho
2
Sie müssen diese ganze Arbeit nicht erledigen. Verwenden Sie die eingebaute Arel-Bibliothek oder Squeel
Dogweather
17
In Rails 4 können Sie jetzt tunmodel = Product.where('lower(name) = ?', name.downcase).first_or_create
Derek Lucas
1
@DerekLucas Obwohl dies in Rails 4 möglich ist, kann diese Methode ein unerwartetes Verhalten verursachen. Angenommen, wir haben einen after_createRückruf im ProductModell und innerhalb des Rückrufs haben wir eine whereKlausel, z products = Product.where(country: 'us'). In diesem Fall werden die whereKlauseln verkettet, wenn Rückrufe im Kontext des Bereichs ausgeführt werden. Nur zur Info.
Elquimista
100

Dies ist eine vollständige Einrichtung in Rails, als meine eigene Referenz. Ich bin froh, wenn es dir auch hilft.

die Abfrage:

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

der Validator:

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

der Index (Antwort vom eindeutigen Index ohne Berücksichtigung der Groß- und Kleinschreibung in Rails / ActiveRecord? ):

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

Ich wünschte, es gäbe eine schönere Möglichkeit, die erste und die letzte zu machen, aber andererseits sind Rails und ActiveRecord Open Source. Wir sollten uns nicht beschweren - wir können es selbst implementieren und Pull-Anfragen senden.

oma
quelle
6
Vielen Dank für die Anerkennung bei der Erstellung des Index ohne Berücksichtigung der Groß- und Kleinschreibung in PostgreSQL. Wir danken Ihnen, dass Sie gezeigt haben, wie man es in Rails verwendet! Ein zusätzlicher Hinweis: Wenn Sie einen Standardfinder verwenden, z. B. find_by_name, stimmt dieser immer noch genau überein. Sie müssen benutzerdefinierte Finder schreiben, ähnlich wie in der obigen Zeile "Abfrage", wenn bei der Suche die Groß- und Kleinschreibung nicht berücksichtigt werden soll.
Mark Berry
In Anbetracht dessen, dass dies find(:first, ...)jetzt veraltet ist, denke ich, dass dies die richtigste Antwort ist.
Benutzer
wird name.downcase benötigt? Es scheint zu funktionieren mitProduct.where("lower(name) = ?", name).first
Jordan
1
@ Jordan hast du das mit Namen versucht, die Großbuchstaben haben?
Oma
1
@ Jordan, vielleicht nicht zu wichtig, aber wir sollten uns um Genauigkeit bei SO bemühen, da wir anderen helfen :)
Oma
28

Wenn Sie Postegres and Rails 4+ verwenden, haben Sie die Möglichkeit, den Spaltentyp CITEXT zu verwenden, der Abfragen ohne Berücksichtigung der Groß- und Kleinschreibung ermöglicht, ohne die Abfragelogik ausschreiben zu müssen.

Die Migration:

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

Und um es zu testen, sollten Sie Folgendes erwarten:

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">
Viet
quelle
21

Möglicherweise möchten Sie Folgendes verwenden:

validates_uniqueness_of :name, :case_sensitive => false

Bitte beachten Sie, dass die Einstellung standardmäßig lautet: case_sensitive => false, sodass Sie diese Option nicht einmal schreiben müssen, wenn Sie keine anderen Methoden geändert haben.

Weitere Informationen finden Sie unter: http://api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html#method-i-validates_uniqueness_of

Sohan
quelle
5
Nach meiner Erfahrung ist case_sensitive im Gegensatz zur Dokumentation standardmäßig true. Ich habe dieses Verhalten in postgresql gesehen und andere haben dasselbe in mysql gemeldet.
Troy
1
Also versuche ich das mit Postgres und es funktioniert nicht. find_by_x unterscheidet unabhängig von der Groß- und Kleinschreibung ...
Louis Sayers
Diese Validierung erfolgt nur beim Erstellen des Modells. Wenn Sie also 'HAML' in Ihrer Datenbank haben und versuchen, 'haml' hinzuzufügen, werden die Validierungen nicht bestanden.
Dudo
14

In postgres:

 user = User.find(:first, :conditions => ['username ~* ?', "regedarek"])
tomekfranek
quelle
1
Schienen auf Heroku, also mit Postgres… ILIKE ist brillant. Danke dir!
FeifanZ
Auf jeden Fall ILIKE unter PostgreSQL verwenden.
Dom
12

Mehrere Kommentare beziehen sich auf Arel, ohne ein Beispiel anzugeben.

Hier ist ein Arel-Beispiel für eine Suche ohne Berücksichtigung der Groß- und Kleinschreibung:

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

Der Vorteil dieser Art von Lösung besteht darin, dass sie datenbankunabhängig ist - sie verwendet die richtigen SQL-Befehle für Ihren aktuellen Adapter ( matcheswird ILIKEfür Postgres und LIKEfür alles andere verwendet).

Brad Werth
quelle
9

Zitat aus der SQLite-Dokumentation :

Jedes andere Zeichen entspricht sich selbst oder seinem Äquivalent in Klein- / Großbuchstaben (dh Übereinstimmung ohne Berücksichtigung der Groß- und Kleinschreibung).

... was ich nicht wusste. Aber es funktioniert:

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

Sie könnten also so etwas tun:

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

Nicht #find_or_create, ich weiß, und es ist vielleicht nicht sehr datenbankübergreifend, aber einen Blick wert?

Mike Woodhouse
quelle
1
wie ist Groß- und Kleinschreibung in MySQL, aber nicht in Postgresql. Ich bin mir bei Oracle oder DB2 nicht sicher. Der Punkt ist, dass Sie sich nicht darauf verlassen können und wenn Sie es verwenden und Ihr Chef Ihre zugrunde liegende Datenbank ändert, werden Sie ohne einen offensichtlichen Grund "fehlende" Datensätze haben. Der niedrigere (Namens-) Vorschlag von @ neutrino ist wahrscheinlich der beste Weg, dies zu beheben.
Masukomi
6

Ein anderer Ansatz, den niemand erwähnt hat, ist das Hinzufügen von Findern ohne Berücksichtigung der Groß- und Kleinschreibung zu ActiveRecord :: Base. Details finden Sie hier . Der Vorteil dieses Ansatzes besteht darin, dass Sie nicht jedes Modell ändern müssen und die lower()Klausel nicht zu allen Abfragen hinzufügen müssen , bei denen die Groß- und Kleinschreibung nicht berücksichtigt wird. Stattdessen verwenden Sie einfach eine andere Finder-Methode.

Alex Korban
quelle
Wenn die Seite, auf die Sie verlinken, stirbt, stirbt auch Ihre Antwort.
Anthony
Wie @Anthony prophezeit hat, so ist es geschehen. Link tot.
XP84
3
@ XP84 Ich weiß nicht mehr, wie relevant das ist, aber ich habe den Link behoben.
Alex Korban
6

Groß- und Kleinbuchstaben unterscheiden sich nur um ein Bit. Der effizienteste Weg, sie zu durchsuchen, besteht darin, dieses Bit zu ignorieren, nicht das untere oder obere zu konvertieren usw. Siehe Schlüsselwörter COLLATIONfür MSSQL, prüfen, NLS_SORT=BINARY_CIob Oracle verwendet wird usw.

Dean Radcliffe
quelle
4

Find_or_create ist jetzt veraltet. Sie sollten stattdessen eine AR-Beziehung plus first_or_create verwenden, wie folgt:

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

Dadurch wird das erste übereinstimmende Objekt zurückgegeben oder eines für Sie erstellt, falls keines vorhanden ist.

überleuchtet
quelle
2

Hier gibt es viele gute Antworten, insbesondere bei @ oma. Sie können jedoch auch versuchen, eine benutzerdefinierte Spaltenserialisierung zu verwenden. Wenn es Ihnen nichts ausmacht, dass alles in Ihrer Datenbank in Kleinbuchstaben gespeichert wird, können Sie Folgendes erstellen:

# 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

Dann in Ihrem Modell:

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

Der Vorteil dieses Ansatzes besteht darin, dass Sie weiterhin alle regulären Finder (einschließlich find_or_create_by) verwenden können, ohne benutzerdefinierte Bereiche, Funktionen oder Funktionen zu verwendenlower(name) = ? Abfragen zu verwenden.

Der Nachteil ist, dass Sie Gehäuseinformationen in der Datenbank verlieren.

Nate Murray
quelle
2

Ähnlich wie Andrews, der # 1 ist:

Etwas, das für mich funktioniert hat, ist:

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

Dadurch entfällt die Notwendigkeit, eine #whereund #firstdieselbe Abfrage durchzuführen. Hoffe das hilft!

Jonathan Fairbanks
quelle
1

Sie können auch Bereiche wie diesen unten verwenden und sie in ein Anliegen aufnehmen und in Modelle aufnehmen, die Sie möglicherweise benötigen:

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

Dann verwenden Sie wie folgt: Model.ci_find('column', 'value')

der Terminalguy
quelle
0
user = Product.where(email: /^#{email}$/i).first
shilovk
quelle
TypeError: Cannot visit Regexp
Dorian
@ Shilovk danke. Genau das habe ich gesucht. Und es sah besser aus als die akzeptierte Antwort stackoverflow.com/a/2220595/1380867
MZaragoza
Ich mag diese Lösung, aber wie sind Sie über den Fehler "Regexp kann nicht besucht werden" hinweggekommen? Das sehe ich auch.
Gayle
0

Einige Leute zeigen mit LIKE oder ILIKE, aber diese erlauben Regex-Suchen. Außerdem müssen Sie in Ruby kein Downcase erstellen. Sie können die Datenbank dies für Sie tun lassen. Ich denke, es kann schneller sein. Auch first_or_createkann nach verwendet werden 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"> 
6ft Dan
quelle
0

Eine Alternative kann sein

c = Product.find_by("LOWER(name)= ?", name.downcase)
David Barrientos
quelle
-9

Bisher habe ich mit Ruby eine Lösung gefunden. Fügen Sie dies in das Produktmodell ein:

  #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

Dies gibt mir das erste Produkt, bei dem die Namen übereinstimmen. Oder null.

>> 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
quelle
2
Das ist für einen größeren Datensatz äußerst ineffizient, da das gesamte Objekt in den Speicher geladen werden muss. Mit nur wenigen hundert Einträgen ist dies zwar kein Problem für Sie, aber keine gute Vorgehensweise.
Lambshaanxy