Apparo Fast Edit: Einfügen von Daten in mehreren Tabellen – TEIL 2: INSTEAD OF Trigger

Schreibbare Views mit INSTEAD OF Triggern in SQL Server

Manchmal hat ein Eingabetool eine einfache Einschränkung: Es kann nur gegen eine tabellenartige Struktur schreiben. Fachlich liegen die Daten aber in mehreren Tabellen. Genau hier können Views mit INSTEAD OF Triggern helfen.

In diesem Beispiel gibt es eine Produkttabelle mit Produktnummer und Preis. Zusätzlich gibt es eine Sprachentabelle mit Produktnummer, Sprachencode und Produktname. Der Produktname ist also sprachabhängig, der Preis dagegen ein globales Produktattribut.

Warum das für Apparo Fast Edit relevant ist

In Apparo Fast Edit basiert ein Business Case typischerweise auf einer zentralen schreibbaren Datenstruktur. Über Lookups können zwar zusätzliche Informationen aus anderen Tabellen angezeigt oder ausgewählt werden, das eigentliche Schreiben erfolgt aber auf die definierte Zielstruktur des Business Case.

Wenn die fachlichen Daten jedoch auf mehrere physische Tabellen verteilt sind, entsteht eine typische Herausforderung. Im Beispiel liegen Produktnummer und Preis in einer Produkttabelle, während die sprachabhängigen Produktnamen in einer separaten Sprachentabelle gespeichert werden.

LÖSUNG IN APPARO FAST EDIT

Auch in Apparo Fast Edit ist diese Anforderung einfach zu lösen indem nach der Eingabe der Daten ein UPDATE- oder INSERT-Skript aufgerufen wird. Diese Lösung stellen wir hier vor: Daten in mehrere Tabellen einfügen.

Für den Anwender soll das trotzdem wie eine einzige bearbeitbare Tabelle aussehen. Genau dafür kann eine SQL-View mit INSTEAD OF Triggern eingesetzt werden.

Apparo Fast Edit schreibt dann gegen die View. Die Datenbank übernimmt über die Trigger die technische Verteilung der Änderungen auf die eigentlichen Basistabellen. Für den Business Case bleibt das Modell einfach, während die Datenbankstruktur sauber normalisiert bleiben kann.

Der Vorteil: Der Anwender pflegt Produktnummer, Preis und Produktname in einer einzigen Oberfläche. Im Hintergrund wird der Preis in der Produkttabelle gespeichert, während der sprachabhängige Produktname in der Sprachentabelle gepflegt wird.

Das Datenmodell

Die Haupttabelle enthält die produktbezogenen Stammdaten:

CREATE TABLE dbo.Produkt (
    Produktnummer INT NOT NULL PRIMARY KEY,
    Preis DECIMAL(10,2) NOT NULL
);

Die Sprachentabelle enthält die sprachabhängigen Produktnamen:

CREATE TABLE dbo.ProduktSprache (
    Produktnummer INT NOT NULL,
    Sprachencode CHAR(2) NOT NULL,
    Produktname NVARCHAR(200) NOT NULL,

    CONSTRAINT PK_ProduktSprache
        PRIMARY KEY (Produktnummer, Sprachencode),

    CONSTRAINT FK_ProduktSprache_Produkt
        FOREIGN KEY (Produktnummer)
        REFERENCES dbo.Produkt (Produktnummer)
        ON DELETE CASCADE
);

Die Herausforderung

Ein deutscher Benutzer sieht deutsche Produktnamen. Ein englischer Benutzer sieht englische Produktnamen. Das klingt zunächst nach einer einfachen gefilterten View.

Problematisch wird es aber, wenn ein Produkt bereits existiert, jedoch noch keine englische Übersetzung vorhanden ist. Bei einer normalen inneren Verknüpfung würde der englische Benutzer das Produkt gar nicht sehen. Er könnte also annehmen, dass das Produkt noch nicht existiert.

