web-dev-qa-db-fra.com

Résolution des cycles ON DELETE CASCADE avec des déclencheurs sur MS SQL Server

J'ai du code qui fonctionne bien dans PostgreSQL et je dois maintenant le porter sur MS SQL Server. Il implique des tables avec des cycles potentiels sur les événements de suppression/mise à jour et SQL Server s'en plaint:

-- TABLE t_parent
CREATE TABLE t_parent (m_id INT IDENTITY PRIMARY KEY NOT NULL, m_name nvarchar(450));

-- TABLE t_child
CREATE TABLE t_child (m_id INT IDENTITY PRIMARY KEY NOT NULL, m_name nvarchar(450),
    id_parent int CONSTRAINT fk_t_child_parent FOREIGN KEY REFERENCES t_parent(m_id)
    --ON DELETE CASCADE ON UPDATE CASCADE
);

-- TABLE t_link
CREATE TABLE t_link (m_id INT IDENTITY PRIMARY KEY NOT NULL,
    id_parent int CONSTRAINT fk_t_link_parent FOREIGN KEY REFERENCES t_parent(m_id)
    -- ON DELETE CASCADE ON UPDATE CASCADE
    , id_child int CONSTRAINT fk_t_link_child FOREIGN KEY REFERENCES t_child(m_id)
    -- ON DELETE SET NULL ON UPDATE CASCADE
    , link_name nvarchar(450));

J'ai commenté les contraintes ON DELETE/UPDATE Qui ont été acceptées par PostgreSQL, qui montrent le comportement exact que j'essaie de reproduire dans MS SQL Server, sinon j'obtiens l'erreur:

L'introduction de la contrainte FOREIGN KEY 'fk_t_link_child' sur la table 't_link' peut provoquer des cycles ou plusieurs chemins de cascade. Spécifiez ON DELETE NO ACTION ou ON UPDATE NO ACTION, ou modifiez d'autres contraintes FOREIGN KEY.

Je les ai donc supprimés (équivalent à NO ACTION De la documentation ) et j'ai décidé de suivre la voie de déclenchement (comme suggéré par plusieurs sites ) pour supprimer les t_link Lignes lorsque le t_parent Correspondant est supprimé:

CREATE TRIGGER trg_delete_CASCADE_t_link_id_parent ON t_parent AFTER DELETE AS BEGIN
    DELETE FROM t_link WHERE id_parent IN (SELECT m_id FROM DELETED)
END;

Ce que j'essaye d'avoir globalement c'est:

  • tous les enregistrements t_child supprimés lorsque leur enregistrement t_parent associé est supprimé (ON DELETE CASCADE), et les enregistrements t_link liés à la suppression t_child sont également supprimés
  • tous les enregistrements t_link sont supprimés lorsque leur enregistrement t_parent est supprimé (ON DELETE CASCADE)
  • t_link.id_child Défini sur NULL lorsque leur enregistrement t_child Associé est supprimé ou également supprimé, si cela facilite les choses (ON DELETE SET NULL ou ON DELETE CASCADE)

Ensuite, j'insère quelques données de test et j'essaie:

insert into t_parent (m_name) values('toto');
insert into t_link (id_parent, id_child, link_name) values (1, NULL, 'chan');
delete from t_parent where m_id = 1;

ERREUR: l'instruction DELETE est en conflit avec la contrainte REFERENCE "fk_t_link_parent". Le conflit s'est produit dans la base de données "DBTest", table "dbo.t_link", colonne 'id_parent'.

Je suppose que le problème est que mon déclencheur n'est pas appelé car il se produit après la suppression elle-même, qui échoue avec le message ci-dessus; et il n'y a pas de type de déclencheur BEFORE DELETE (ce qui ressemblerait à quelque chose que j'aimerais avoir).

Maintenant, je dois dire que le SQL est entièrement généré par un Java programme de type JPA qui doit faire face aux différents SGBD (une sous-classe pour PostgreSQL, une pour SQL Server, ...) donc je devrais rester générique: je ne peux pas mettre des contraintes ON DELETE CASCADE sur une table et utiliser des déclencheurs (ou toute autre méthode que vous connaissez peut-être) avec d'autres (je le pourrais, mais au prix d'une complexification du code qui J'essaye d'éviter).

Le SQL Server est un image Docker donc je ne suis pas sûr que je pourrais avoir une sortie de débogage quelque part (sauf dans la commande sqlcmd). Si c'est pertinent, la version est 2017.

Le seul moyen de sortir de cela que je vois est simplement de supprimer la contrainte de référence et de tout gérer manuellement avec des déclencheurs. Mais alors: quel est l'intérêt d'avoir des contraintes de clé étrangère?


[~ # ~] modifier [~ # ~] : Après réponse de David , je devrais clarifier quelques points:

Le SQL CREATE TABLE Et CREATE TRIGGER Est généré par le code, chaque fois qu'une nouvelle table doit être ajoutée (à partir d'un fichier de configuration SQL-agnostique). Comme SQL Server peut refuser de créer des contraintes ON DELETE CASCADE En raison de cycles potentiels, j'ai décidé d'indiquer simplement la contrainte FOREIGN KEY, Puis de faire en sorte que chaque table référençant t_parent Crée un FOR DELETE, chacun effectuant l'opération CASCADE ou SET NULL sur ses propres lignes.

