Recherche insensible à la casse dans le modèle Rails

211

Mon modèle de produit contient des éléments

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

J'importe maintenant certains paramètres de produit d'un autre ensemble de données, mais il y a des incohérences dans l'orthographe des noms. Par exemple, dans l'autre jeu de données, Blue jeanspourrait être orthographié Blue Jeans.

Je le voulais Product.find_or_create_by_name("Blue Jeans"), mais cela va créer un nouveau produit, presque identique au premier. Quelles sont mes options si je veux trouver et comparer le nom en minuscule.

Les problèmes de performances ne sont pas vraiment importants ici: il n'y a que 100 à 200 produits, et je veux exécuter cela comme une migration qui importe les données.

Des idées?

Jesper Rønn-Jensen
la source

Réponses:

368

Vous devrez probablement être plus verbeux ici

name = "Blue Jeans"
model = Product.where('lower(name) = ?', name.downcase).first 
model ||= Product.create(:name => name)
alex.zherdev
la source
5
Le commentaire de @ botbot ne s'applique pas aux chaînes provenant de l'entrée utilisateur. "# $$" est un raccourci peu connu pour échapper des variables globales avec une interpolation de chaîne Ruby. C'est équivalent à "# {$$}". Mais l'interpolation de chaîne n'arrive pas aux chaînes entrées par l'utilisateur. Essayez-les dans Irb pour voir la différence: "$##"et '$##'. Le premier est interpolé (guillemets doubles). Le second ne l'est pas. L'entrée utilisateur n'est jamais interpolée.
Brian Morearty
5
Juste pour noter que find(:first)c'est obsolète, et l'option est maintenant d'utiliser #first. Ainsi,Product.first(conditions: [ "lower(name) = ?", name.downcase ])
Luís Ramalho
2
Vous n'avez pas besoin de faire tout ce travail. Utilisez la bibliothèque Arel intégrée ou
Squeel
17
Dans Rails 4, vous pouvez maintenant le fairemodel = Product.where('lower(name) = ?', name.downcase).first_or_create
Derek Lucas
1
@DerekLucas bien qu'il soit possible de le faire dans Rails 4, cette méthode peut provoquer un comportement inattendu. Supposons que nous ayons un after_createrappel dans le Productmodèle et à l'intérieur du rappel, nous avons une whereclause, par exemple products = Product.where(country: 'us'). Dans ce cas, les whereclauses sont chaînées lorsque les rappels s'exécutent dans le contexte de la portée. Juste FYI.
elquimista
100

Ceci est une configuration complète dans Rails, pour ma propre référence. Je suis content si ça vous aide aussi.

la requête:

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

le validateur:

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

l'index (réponse de l' index unique insensible à la casse dans Rails / ActiveRecord? ):

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

Je souhaite qu'il y ait une plus belle façon de faire la première et la dernière, mais là encore, Rails et ActiveRecord sont open source, nous ne devrions pas nous plaindre - nous pouvons l'implémenter nous-mêmes et envoyer une demande de tirage.

oma
la source
6
Merci pour le mérite d'avoir créé l'index non sensible à la casse dans PostgreSQL. Nous vous remercions d'avoir montré comment l'utiliser dans Rails! Une remarque supplémentaire: si vous utilisez un chercheur standard, par exemple find_by_name, il fait toujours une correspondance exacte. Vous devez écrire des moteurs de recherche personnalisés, similaires à votre ligne de "requête" ci-dessus, si vous souhaitez que votre recherche ne respecte pas la casse.
Mark Berry
Étant donné que find(:first, ...)c'est désormais obsolète, je pense que c'est la réponse la plus appropriée.
utilisateur
faut-il name.downcase? Il semble fonctionner avecProduct.where("lower(name) = ?", name).first
Jordan
1
@Jordan avez-vous essayé cela avec des noms en majuscules?
oma
1
@Jordan, peut-être pas trop important, mais nous devons nous efforcer d'être précis sur SO car nous aidons les autres :)
oma
28

Si vous utilisez Postegres et Rails 4+, vous avez la possibilité d'utiliser le type de colonne CITEXT, qui permettra des requêtes insensibles à la casse sans avoir à écrire la logique de requête.

La 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

Et pour le tester, vous devez vous attendre à ce qui suit:

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
la source
21

Vous pouvez utiliser les éléments suivants:

validates_uniqueness_of :name, :case_sensitive => false

Veuillez noter que par défaut, le paramètre est: case_sensitive => false, vous n'avez donc même pas besoin d'écrire cette option si vous n'avez pas changé d'autres façons.

Plus d'informations sur: http://api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html#method-i-validates_uniqueness_of

Sohan
la source
5
D'après mon expérience, contrairement à la documentation, case_sensitive est vrai par défaut. J'ai vu ce comportement dans postgresql et d'autres ont signalé le même dans mysql.
Troy
1
donc j'essaye avec postgres, et ça ne marche pas. find_by_x est sensible à la casse indépendamment ...
Louis Sayers
Cette validation est uniquement lors de la création du modèle. Donc, si vous avez «HAML» dans votre base de données et que vous essayez d'ajouter «haml», il ne passera pas les validations.
Dudo
14

En postgres:

 user = User.find(:first, :conditions => ['username ~* ?', "regedarek"])
