Ricerca senza distinzione tra maiuscole e minuscole nel modello Rails


211

Il mio modello di prodotto contiene alcuni articoli

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

Ora sto importando alcuni parametri del prodotto da un altro set di dati, ma ci sono incongruenze nell'ortografia dei nomi. Ad esempio, nell'altro set di dati, Blue jeanspotrebbe essere scritto Blue Jeans.

Volevo Product.find_or_create_by_name("Blue Jeans"), ma questo creerà un nuovo prodotto, quasi identico al primo. Quali sono le mie opzioni se voglio trovare e confrontare il nome in minuscolo.

I problemi di prestazioni non sono molto importanti qui: ci sono solo 100-200 prodotti e voglio eseguirlo come una migrazione che importa i dati.

Qualche idea?

Risposte:


368

Probabilmente dovrai essere più dettagliato qui

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

5
Il commento di @ botbot non si applica alle stringhe di input dell'utente. "# $$" è una scorciatoia poco conosciuta per sfuggire alle variabili globali con interpolazione di stringhe Ruby. È equivalente a "# {$$}". Ma l'interpolazione delle stringhe non si verifica nelle stringhe di input dell'utente. Prova questi in Irb per vedere la differenza: "$##"e '$##'. Il primo è interpolato (virgolette doppie). Il secondo no. L'input dell'utente non viene mai interpolato.
Brian Morearty,

5
Solo per notare che find(:first)è deprecato, e l'opzione ora è da usare #first. Quindi,Product.first(conditions: [ "lower(name) = ?", name.downcase ])
Luís Ramalho,

2
Non devi fare tutto questo lavoro. Usa la libreria Arel integrata o Squeel
Dogweather il

17
In Rails 4 ora puoi faremodel = Product.where('lower(name) = ?', name.downcase).first_or_create
Derek Lucas

1
@DerekLucas anche se è possibile farlo in Rails 4, questo metodo potrebbe causare un comportamento imprevisto. Supponiamo di avere after_createcallback nel Productmodello e all'interno del callback abbiamo whereclausole, ad es products = Product.where(country: 'us'). In questo caso, le whereclausole sono concatenate mentre i callback vengono eseguiti nel contesto dell'ambito. Cordiali saluti.
elquimista

100

Questa è una configurazione completa in Rails, per mio riferimento. Sono felice se ti aiuta anche tu.

la query:

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

il validatore:

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

l'indice (risposta dall'indice univoco senza distinzione tra maiuscole e minuscole in Rails / ActiveRecord? ):

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

Vorrei che ci fosse un modo più bello di fare il primo e l'ultimo, ma poi di nuovo, Rails e ActiveRecord sono open source, non dovremmo lamentarci: possiamo implementarlo da soli e inviare una richiesta pull.


6
Grazie per il merito di aver creato l'indice senza distinzione tra maiuscole e minuscole in PostgreSQL. Ringraziamo te per aver mostrato come usarlo in Rails! Un'ulteriore nota: se usi un cercatore standard, ad esempio find_by_name, fa ancora una corrispondenza esatta. Devi scrivere cercatori personalizzati, simili alla riga "query" sopra, se vuoi che la tua ricerca non faccia distinzione tra maiuscole e minuscole.
Mark Berry,

Considerando che find(:first, ...)ora è deprecato, penso che questa sia la risposta più corretta.
utente

è necessario name.downcase? Sembra di lavoro conProduct.where("lower(name) = ?", name).first
Jordan

1
@Jordan ci hai provato con nomi con lettere maiuscole?
oma

1
@Jordan, forse non troppo importante, ma dovremmo cercare la precisione su SO mentre stiamo aiutando gli altri :)
oma

28

Se si utilizzano Postegres e Rails 4+, è possibile utilizzare il tipo di colonna CITEXT, che consentirà query senza distinzione tra maiuscole e minuscole senza dover scrivere la logica della query.

La migrazione:

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

E per provarlo dovresti aspettarti quanto segue:

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

Potresti voler usare quanto segue:

validates_uniqueness_of :name, :case_sensitive => false

Si noti che per impostazione predefinita l'impostazione è: case_sensitive => false, quindi non è nemmeno necessario scrivere questa opzione se non si sono modificati altri modi.

Maggiori informazioni su: http://api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html#method-i-validates_uniqueness_of


5
Nella mia esperienza, contrariamente alla documentazione, case_sensitive è vero per impostazione predefinita. Ho visto che il comportamento in postgresql e altri hanno riportato lo stesso in mysql.
Troia,

1
quindi sto provando questo con Postgres e non funziona. find_by_x fa distinzione tra maiuscole e minuscole a prescindere ...
Louis Sayers,

Questa convalida è solo durante la creazione del modello. Quindi se hai 'HAML' nel tuo database e provi ad aggiungere 'haml', non passerà le convalide.
Dudo,

