web-dev-qa-db-fra.com

Est-il possible de spécifier un index unique avec des valeurs NULL autorisées dans Rails / ActiveRecord?

Je souhaite spécifier un index unique sur une colonne, mais je dois également autoriser les valeurs NULL (plusieurs enregistrements peuvent avoir des valeurs NULL). Lors des tests avec PostgreSQL, je vois que je peux avoir 1 enregistrement avec une valeur NULL, mais le suivant causera un problème:

irb(main):001:0> u=User.find(5)
  User Load (111.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1  [["id", 5]]
=> #<User id: 5, email: "[email protected]", created_at: "2013-08-28 09:55:28", updated_at: "2013-08-28 09:55:28">
irb(main):002:0> u.email=nil
=> nil
irb(main):003:0> u.save
   (1.1ms)  BEGIN
  User Exists (4.8ms)  SELECT 1 AS one FROM "users" WHERE ("users"."email" IS NULL AND "users"."id" != 5) LIMIT 1
   (1.5ms)  ROLLBACK
=> false

Donc, même si la base de données le permet, Rails vérifie d'abord si un User existe avec un identifiant différent et avec la colonne email définie sur NULL. Existe-t-il un moyen pour que non seulement la base de données le permette, mais Rails ne vérifiera pas en premier comme ci-dessus également?

L'idée est que les utilisateurs n'ont pas à saisir d'e-mail, mais s'ils le font, je dois pouvoir trouver un utilisateur par e-mail. Je sais que je peux créer un autre modèle pour associer les utilisateurs aux e-mails, mais je préfère de loin le faire de la manière ci-dessus.

UPDATE : Voici le code de migration que j'avais créé pour ajouter le email colonne:

class AddEmailToUsers < ActiveRecord::Migration
  def change
    add_column :users, :email, :string
    add_index :users, :email, :unique => true
  end
end

Et voici le code que j'avais ajouté au modèle User:

validates :email, uniqueness: true

J'ai oublié que j'avais ajouté l'appel validates au modèle User. Il est donc logique que Rails vérifie en premier. Je suppose que la seule autre question est de savoir s'il est sécuritaire pour les bases de données d'avoir un index unique et des champs NULL? Y a-t-il un moyen de spécifier dans Rails que je veux valider l'email est unique sauf s'il s'agit de nil?

33
at.

Votre migration fonctionnera et autorisera plusieurs valeurs null (pour la plupart des moteurs de base de données).

Mais votre validation pour la classe d'utilisateur devrait ressembler à ci-dessous.

validates :email, uniqueness: true, allow_nil: true
38
aross

Pour clarifier pourquoi cela fonctionne au niveau de la base de données, vous devez comprendre la logique à trois valeurs utilisée dans SQL: true, false, null.

null signifie généralement inconnu, donc sa sémantique dans les opérations équivaut généralement à ne pas savoir quelle est cette valeur particulière et à voir si vous pouvez toujours trouver une réponse. Ainsi par exemple 1.0 * null est null mais null OR true est true. Dans le premier cas, la multiplication par une inconnue est inconnue, mais dans le second, la seconde moitié du conditionnel rend l'énoncé entier toujours vrai, donc peu importe ce qui se trouve sur le côté gauche.

Maintenant, en ce qui concerne les index, la norme ne spécifie rien, les fournisseurs doivent donc interpréter ce que signifie inconnu. Personnellement, je pense qu'un index unique devrait être défini comme dans la documentation PostgreSQL:

Lorsqu'un index est déclaré unique, plusieurs lignes de table avec des valeurs indexées égales ne seront pas autorisées

La question devrait alors être de savoir quelle est la valeur de null = null? La bonne réponse doit être null. Donc, si vous lisez un peu entre les lignes de ces documents PostgreSQL et dites qu'un index unique interdira plusieurs lignes pour lesquelles l'opérateur d'égalité retourne true pour ladite valeur, alors plusieurs valeurs null devraient être autorisées. C'est exactement ainsi que fonctionne PostgreSQL, donc dans cette configuration, vous pouvez avoir une colonne unique avec plusieurs lignes ayant null comme valeur.

D'un autre côté, si vous vouliez interpréter la définition d'un index unique pour interdire plusieurs lignes pour lesquelles l'opérateur d'inégalité ne renvoie pas false, vous ne pourrez pas avoir plusieurs lignes avec des valeurs null. Qui choisirait d'opérer dans cette configuration contrapositive? C'est ainsi que Microsoft SQL Server choisit de définir un index unique.

Ces deux façons de définir un index unique sont correctes d'après la définition de null de la norme SQL 2003. Cela dépend donc vraiment de votre base de données sous-jacente. Mais cela étant dit, je pense que la majorité fonctionne de manière similaire à PostgreSQL.

16
rokob