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?

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)

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.


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

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


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

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


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?


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.


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.


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.



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.


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!


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



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

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


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

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
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.