14

In postgres:

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

1
Rotaie su Heroku, quindi usando Postgres ... ILIKE è geniale. Grazie!
FeifanZ,

Sicuramente usando ILIKE su PostgreSQL.
Dom

12

Numerosi commenti si riferiscono ad Arel, senza fornire un esempio.

Ecco un esempio di Arel di una ricerca senza distinzione tra maiuscole e minuscole:

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

Il vantaggio di questo tipo di soluzione è che è indipendente dal database: utilizzerà i comandi SQL corretti per la scheda corrente ( matchesverrà utilizzata ILIKEper Postgres e LIKEper tutto il resto).


9

Citando dalla documentazione di SQLite :

Qualsiasi altro carattere corrisponde a se stesso o al suo equivalente maiuscolo / minuscolo (ovvero corrispondenza non sensibile al maiuscolo / minuscolo)

... che non sapevo, ma funziona:

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

Quindi potresti fare qualcosa del genere:

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

Non #find_or_createlo so, e potrebbe non essere molto adatto a più database, ma vale la pena guardarlo?


1
come è case sensitive in mysql ma non in postgresql. Non sono sicuro di Oracle o DB2. Il punto è che non puoi contare su di esso e se lo usi e il tuo capo modifica il tuo db sottostante inizierai ad avere record "mancanti" senza un ovvio motivo per cui. Il suggerimento più basso (nome) di neutrino è probabilmente il modo migliore per affrontarlo.
Masukomi,

6

Un altro approccio che nessuno ha menzionato è quello di aggiungere rilevatori senza distinzione tra maiuscole e minuscole in ActiveRecord :: Base. I dettagli sono disponibili qui . Il vantaggio di questo approccio è che non è necessario modificare tutti i modelli e non è necessario aggiungere la lower()clausola a tutte le query senza distinzione tra maiuscole e minuscole, è sufficiente utilizzare un metodo finder diverso.


quando la pagina che link muore, così fa la tua risposta.
Anthony,

Come ha profetizzato @Anthony, così è successo. Link morto.
XP84,

3
@ XP84 Non so più quanto sia rilevante, ma ho corretto il collegamento.
Alex Korban,

6

Le lettere maiuscole e minuscole differiscono solo per un singolo bit. Il modo più efficiente per cercarli è ignorare questo bit, non convertire in basso o in alto, ecc. Vedi le parole chiave COLLATIONper MSSQL, vedi NLS_SORT=BINARY_CIse usi Oracle, ecc.


4

Find_or_create ora è deprecato, dovresti usare una relazione AR invece di first_or_create, in questo modo:

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

Ciò restituirà il primo oggetto corrispondente o ne creerà uno per te se non esiste.



2

Ci sono molte ottime risposte qui, in particolare @ oma's. Ma un'altra cosa che potresti provare è utilizzare la serializzazione personalizzata delle colonne. Se non ti dispiace che tutto venga archiviato in minuscolo nel tuo db, puoi creare:

# 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

Quindi nel tuo modello:

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

Il vantaggio di questo approccio è che è ancora possibile utilizzare tutti i rilevatori regolari (incluso find_or_create_by) senza utilizzare ambiti, funzioni personalizzate o senza avere lower(name) = ?domande.

Il rovescio della medaglia è che si perdono le informazioni sul case nel database.


2

Simile ad Andrews che è # 1:

Qualcosa che ha funzionato per me è:

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

Ciò elimina la necessità di eseguire una #wheree #firstnella stessa query. Spero che questo ti aiuti!


1

Puoi anche usare ambiti come questo qui sotto e metterli in una preoccupazione e includere nei modelli che potrebbero essere necessari:

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

Quindi utilizzare in questo modo: Model.ci_find('column', 'value')



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

TypeError: Cannot visit Regexp
Dorian

@shilovk grazie. Questo e 'esattamente quello che stavo cercando. E sembrava meglio di quanto la risposta accettata stackoverflow.com/a/2220595/1380867
MZaragoza

Mi piace questa soluzione, ma come hai superato l'errore "Impossibile visitare Regexp"? Lo vedo anche io.
Gayle,

0

Alcune persone mostrano usando LIKE o ILIKE, ma quelli consentono ricerche regex. Inoltre non è necessario effettuare il downcase in Ruby. Puoi lasciare che il database lo faccia per te. Penso che potrebbe essere più veloce. Inoltre first_or_createpuò essere usato dopo 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

Finora ho fatto una soluzione usando Ruby. Inseriscilo nel modello del prodotto:

  #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

Questo mi darà il primo prodotto in cui i nomi corrispondono. O 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
Questo è estremamente inefficiente per un set di dati più grande, poiché deve caricare l'intera cosa in memoria. Sebbene non sia un problema per te con solo poche centinaia di voci, questa non è una buona pratica.
lambshaanxy,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.