Pesquisa sem distinção entre maiúsculas e minúsculas no modelo Rails

211

Meu modelo de produto contém alguns itens

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

Agora estou importando alguns parâmetros do produto de outro conjunto de dados, mas há inconsistências na ortografia dos nomes. Por exemplo, no outro conjunto de dados, Blue jeanspoderia ser escrito Blue Jeans.

Eu queria Product.find_or_create_by_name("Blue Jeans"), mas isso criará um novo produto, quase idêntico ao primeiro. Quais são minhas opções se eu quiser encontrar e comparar o nome em minúsculas.

Os problemas de desempenho não são realmente importantes aqui: existem apenas 100-200 produtos e eu quero executá-lo como uma migração que importa os dados.

Alguma ideia?

Jesper Rønn-Jensen
fonte

Respostas:

368

Você provavelmente terá que ser mais detalhado aqui

name = "Blue Jeans"
model = Product.where('lower(name) = ?', name.downcase).first 
model ||= Product.create(:name => name)
alex.zherdev
fonte
5
O comentário do @ botbot não se aplica a strings da entrada do usuário. "# $$" é um atalho pouco conhecido para escapar de variáveis ​​globais com interpolação de string Ruby. É equivalente a "# {$$}". Mas a interpolação de strings não acontece com as strings de entrada do usuário. Experimente estes no Irb para ver a diferença: "$##"e '$##'. O primeiro é interpolado (aspas duplas). O segundo não é. A entrada do usuário nunca é interpolada.
Brian Morearty
5
Apenas observe que find(:first)está obsoleto e a opção agora é usar #first. Assim,Product.first(conditions: [ "lower(name) = ?", name.downcase ])
Luís Ramalho
2
Você não precisa fazer todo esse trabalho. Use a biblioteca interna da Arel ou Squeel
Dogweather
17
No Rails 4 agora você pode fazermodel = Product.where('lower(name) = ?', name.downcase).first_or_create
Derek Lucas
1
@DerekLucas, embora seja possível fazê-lo no Rails 4, esse método pode causar um comportamento inesperado. Suponha que tenhamos after_createretorno de chamada no Productmodelo e dentro do retorno de chamada, tenhamos wherecláusula, por exemplo products = Product.where(country: 'us'). Nesse caso, as wherecláusulas são encadeadas à medida que os retornos de chamada são executados no contexto do escopo. Apenas para sua informação.
Elquimista 4/16
100

Esta é uma configuração completa no Rails, para minha própria referência. Fico feliz se isso também ajudar.

A pergunta:

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

o validador:

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

o índice (resposta do índice exclusivo que não diferencia maiúsculas de minúsculas no Rails / ActiveRecord? ):

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

Eu gostaria que houvesse uma maneira mais bonita de fazer a primeira e a última, mas, novamente, o Rails e o ActiveRecord são de código aberto, não devemos reclamar - podemos implementá-lo nós mesmos e enviar solicitação de recebimento.

oma
fonte
6
Obrigado pelo crédito na criação do índice que não diferencia maiúsculas de minúsculas no PostgreSQL. Devolva-lhe o crédito por mostrar como usá-lo no Rails! Uma observação adicional: se você usar um localizador padrão, por exemplo, find_by_name, ele ainda fará uma correspondência exata. Você precisa escrever localizadores personalizados, semelhantes à sua linha de "consulta" acima, se quiser que sua pesquisa não diferencie maiúsculas de minúsculas.
Marque 'Berry
Considerando que find(:first, ...)agora está obsoleto, acho que esta é a resposta mais adequada.
utilizador
é necessário name.downcase? Parece que funciona comProduct.where("lower(name) = ?", name).first
Jordan
1
@ Jordan você já tentou isso com nomes com letras maiúsculas?
oma
1
@Jordan, talvez não muito importante, mas devemos nos esforçar para precisão no SO como estamos ajudando os outros :)
oma
28

Se você estiver usando Postegres e Rails 4+, terá a opção de usar o tipo de coluna CITEXT, que permitirá consultas sem distinção entre maiúsculas e minúsculas sem precisar escrever a lógica da consulta.

A migração:

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 para testá-lo, você deve esperar o seguinte:

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

Você pode querer usar o seguinte:

validates_uniqueness_of :name, :case_sensitive => false

Observe que, por padrão, a configuração é: case_sensitive => false, para que você nem precise escrever essa opção se não tiver alterado outras maneiras.

Encontre mais em: http://api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html#method-i-validates_uniqueness_of

Sohan
fonte
5
Na minha experiência, em contraste com a documentação, case_sensitive é verdadeiro por padrão. Eu já vi esse comportamento no postgresql e outros relataram o mesmo no mysql.
Troy
1
então estou tentando isso com o postgres e não funciona. find_by_x é sensível a maiúsculas, independentemente ...
Louis Sayers
Essa validação é apenas ao criar o modelo. Portanto, se você tiver 'HAML' em seu banco de dados e tentar adicionar 'haml', ele não passará nas validações.
Dudo 10/10
14

No postgres:

 user = User.find(:first, :conditions => ['username ~* ?', "regedarek"])