Die bessere Lösung ist daher eine View mit LEFT JOIN. Dadurch werden Produktnummer und Preis immer angezeigt. Wenn der englische Produktname noch fehlt, bleibt dieser einfach leer.

View für englische Benutzer

CREATE VIEW dbo.v_Produkt_EN
AS
SELECT
    p.Produktnummer,
    p.Preis,
    CAST('EN' AS CHAR(2)) AS Sprachencode,
    ps.Produktname
FROM dbo.Produkt p
LEFT JOIN dbo.ProduktSprache ps
    ON ps.Produktnummer = p.Produktnummer
   AND ps.Sprachencode = 'EN';
GO

Wichtig ist hier: Der Filter auf EN steht im ON-Teil des Joins, nicht im WHERE-Teil. Würde man den Sprachfilter in die WHERE-Bedingung schreiben, würde der LEFT JOIN faktisch wieder wie ein INNER JOIN wirken.

Beispieldaten

INSERT INTO dbo.Produkt
VALUES (123, 10.00);

INSERT INTO dbo.ProduktSprache
VALUES (123, 'DE', N'Stuhl');

Das Produkt existiert also bereits. Es hat die Produktnummer 123, kostet 10,00 Euro und hat bisher nur einen deutschen Namen: Stuhl.

Ein englischer Benutzer sieht über die View trotzdem bereits das Produkt:

SELECT *
FROM dbo.v_Produkt_EN;

Das Ergebnis wäre sinngemäß:

Produktnummer | Preis | Sprachencode | Produktname
123           | 10.00 | EN           | NULL

Der Benutzer sieht also: Das Produkt existiert bereits. Nur der englische Produktname fehlt noch.

Schreiben auf die View

Damit ein Eingabetool gegen diese View schreiben kann, werden INSTEAD OF Trigger verwendet. Diese Trigger fangen INSERT, UPDATE und DELETE auf der View ab und führen stattdessen die passenden Änderungen auf den Basistabellen aus.

INSERT-Trigger

Beim Insert wird zuerst geprüft, ob das Produkt bereits existiert. Falls ja, wird der Preis aktualisiert. Falls nein, wird das Produkt neu angelegt. Anschließend wird der englische Produktname eingefügt oder aktualisiert.

CREATE TRIGGER dbo.trg_v_Produkt_EN_insert
ON dbo.v_Produkt_EN
INSTEAD OF INSERT
AS
BEGIN
    SET NOCOUNT ON;

    MERGE dbo.Produkt AS target
    USING (
        SELECT DISTINCT
            Produktnummer,
            Preis
        FROM inserted
    ) AS source
    ON target.Produktnummer = source.Produktnummer
    WHEN MATCHED THEN
        UPDATE SET Preis = source.Preis
    WHEN NOT MATCHED THEN
        INSERT (Produktnummer, Preis)
        VALUES (source.Produktnummer, source.Preis);

    MERGE dbo.ProduktSprache AS target
    USING (
        SELECT
            Produktnummer,
            Produktname
        FROM inserted
        WHERE Produktname IS NOT NULL
    ) AS source
    ON target.Produktnummer = source.Produktnummer
   AND target.Sprachencode = 'EN'
    WHEN MATCHED THEN
        UPDATE SET Produktname = source.Produktname
    WHEN NOT MATCHED THEN
        INSERT (Produktnummer, Sprachencode, Produktname)
        VALUES (source.Produktnummer, 'EN', source.Produktname);
END;
GO

UPDATE-Trigger

Beim Update wird der Preis in der Produkttabelle aktualisiert. Der Produktname wird in der Sprachentabelle für den Sprachencode EN aktualisiert oder neu eingefügt, falls er noch nicht existiert.