Le déclencheur INSTEAD OF DELETE Suggéré est certainement la mécanique que je recherche, mais une seule instance de ce déclencheur peut être créée pour une table (ce qui est logique), donc je ne suis pas allé de cette façon.

Je pourrais finir par créer des procédures stockées au lieu de mes déclencheurs actuels et mettre à jour le déclencheur INSTEAD OF chaque fois qu'une nouvelle table de référence (et procédure) est ajoutée, en appelant chaque procédure stockée.

5
Matthieu

Tu es proche. AFTER des déclencheurs se produisent après vérification des contraintes de clé étrangère. Vous avez donc besoin d'un INSTEAD OF déclencheur. De cette façon, vous pouvez modifier les tables enfants avant d'effectuer la SUPPRESSION sur la table cible.

par exemple

-- TABLE t_parent
CREATE TABLE t_parent 
(
  m_id INT IDENTITY PRIMARY KEY NOT NULL, 
  m_name nvarchar(450)
);

-- TABLE t_child
CREATE TABLE t_child 
(
    m_id INT IDENTITY PRIMARY KEY NOT NULL, 
    m_name nvarchar(450),
    id_parent int CONSTRAINT fk_t_child_parent FOREIGN KEY REFERENCES t_parent(m_id)
      ON DELETE CASCADE ON UPDATE CASCADE
);

-- TABLE t_link
CREATE TABLE t_link (m_id INT IDENTITY PRIMARY KEY NOT NULL,
    id_parent int CONSTRAINT fk_t_link_parent FOREIGN KEY REFERENCES t_parent(m_id)
      ON DELETE NO ACTION
    , id_child int CONSTRAINT fk_t_link_child FOREIGN KEY REFERENCES t_child(m_id)
      ON DELETE CASCADE
    , link_name nvarchar(450));


    go

CREATE OR ALTER TRIGGER trg_delete_CASCADE_t_link_id_parent 
ON t_parent INSTEAD OF DELETE 
AS 
BEGIN

    SET NOCOUNT ON
    DELETE FROM t_link WHERE id_parent IN (SELECT m_id FROM DELETED);
    DELETE FROM t_parent WHERE m_id IN (SELECT m_id FROM DELETED);

END;

go

insert into t_parent (m_name) values('toto');
insert into t_link (id_parent, id_child, link_name) values (1, NULL, 'chan');
delete from t_parent where m_id = 1;

De cette façon, t_parent-> t_child-> t_link utilise CASCADE DELETES et t_parent-> t_link est géré par le déclencheur INSTEAD OF.

Pour plus de simplicité dans le générateur SQL, j'ai fini par supprimer toutes les contraintes de clé étrangère et j'ai utilisé AFTER DELETE se déclenche sur les tables pour mieux traiter la suppression de t_link enregistrements (je veux les supprimer lorsque leurs parentetchild sont supprimés/NULL):

CREATE TABLE t_parent (m_id INT IDENTITY PRIMARY KEY NOT NULL, 
    m_name NVARCHAR(450));

t_child la table la perd CONSTRAINT xxx FOREIGN KEY, remplacé par un AFTER DELETE déclencheur:

CREATE TABLE t_child (m_id INT IDENTITY PRIMARY KEY NOT NULL, 
    id_parent INT,
    m_name NVARCHAR(450));

-- ON DELETE SET NULL equivalent
CREATE TRIGGER trg_delete_nulls_t_child_t_parent ON t_parent AFTER DELETE AS BEGIN
    UPDATE t_child SET id_parent = NULL WHERE id_parent IN (SELECT m_id FROM DELETED);
END;

Pareil pour t_link, qui est un ON DELETE SET NULL promu à CASCADE si les deux id_parentetid_child sont NULL (c'est-à-dire supprimés/inexistants):

CREATE TABLE t_link (m_id INT IDENTITY PRIMARY KEY NOT NULL,
    id_parent INT, id_child INT,
    link_name NVARCHAR(450));

-- Trigger ON DELETE SET NULL when t_parent is deleted, or ON DELETE CASCADE if no linked t_child
CREATE TRIGGER trg_delete_nulls_t_link_t_parent ON t_parent AFTER DELETE AS BEGIN
    -- "Promotion" to CASCADE if id_child is also NULL
    DELETE FROM t_link WHERE id_child IS NULL AND id_parent IN (SELECT m_id FROM DELETED);
    -- ON DELETE SET NULL (that might not be triggered if the previous statement has deleted all the records)
    UPDATE t_link SET id_parent = NULL WHERE id_parent IN (SELECT m_id FROM DELETED);
END;

-- Same for t_child deletions vs. t_parent
CREATE TRIGGER trg_delete_nulls_t_link_t_child ON t_child AFTER DELETE AS BEGIN
    UPDATE t_link SET id_child = NULL WHERE id_child IN (SELECT m_id FROM DELETED);
    DELETE FROM t_link WHERE id_parent IS NULL AND id_child IN (SELECT m_id FROM DELETED);
END;

Bien sûr, c'est probablement moins efficace que d'avoir des contraintes artisanales, mais c'est assez bon (jusqu'à présent) pour mon cas d'utilisation et simplifie le code du générateur SQL beaucoup.

Je suppose qu'il y a des comportements "gris" par exemple quand un enregistrement id_child est supprimé (DELETE FROM t_link ...) puis NULL par la ligne suivante (UPDATE t_link SET id_child=NULL), surtout en ce qui concerne les transactions, mais cela semble fonctionner sur mes tests unitaires.

1
Matthieu