it-swarm.com.de

Lösen von CASCADE-Zyklen beim Löschen mit Triggern auf MS SQL Server

Ich habe Code, der in PostgreSQL einwandfrei funktioniert, und muss ihn jetzt auf MS SQL Server portieren. Es handelt sich um Tabellen mit potenziellen Zyklen für Lösch-/Aktualisierungsereignisse, und SQL Server beschwert sich darüber:

-- 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));

Ich habe die von PostgreSQL akzeptierten ON DELETE/UPDATE-Einschränkungen auskommentiert, die das genaue Verhalten anzeigen, das ich in MS SQL Server reproduzieren möchte. Andernfalls wird folgende Fehlermeldung angezeigt:

Die Einführung der FOREIGN KEY-Einschränkung 'fk_t_link_child' in der Tabelle 't_link' kann zu Zyklen oder mehreren Kaskadenpfaden führen. Geben Sie ON DELETE NO ACTION oder ON UPDATE NO ACTION an oder ändern Sie andere FOREIGN KEY-Einschränkungen.

Also habe ich sie entfernt (entspricht NO ACTION aus der Dokumentation ) und beschlossen, den Trigger-Weg zu gehen (wie durch mehrere Sites angedeutet), um verwandte t_link Zeilen zu löschen, wenn der verwandte t_parent ist gelöscht:

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;

Was ich insgesamt versuche, ist:

  • alle t_child -Datensätze werden gelöscht, wenn der zugehörige t_parent- Datensatz gelöscht wird (ON DELETE CASCADE), und t_link-Datensätze, die sich auf gelöschte t_child-Dateien beziehen, werden ebenfalls gelöscht
  • alle t_link-Datensätze werden gelöscht, wenn der zugehörige t_parent-Datensatz gelöscht wird (ON DELETE CASCADE).
  • t_link.id_child wird auf NULL gesetzt, wenn der zugehörige t_child -Datensatz gelöscht wird oder ebenfalls gelöscht, wenn dies die Sache einfacher macht (ON DELETE SET NULL oder ON DELETE CASCADE)

Dann füge ich ein paar Testdaten ein und versuche:

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;

FEHLER: Die DELETE-Anweisung steht in Konflikt mit der REFERENCE-Einschränkung "fk_t_link_parent". Der Konflikt trat in der Datenbank "DBTest", Tabelle "dbo.t_link", Spalte 'id_parent' auf.

Ich vermute, das Problem ist, dass mein Trigger nicht aufgerufen wird, weil es passiert after das Löschen selbst, was mit der obigen Meldung fehlschlägt; und es gibt keinen BEFORE DELETE Trigger-Typ (der nach etwas klingen würde, das ich gerne hätte).

Jetzt muss ich sagen, dass die SQL alle von einem Java JPA-ähnlichen Programm generiert wird, das mit den verschiedenen DBMS fertig werden muss (eine Unterklasse für PostgreSQL, eine für SQL Server, ...) Daher sollte ich generisch bleiben: Ich kann keine ON DELETE CASCADE -Einschränkungen für eine Tabelle festlegen und Trigger (oder eine andere Methode, die Sie vielleicht kennen) mit anderen verwenden (ich könnte, aber auf Kosten einer Code-Überkomplexisierung, die ich versuche) vermeiden).

Der SQL Server ist ein Docker-Image , daher bin ich mir nicht sicher, ob ich irgendwo eine Debug-Ausgabe haben könnte (außer im Befehl sqlcmd). Wenn es relevant ist, ist die Version 2017.

Der einzige Ausweg, den ich sehe, besteht darin, die Referenzbeschränkung zu löschen und alles manuell mit Triggern zu behandeln. Aber dann: Was bringt es, Fremdschlüsseleinschränkungen zu haben?


[~ # ~] edit [~ # ~] : Nach Davids Antwort ​​sollte ich einige Punkte klarstellen:

Das SQL CREATE TABLE und CREATE TRIGGER wird jedes Mal durch Code generiert, wenn eine neue Tabelle hinzugefügt werden soll (aus einer SQL-unabhängigen Konfigurationsdatei). Da SQL Server aufgrund möglicher Zyklen die Erstellung von ON DELETE CASCADE-Einschränkungen ablehnen kann, habe ich beschlossen, nur die FOREIGN KEY-Einschränkung anzugeben und dann für jede Tabelle, die auf t_parent verweist, einen FOR DELETE- Trigger zu erstellen, der jeweils die CASCADE- oder SET NULL-Operation in eigenen Zeilen ausführt.

Der vorgeschlagene Trigger INSTEAD OF DELETE ist definitiv die Mechanik, nach der ich suche, aber es kann nur eine einzige Instanz eines solchen Triggers für eine Tabelle erstellt werden (was sinnvoll ist), sodass ich diesen Weg nicht gegangen bin.

Möglicherweise erstelle ich gespeicherte Prozeduren anstelle meiner aktuellen Trigger und aktualisiere den INSTEAD OF-Trigger jedes Mal, wenn eine neue Referenzierungstabelle (und Prozedur) hinzugefügt wird, wobei jede gespeicherte Prozedur aufgerufen wird.

5
Matthieu

Du bist nah dran. AFTER Trigger passieren nach Überprüfung von Fremdschlüsseleinschränkungen. Du brauchst also ein INSTEAD OF auslösen. Auf diese Weise können Sie die untergeordneten Tabellen ändern, bevor Sie DELETE für die Zieltabelle ausführen.

z.B

-- 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;

Auf diese Weise verwendet t_parent-> t_child-> t_link CASCADE DELETES und t_parent-> t_link wird vom INSTEAD OF-Trigger behandelt.

Der Einfachheit halber habe ich im SQL-Generator alle Fremdschlüsseleinschränkungen gelöscht und AFTER DELETE - Trigger für Tabellen verwendet, um das Löschen von t_link - Datensätzen besser zu adressieren (ich möchte sie löschen, wenn ihre parentndchild werden gelöscht/NULL):

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

Die Tabelle t_child Verliert sie CONSTRAINT xxx FOREIGN KEY Und wird durch einen Trigger AFTER DELETE Ersetzt:

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;

Gleiches gilt für t_link, Ein ON DELETE SET NULL, Der zu CASCADE befördert wird, wenn beide id_parentndid_child NULL sind (dh gelöscht/nicht vorhanden):

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;

Natürlich ist das wahrscheinlich weniger effizient als handgefertigte Einschränkungen, aber es ist (bis jetzt) ​​gut genug für meinen Anwendungsfall und vereinfacht den SQL-Generatorcode viel.

Ich denke, es gibt einige "graue" Verhaltensweisen, z. Wenn ein Datensatz id_child gelöscht (DELETE FROM t_link ...) und dann in der nächsten Zeile (UPDATE t_link SET id_child=NULL) auf NULL gesetzt wird, insbesondere in Bezug auf Transaktionen, scheint dies bei meinen Komponententests zu funktionieren .

1
Matthieu