Búsqueda sin distinción entre mayúsculas y minúsculas en el modelo Rails

211

Mi modelo de producto contiene algunos artículos.

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

Ahora estoy importando algunos parámetros del producto desde otro conjunto de datos, pero hay inconsistencias en la ortografía de los nombres. Por ejemplo, en el otro conjunto de datos, Blue jeanspodría escribirse Blue Jeans.

Quería hacerlo Product.find_or_create_by_name("Blue Jeans"), pero esto creará un nuevo producto, casi idéntico al primero. ¿Cuáles son mis opciones si quiero encontrar y comparar el nombre en minúsculas?

Los problemas de rendimiento no son realmente importantes aquí: solo hay entre 100 y 200 productos, y quiero ejecutar esto como una migración que importa los datos.

¿Algunas ideas?

Jesper Rønn-Jensen
fuente

Respuestas:

368

Probablemente tengas que ser más detallado aquí

name = "Blue Jeans"
model = Product.where('lower(name) = ?', name.downcase).first 
model ||= Product.create(:name => name)
alex.zherdev
fuente
55
El comentario de @botbot no se aplica a las cadenas de entrada del usuario. "# $$" es un acceso directo poco conocido para escapar de variables globales con interpolación de cadenas Ruby. Es equivalente a "# {$$}". Pero la interpolación de cadenas no sucede con las cadenas de entrada del usuario. Pruebe esto en Irb para ver la diferencia: "$##"y '$##'. El primero es interpolado (comillas dobles). El segundo no es. La entrada del usuario nunca se interpola.
Brian Morearty el
55
Solo para notar que find(:first)está en desuso, y la opción ahora es usar #first. Así,Product.first(conditions: [ "lower(name) = ?", name.downcase ])
Luís Ramalho
2
No necesitas hacer todo este trabajo. Utilice la biblioteca de Arel incorporada o Squeel
Dogweather
17
En Rails 4 ahora puedes hacerlomodel = Product.where('lower(name) = ?', name.downcase).first_or_create
Derek Lucas,
1
@DerekLucas, aunque es posible hacerlo en Rails 4, este método puede causar un comportamiento inesperado. Supongamos que tenemos after_createdevolución de llamada en el Productmodelo y dentro de la devolución de llamada, tenemos una wherecláusula, por ejemplo products = Product.where(country: 'us'). En este caso, las wherecláusulas se encadenan a medida que las devoluciones de llamada se ejecutan dentro del contexto del ámbito. Solo para tu información.
elquimista
100

Esta es una configuración completa en Rails, para mi propia referencia. Estoy feliz si te ayuda también.

la consulta:

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

el validador:

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

el índice (respuesta de índice único que no distingue entre mayúsculas y minúsculas en Rails / ActiveRecord? ):

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

Desearía que hubiera una forma más hermosa de hacer lo primero y lo último, pero, una vez más, Rails y ActiveRecord son de código abierto, no deberíamos quejarnos: podemos implementarlo nosotros mismos y enviar una solicitud de extracción.

oma
fuente
66
Gracias por el crédito en la creación del índice sin distinción entre mayúsculas y minúsculas en PostgreSQL. ¡Crédito para mostrarle cómo usarlo en Rails! Una nota adicional: si usa un buscador estándar, por ejemplo, find_by_name, todavía hace una coincidencia exacta. Debe escribir buscadores personalizados, similares a la línea de "consulta" anterior, si desea que su búsqueda no distinga entre mayúsculas y minúsculas.
Mark Berry
Teniendo en cuenta que find(:first, ...)ahora está en desuso, creo que esta es la respuesta más adecuada.
usuario
¿Se necesita name.downcase? Parece que funciona conProduct.where("lower(name) = ?", name).first
Jordan
1
@ Jordan ¿Has probado eso con nombres que tienen letras mayúsculas?
oma
1
@Jordan, tal vez no sea demasiado importante, pero deberíamos esforzarnos por la precisión en SO ya que estamos ayudando a otros :)
oma
28

Si está utilizando Postegres y Rails 4+, tiene la opción de usar el tipo de columna CITEXT, que permitirá consultas que no distinguen entre mayúsculas y minúsculas sin tener que escribir la lógica de la consulta.

La migración:

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

Y para probarlo, debe esperar lo siguiente:

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
fuente
21

Es posible que desee utilizar lo siguiente:

validates_uniqueness_of :name, :case_sensitive => false

Tenga en cuenta que, de forma predeterminada, la configuración es: case_sensitive => false, por lo que ni siquiera necesita escribir esta opción si no ha cambiado otras formas.

Encuentre más en: http://api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html#method-i-validates_uniqueness_of

Sohan
fuente
55
En mi experiencia, en contraste con la documentación, case_sensitive es verdadero por defecto. He visto ese comportamiento en postgresql y otros han informado lo mismo en mysql.
Troy el
1
así que estoy intentando esto con postgres, y no funciona. find_by_x distingue entre mayúsculas y minúsculas independientemente ...
Louis Sayers
Esta validación es solo cuando se crea el modelo. Entonces, si tiene 'HAML' en su base de datos e intenta agregar 'haml', no pasará validaciones.
Dudo
14