CREATE TRIGGER dbo.trg_v_Produkt_EN_update
ON dbo.v_Produkt_EN
INSTEAD OF UPDATE
AS
BEGIN
    SET NOCOUNT ON;

    IF UPDATE(Produktnummer)
    BEGIN
        RAISERROR('Produktnummer darf über diese View nicht geändert werden.', 16, 1);
        RETURN;
    END;

    UPDATE p
    SET p.Preis = i.Preis
    FROM dbo.Produkt p
    JOIN inserted i
        ON i.Produktnummer = p.Produktnummer;

    MERGE dbo.ProduktSprache AS target
    USING (
        SELECT
            Produktnummer,
            Produktname
        FROM inserted
        WHERE Produktname IS NOT NULL
    ) AS source
    ON target.Produktnummer = source.Produktnummer
   AND target.Sprachencode = 'EN'
    WHEN MATCHED THEN
        UPDATE SET Produktname = source.Produktname
    WHEN NOT MATCHED THEN
        INSERT (Produktnummer, Sprachencode, Produktname)
        VALUES (source.Produktnummer, 'EN', source.Produktname);
END;
GO

DELETE-Trigger

Beim Löschen wird zunächst der englische Spracheintrag entfernt. Danach wird geprüft, ob zu diesem Produkt noch irgendein Spracheintrag existiert. Falls keine Sprache mehr vorhanden ist, wird auch das Produkt selbst gelöscht.

CREATE TRIGGER dbo.trg_v_Produkt_EN_delete
ON dbo.v_Produkt_EN
INSTEAD OF DELETE
AS
BEGIN
    SET NOCOUNT ON;

    DELETE ps
    FROM dbo.ProduktSprache ps
    JOIN deleted d
        ON d.Produktnummer = ps.Produktnummer
    WHERE ps.Sprachencode = 'EN';

    DELETE p
    FROM dbo.Produkt p
    JOIN deleted d
        ON d.Produktnummer = p.Produktnummer
    WHERE NOT EXISTS (
        SELECT 1
        FROM dbo.ProduktSprache ps
        WHERE ps.Produktnummer = p.Produktnummer
    );
END;
GO

Beispiel: Englischen Produktnamen ergänzen

Der englische Benutzer sieht das Produkt bereits mit Preis, aber ohne Produktnamen. Er kann nun den englischen Namen ergänzen und bei Bedarf auch den Preis ändern.

UPDATE dbo.v_Produkt_EN
SET Produktname = N'Chair',
    Preis = 15.00
WHERE Produktnummer = 123;

Danach ist der Preis global auf 15.00 geändert und der englische Produktname Chair wurde ergänzt.

Wichtig: Der Preis liegt nicht sprachabhängig in der Sprachentabelle, sondern global in der Produkttabelle. Deshalb sehen anschließend auch andere Sprach-Views den neuen Preis.

Warum der LEFT JOIN wichtig ist

Ohne LEFT JOIN würde ein Benutzer nur Produkte sehen, für die bereits ein Spracheintrag in seiner Sprache existiert. Das kann fachlich irreführend sein.

Mit dem LEFT JOIN sieht der Benutzer dagegen auch Produkte, bei denen die Übersetzung noch fehlt. Dadurch erkennt er vorhandene Produktnummern und vorhandene Preise und ergänzt nur den fehlenden sprachabhängigen Anteil.

Vollständiges Skript

CREATE TABLE dbo.Produkt (
    Produktnummer INT NOT NULL PRIMARY KEY,
    Preis DECIMAL(10,2) NOT NULL
);
GO

CREATE TABLE dbo.ProduktSprache (
    Produktnummer INT NOT NULL,
    Sprachencode CHAR(2) NOT NULL,
    Produktname NVARCHAR(200) NOT NULL,

    CONSTRAINT PK_ProduktSprache
        PRIMARY KEY (Produktnummer, Sprachencode),

    CONSTRAINT FK_ProduktSprache_Produkt
        FOREIGN KEY (Produktnummer)
        REFERENCES dbo.Produkt (Produktnummer)
        ON DELETE CASCADE
);
GO

CREATE VIEW dbo.v_Produkt_EN
AS
SELECT
    p.Produktnummer,
    p.Preis,
    CAST('EN' AS CHAR(2)) AS Sprachencode,
    ps.Produktname
