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?

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)

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.


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">

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


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"])

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


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?


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.


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.


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.



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.


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!


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')



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

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"> 


-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 :)

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
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.