tomekfranek
fonte
1
Trilhos no Heroku, então usando o Postgres… ILIKE é brilhante. Obrigado!
FeifanZ 06/07/2013
Definitivamente usando o ILIKE no PostgreSQL.
Dom
12

Vários comentários se referem à Arel, sem fornecer um exemplo.

Aqui está um exemplo da Arel de uma pesquisa que não diferencia maiúsculas de minúsculas:

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

A vantagem desse tipo de solução é que ele é independente do banco de dados - ele usará os comandos SQL corretos para o seu adaptador atual ( matcheso ILIKEPostgres e LIKEtodo o resto).

Brad Werth
fonte
9

Citando a partir da documentação do SQLite :

Qualquer outro caractere corresponde a si mesmo ou a seu equivalente em maiúsculas / minúsculas (ou seja, correspondência que não diferencia maiúsculas de minúsculas)

... o que eu não sabia.Mas 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

Então você poderia fazer algo assim:

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

Não #find_or_create, eu sei, e pode não ser muito compatível com vários bancos de dados, mas vale a pena olhar?

Mike Woodhouse
fonte
1
like diferencia maiúsculas de minúsculas no mysql, mas não no postgresql. Não tenho certeza sobre Oracle ou DB2. O ponto é que você não pode contar com isso e se você o usar e seu chefe alterar seu banco de dados subjacente, você começará a ter registros "ausentes" sem uma razão óbvia. A sugestão mais baixa (nome) de neutrino é provavelmente a melhor maneira de resolver isso.
Masukomi
6

Outra abordagem mencionada por ninguém é adicionar localizadores que não diferenciam maiúsculas de minúsculas no ActiveRecord :: Base. Detalhes podem ser encontrados aqui . A vantagem dessa abordagem é que você não precisa modificar todos os modelos e não precisa adicionar a lower()cláusula a todas as consultas sem distinção entre maiúsculas e minúsculas; basta usar um método localizador diferente.

Alex Korban
fonte
quando a página que você vincula morre, o mesmo acontece com a sua resposta.
Anthony
Como profetizou o @Anthony, isso aconteceu. Link morto.
XP84 24/0718
3
@ XP84 Não sei mais o quanto isso é relevante, mas consertei o link.
precisa
6

Letras maiúsculas e minúsculas diferem apenas em um único bit. A maneira mais eficiente de procurá-los é ignorar esse bit, não converter inferior ou superior, etc. Veja palavras-chave COLLATIONpara MSSQL, veja NLS_SORT=BINARY_CIse está usando Oracle etc.

Dean Radcliffe
fonte
4

Find_or_create agora está obsoleto, você deve usar uma relação de AR e first_or_create, da seguinte forma:

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

Isso retornará o primeiro objeto correspondido ou criará um para você, se não houver nenhum.

superluminário
fonte
2

Há muitas ótimas respostas aqui, principalmente as da @ oma. Mas outra coisa que você pode tentar é usar a serialização de coluna personalizada. Se você não se importa de tudo ser armazenado em minúsculas no seu banco de dados, você pode criar:

# 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

Então no seu modelo:

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

O benefício dessa abordagem é que você ainda pode usar todos os localizadores regulares (incluindo find_or_create_by) sem usar escopos, funções ou ter lower(name) = ?em suas consultas.

A desvantagem é que você perde as informações da caixa no banco de dados.

Nate Murray
fonte
2

Semelhante ao Andrews, que é o número 1:

Algo que funcionou para mim é:

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

Isso elimina a necessidade de fazer uma #wheree #firstna mesma consulta. Espero que isto ajude!

Jonathan Fairbanks
fonte
1

Você também pode usar escopos como este abaixo e colocá-los em uma preocupação e incluir nos modelos que podem ser necessários:

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

Então use assim: Model.ci_find('column', 'value')

theterminalguy
fonte
0
user = Product.where(email: /^#{email}$/i).first
shilovk
fonte
TypeError: Cannot visit Regexp
Dorian
@shilovk thanks. Era exatamente isso que eu estava procurando. E parecia melhor do que a resposta aceita stackoverflow.com/a/2220595/1380867
MZaragoza
Eu gosto dessa solução, mas como você passou pelo erro "Não é possível visitar o Regexp"? Eu também estou vendo isso.
Gayle
0

Algumas pessoas mostram usando LIKE ou ILIKE, mas elas permitem pesquisas de expressões regulares. Além disso, você não precisa fazer downcase no Ruby. Você pode deixar o banco de dados fazer isso por você. Eu acho que pode ser mais rápido. Também first_or_createpode ser usado depois 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
fonte
0

Uma alternativa pode ser

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

Até agora, eu fiz uma solução usando Ruby. Coloque isso dentro do modelo do produto:

  #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

Isso me dará o primeiro produto em que os nomes correspondem. Ou 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
fonte
2
Isso é extremamente ineficiente para um conjunto de dados maior, pois ele precisa carregar tudo na memória. Embora não seja um problema para você, com apenas algumas centenas de entradas, isso não é uma boa prática.
lambshaanxy