FROM dbo.Produkt p
LEFT JOIN dbo.ProduktSprache ps
    ON ps.Produktnummer = p.Produktnummer
   AND ps.Sprachencode = 'EN';
GO

CREATE TRIGGER dbo.trg_v_Produkt_EN_insert
ON dbo.v_Produkt_EN
INSTEAD OF INSERT
AS
BEGIN
    SET NOCOUNT ON;

    MERGE dbo.Produkt AS target
    USING (
        SELECT DISTINCT
            Produktnummer,
            Preis
        FROM inserted
    ) AS source
    ON target.Produktnummer = source.Produktnummer
    WHEN MATCHED THEN
        UPDATE SET Preis = source.Preis
    WHEN NOT MATCHED THEN
        INSERT (Produktnummer, Preis)
        VALUES (source.Produktnummer, source.Preis);

    MERGE dbo.ProduktSprache AS target
    USING (
        SELECT
            Produktnummer,
            Produktname
        FROM inserted
        WHERE Produktname IS NOT NULL
    ) AS source
    ON target.Produktnummer = source.Produktnummer
   AND target.Sprachencode = 'EN'
    WHEN MATCHED THEN
        UPDATE SET Produktname = source.Produktname
    WHEN NOT MATCHED THEN
        INSERT (Produktnummer, Sprachencode, Produktname)
        VALUES (source.Produktnummer, 'EN', source.Produktname);
END;
GO

CREATE TRIGGER dbo.trg_v_Produkt_EN_update
ON dbo.v_Produkt_EN
INSTEAD OF UPDATE
AS
BEGIN
    SET NOCOUNT ON;

    IF UPDATE(Produktnummer)
    BEGIN
        RAISERROR('Produktnummer darf über diese View nicht geändert werden.', 16, 1);
        RETURN;
    END;

    UPDATE p
    SET p.Preis = i.Preis
    FROM dbo.Produkt p
    JOIN inserted i
        ON i.Produktnummer = p.Produktnummer;

    MERGE dbo.ProduktSprache AS target
    USING (
        SELECT
            Produktnummer,
            Produktname
        FROM inserted
        WHERE Produktname IS NOT NULL
    ) AS source
    ON target.Produktnummer = source.Produktnummer
   AND target.Sprachencode = 'EN'
    WHEN MATCHED THEN
        UPDATE SET Produktname = source.Produktname
    WHEN NOT MATCHED THEN
        INSERT (Produktnummer, Sprachencode, Produktname)
        VALUES (source.Produktnummer, 'EN', source.Produktname);
END;
GO

CREATE TRIGGER dbo.trg_v_Produkt_EN_delete
ON dbo.v_Produkt_EN
INSTEAD OF DELETE
AS
BEGIN
    SET NOCOUNT ON;

    DELETE ps
    FROM dbo.ProduktSprache ps
    JOIN deleted d
        ON d.Produktnummer = ps.Produktnummer
    WHERE ps.Sprachencode = 'EN';

    DELETE p
    FROM dbo.Produkt p
    JOIN deleted d
        ON d.Produktnummer = p.Produktnummer
    WHERE NOT EXISTS (
        SELECT 1
        FROM dbo.ProduktSprache ps
        WHERE ps.Produktnummer = p.Produktnummer
    );
END;
GO

Fazit

Eine View mit LEFT JOIN und INSTEAD OF Triggern kann ein mehrtabelliges Datenmodell für ein einfaches Eingabetool wie eine normale Tabelle wirken lassen.

Der entscheidende Punkt ist die saubere fachliche Modellierung: Der Preis gehört zum Produkt und wird global geändert. Der Produktname gehört zur Sprache und wird je Sprachcode gepflegt. Die View verbindet beides zu einer einfach nutzbaren Eingabestruktur.

Jens Bäumler (Apparo Group)

Ähnliche Themen

WP Twitter Auto Publish Powered By : XYZScripts.com