En postgres:

 user = User.find(:first, :conditions => ['username ~* ?', "regedarek"])
tomekfranek
fuente
1
Rails en Heroku, por lo que usar Postgres ... ILIKE es brillante. ¡Gracias!
FeifanZ
Definitivamente usando ILIKE en PostgreSQL.
Dom
12

Varios comentarios se refieren a Arel, sin proporcionar un ejemplo.

Aquí hay un ejemplo de Arel de una búsqueda que no distingue entre mayúsculas y minúsculas:

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

La ventaja de este tipo de solución es que es independiente de la base de datos: usará los comandos SQL correctos para su adaptador actual ( matcheslo usará ILIKEpara Postgres y LIKEpara todo lo demás).

Brad Werth
fuente
9

Citando de la documentación de SQLite :

Cualquier otro carácter coincide con sí mismo o con su equivalente en minúscula / mayúscula (es decir, coincidencia entre mayúsculas y minúsculas)

... que no sabía, pero funciona:

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

Entonces podrías hacer algo como esto:

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

No #find_or_create, lo sé, y puede que no sea muy compatible con todas las bases de datos, pero ¿vale la pena mirarlo?

Mike Woodhouse
fuente
1
like es sensible a mayúsculas y minúsculas en mysql pero no en postgresql. No estoy seguro acerca de Oracle o DB2. El punto es que no puede contar con él y si lo usa y su jefe cambia su base de datos subyacente, comenzará a tener registros "faltantes" sin una razón obvia por qué. La sugerencia inferior (nombre) de @neutrino es probablemente la mejor manera de abordar esto.
masukomi
6

Otro enfoque que nadie ha mencionado es agregar buscadores que no distingan entre mayúsculas y minúsculas en ActiveRecord :: Base. Los detalles se pueden encontrar aquí . La ventaja de este enfoque es que no tiene que modificar todos los modelos, y no tiene que agregar la lower()cláusula a todas sus consultas que no distinguen entre mayúsculas y minúsculas, solo utiliza un método de buscador diferente.

Alex Korban
fuente
cuando la página que enlaza muere, también lo hace su respuesta.
Anthony
Como @Anthony ha profetizado, así ha sucedido. Enlace muerto.
XP84
3
@ XP84 Ya no sé cuán relevante es esto, pero arreglé el enlace.
Alex Korban
6

Las letras mayúsculas y minúsculas difieren solo en un bit. La forma más eficiente de buscarlos es ignorar este bit, no convertirlo en inferior o superior, etc. Vea las palabras clave COLLATIONpara MSSQL, vea NLS_SORT=BINARY_CIsi usa Oracle, etc.

Dean Radcliffe
fuente
4

Find_or_create ahora está en desuso, debe usar una relación AR en su lugar más first_or_create, así:

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

Esto devolverá el primer objeto coincidente, o creará uno para usted si no existe ninguno.

superluminario
fuente
2

Aquí hay muchas respuestas geniales, particularmente las de @ oma. Pero otra cosa que podría intentar es utilizar la serialización de columnas personalizada. Si no le importa que todo se almacene en minúsculas en su base de datos, puede crear:

# 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

Luego en tu modelo:

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

El beneficio de este enfoque es que aún puede usar todos los buscadores regulares (incluidos find_or_create_by) sin usar ámbitos personalizados, funciones o tenerlower(name) = ? en sus consultas.

La desventaja es que pierde información de la carcasa en la base de datos.

Nate Murray
fuente
2

Similar a Andrews, que es el # 1:

Algo que funcionó para mí es:

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

Esto elimina la necesidad de hacer una #wherey #firsten la misma consulta. ¡Espero que esto ayude!

Jonathan Fairbanks
fuente
1

También puede usar ámbitos como este a continuación y ponerlos en duda e incluirlos en los modelos que pueda necesitar:

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

Luego use así: Model.ci_find('column', 'value')

theterminalguy
fuente
0
user = Product.where(email: /^#{email}$/i).first
shilovk
fuente
TypeError: Cannot visit Regexp
Dorian
@shilovk gracias. Esto es exactamente lo que estaba buscando. Y se veía mejor que la respuesta aceptada stackoverflow.com/a/2220595/1380867
MZaragoza
Me gusta esta solución, pero ¿cómo pasó el error "No se puede visitar Regexp"? Yo también estoy viendo eso.
Gayle
0

Algunas personas muestran usando LIKE o ILIKE, pero esas permiten búsquedas de expresiones regulares. Además, no es necesario minimizar en Ruby. Puede dejar que la base de datos lo haga por usted. Creo que puede ser más rápido. También first_or_createse puede usar después 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 pies Dan
fuente
0

Una alternativa puede ser

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

Hasta ahora, hice una solución usando Ruby. Coloque esto dentro del modelo del producto:

  #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

Esto me dará el primer producto donde coinciden los nombres. O nada.

>> 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
fuente
2
Eso es extremadamente ineficiente para un conjunto de datos más grande, ya que tiene que cargar todo en la memoria. Si bien no es un problema para usted con solo unos pocos cientos de entradas, esta no es una buena práctica.
lambshaanxy