2010-11-05 8 views
1

Je développe en utilisant symfony 1.4 (et Doctrine) et ai une table de base de données MySQL avec un index unique sur plusieurs colonnes. Tout d'abord, la définition YAML de la table jusqu'à présent:unique par plusieurs colonnes et valeurs NULL

Campaign: 
    actAs: 
    Sluggable: 
     fields: [name] 
     canUpdate: true 
     uniqueBy: [merchant_id, deleted_at] 
    Timestampable: ~ 
    SoftDelete: ~ 
    columns: 
    merchant_id:  { type: integer, notnull: true } 
    name:    { type: string(255), notnull: true, notblank: true } 
    start_date:  { type: date, notnull: true, notblank: true } 
    end_date:   { type: date, notnull: true, notblank: true } 
    indexes: 
    unique_name: { fields: [name, merchant_id, deleted_at], type: unique } 
    relations: 
    Merchant: { local: merchant_id, foreign: id } 

Comme vous pouvez le voir, je dois traiter avec des campagnes appartenant à des marchands. Une campagne connaît son marchand et a un nom (ainsi qu'une date de début et une date de fin). Le nom d'une campagne doit être unique - pas globalement mais pour ce marchand spécifique. Jusqu'à ici, j'aurais besoin d'un index unique sur le nom de la campagne et le marchand respectif. Mais comme la table "agit comme SoftDelete" et que l'utilisateur devrait être capable de créer une nouvelle campagne avec un nom qui existe déjà pour une campagne "soft-deleted", la colonne deleted_at doit également faire partie de l'index unique. Vous voyez, l'unicité du nom d'une campagne ne concerne que les campagnes non supprimées du marchand respectif.

Venons-en maintenant au problème réel : Comme colonne deleted_at est NULL pour toutes les campagnes non supprimées et les valeurs NULL dans un index unique sont toujours traités comme étant unique, toutes les campagnes sont autorisés à avoir des noms non-uniques - en le vrai sens. Je sais, cela s'applique aux tables MyISAM et InnoDB mais pas aux tables BDB. Cependant, passer à BDB n'est pas mon option préférée, si vous voyez ce que je veux dire.

Maintenant à venir à la question réelle: Quelles sont les autres options possibles en plus de changer le moteur MySQL à BDB? Une solution de contournement pourrait être de renommer une campagne qui est supprimée, par ex. name = 'DELETED AT ' + deleted_at + ': ' + name. Ceci, d'une part, aurait l'avantage que toutes les campagnes à suppression de contenu auraient des noms uniques même dans le cas où elles sont restaurées (réinitialisation deleted_at à NULL). La colonne deleted_at ne devrait plus faire partie de l'index unique et, par conséquent, toutes les campagnes (non supprimées, effacées et restaurées une seule fois) auraient un nom unique - concernant le marchand respectif. Mais, d'un autre côté, je ne pense pas que ce soit la solution la plus élégante. Quelles sont vos opinions et votre expertise à ce sujet?

Je vous remercie beaucoup et je suis heureux de vos contributions.
Flinsch.

Répondre

1

Je pense que vous pouvez garder votre structure de base, vous avez juste besoin d'un moyen de rendre deleted_at non NULL. Cela signifie que vous devrez lui donner un défaut. Une bonne valeur par défaut est 0, ou 0000-00-00 00:00:00.

Ma recommandation est d'ajouter une nouvelle colonne pour marquer si les lignes sont supprimées logiquement. Vous pouvez l'appeler "IS_DELETED". Puis ajoutez la valeur par défaut pour deleted_at et la rendre non nulle, et incluez is_deleted dans votre index unique.

Voici un exemple très simple de cette approche en action:

mysql> create table merchant(
    -> id int unsigned not null auto_increment, 
    -> name varchar(50) not null, 
    -> is_deleted tinyint not null default 0, 
    -> deleted_at datetime not null default 0, 
    -> primary key (id), 
    -> unique key name_and_deleted_at (name,is_deleted,deleted_at) 
    ->) ENGINE = InnoDB; 
Query OK, 0 rows affected (0.08 sec) 

mysql> 
mysql> -- successful inserts 
mysql> insert into merchant (name,is_deleted) values ('foo',0); 
Query OK, 1 row affected (0.00 sec) 

mysql> insert into merchant (name,is_deleted) values ('bar',0); 
Query OK, 1 row affected (0.00 sec) 

mysql> 
mysql> -- insert failure due to duplicate name 
mysql> insert into merchant (name,is_deleted) values ('foo',0); 
ERROR 1062 (23000): Duplicate entry 'foo-0-0000-00-00 00:00:00' for key 'name_and_deleted_at' 
mysql> -- logical delete 
mysql> update merchant set is_deleted = true, deleted_at = now() where name = 'foo'; 
Query OK, 1 row affected (0.00 sec) 
Rows matched: 1 Changed: 1 Warnings: 0 

mysql> -- now the insert succeeds 
mysql> insert into merchant (name,is_deleted) values ('foo',0); 
Query OK, 1 row affected (0.01 sec) 

mysql> 
mysql> -- show data 
mysql> select id,name,is_deleted,deleted_at 
    -> from merchant 
    -> order by id; 
+----+------+------------+---------------------+ 
| id | name | is_deleted | deleted_at   | 
+----+------+------------+---------------------+ 
| 1 | foo |   1 | 2010-11-05 13:54:17 | 
| 2 | bar |   0 | 0000-00-00 00:00:00 | 
| 4 | foo |   0 | 0000-00-00 00:00:00 | 
+----+------+------------+---------------------+ 
3 rows in set (0.00 sec) 
+1

Ike, merci pour votre réponse. J'ai eu une idée très similaire aussi. Et je pense que l'indicateur supplémentaire 'is_deleted' n'est pas vraiment nécessaire car je peux vérifier que' deleted_at' est 0 ou pas. Cependant, je l'ai déjà ignoré, parce que je ne sais pas exactement si définir manuellement 'deleted_at' à une valeur NOT NULL aurait des conséquences imprévues sur la génération de listes d'objets, etc. - comme' deleted_at' est quelque chose comme une «colonne magique "de symfony/Doctrine. Mais je pense que je vais essayer ... :) – Flinsch

+0

MySQL 5.7 peut vous aider avec des colonnes virtuelles !: http://stackoverflow.com/questions/42064759/how-to-do-unique-constraint-works- avec-null-valeur-dans-mysql/42291845 # 42291845 – Nothus

Questions connexes