tomekfranek
la source
1
Rails sur Heroku, donc en utilisant Postgres… ILIKE est génial. Je vous remercie!
FeifanZ
Certainement en utilisant ILIKE sur PostgreSQL.
Dom
12

Plusieurs commentaires se réfèrent à Arel, sans donner d'exemple.

Voici un exemple Arel d'une recherche insensible à la casse:

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

L'avantage de ce type de solution est qu'elle est indépendante de la base de données - elle utilisera les commandes SQL correctes pour votre adaptateur actuel ( matchesutilisera ILIKEpour Postgres et LIKEpour tout le reste).

Brad Werth
la source
9

Citant de la documentation SQLite :

Tout autre caractère correspond à lui-même ou à son équivalent minuscule / majuscule (c.-à-d. Correspondance insensible à la casse)

... que je ne connaissais pas, mais ça marche:

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

Vous pouvez donc faire quelque chose comme ceci:

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_create, je sais, et ce n'est peut-être pas très convivial pour les bases de données, mais ça vaut le coup d'être examiné?

Mike Woodhouse
la source
1
comme est sensible à la casse dans mysql mais pas dans postgresql. Je ne suis pas sûr d'Oracle ou DB2. Le fait est que vous ne pouvez pas compter dessus et si vous l'utilisez et que votre patron change votre base de données sous-jacente, vous commencerez à avoir des enregistrements "manquants" sans raison évidente. La suggestion inférieure de @ neutrino (nom) est probablement la meilleure façon de résoudre ce problème.
masukomi
6

Une autre approche que personne n'a mentionnée consiste à ajouter des chercheurs insensibles à la casse dans ActiveRecord :: Base. Les détails peuvent être trouvés ici . L'avantage de cette approche est que vous n'avez pas à modifier chaque modèle et que vous n'avez pas à ajouter la lower()clause à toutes vos requêtes insensibles à la casse, vous utilisez simplement une méthode de recherche différente à la place.

Alex Korban
la source
lorsque la page que vous liez meurt, votre réponse aussi.
Anthony
Comme @Anthony l'a prophétisé, cela s'est passé. Lien mort.
XP84
3
@ XP84 Je ne sais plus à quel point c'est pertinent, mais j'ai corrigé le lien.
Alex Korban
6

Les lettres majuscules et minuscules ne diffèrent que par un seul bit. Le moyen le plus efficace de les rechercher est d'ignorer ce bit, de ne pas convertir en bas ou en haut, etc. Voir les mots-clés COLLATIONpour MSSQL, voir NLS_SORT=BINARY_CIsi vous utilisez Oracle, etc.

Dean Radcliffe
la source
4

Find_or_create est maintenant obsolète, vous devez utiliser une relation AR à la place plus first_or_create, comme ceci:

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

Cela renverra le premier objet correspondant ou en créera un pour vous s'il n'en existe aucun.

superluminaire
la source
2

Il y a beaucoup de bonnes réponses ici, en particulier chez @ oma. Mais une autre chose que vous pouvez essayer est d'utiliser la sérialisation de colonne personnalisée. Si cela ne vous dérange pas que tout soit stocké en minuscules dans votre base de données, vous pouvez créer:

# 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

Puis dans votre modèle:

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

L'avantage de cette approche est que vous pouvez toujours utiliser tous les moteurs de recherche standard (y compris find_or_create_by) sans utiliser d'étendues, de fonctions personnalisées oulower(name) = ? dans vos requêtes.

L'inconvénient est que vous perdez les informations de boîtier dans la base de données.

Nate Murray
la source
2

Similaire à Andrews qui est n ° 1:

Quelque chose qui a fonctionné pour moi est:

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

Cela élimine le besoin de faire un #whereet #firstdans la même requête. J'espère que cela t'aides!

Jonathan Fairbanks
la source
1

Vous pouvez également utiliser des étendues comme celle-ci ci-dessous et les mettre en danger et les inclure dans les modèles dont vous pourriez avoir besoin:

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

Ensuite, utilisez comme ceci: Model.ci_find('column', 'value')

theterminalguy
la source
0
user = Product.where(email: /^#{email}$/i).first
shilovk
la source
TypeError: Cannot visit Regexp
Dorian
@shilovk merci. Ceci est exactement ce que je cherchais. Et cela semblait mieux que la réponse acceptée stackoverflow.com/a/2220595/1380867
MZaragoza
J'aime cette solution, mais comment avez-vous surmonté l'erreur "Impossible de visiter Regexp"? Je le vois aussi.
Gayle
0

Certaines personnes montrent qu'elles utilisent LIKE ou ILIKE, mais celles-ci autorisent les recherches d'expression régulière. De plus, vous n'avez pas besoin de downcase en Ruby. Vous pouvez laisser la base de données le faire pour vous. Je pense que cela pourrait être plus rapide. first_or_createPeut également être utilisé aprè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"> 
6ft Dan
la source
0

Une alternative peut être

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

Jusqu'à présent, j'ai fait une solution en utilisant Ruby. Placez-le dans le modèle de produit:

  #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

Cela me donnera le premier produit où les noms correspondent. Ou rien.

>> 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
la source
2
C'est extrêmement inefficace pour un ensemble de données plus important, car il doit charger le tout dans la mémoire. Bien que ce ne soit pas un problème pour vous avec seulement quelques centaines d'entrées, ce n'est pas une bonne pratique.
lambshaanxy