Re: on delete set null

From: Elmar Boye (ElmarB_at_gmx.net)
Date: 03/11/05


Date: Fri, 11 Mar 2005 23:53:49 +0100

Hallo Philipp,

Philipp Schumann <phil@mokka.org> schrieb ...
> leider mußte ich feststellen, daß der (mir ansonsten sehr
> sympathische) SQL Server ON DELETE SET (NULL|DEFAULT) bei einem
> FOREIGN KEY nicht unterstützt.
>
> Nun versuche ich das ganze mit einem Trigger zu simulieren (auf SProcs
> möchte ich unbedingt verzichten). Problem hierbei: meine ganze
> Architektur ist schon ziemlich darauf ausgerichtet, zusammenhängende
> SQL-Befehle als einen Batch-Befehl abzusetzen (ohne EXECUTE, USE, GO
> etc., eher simpel mit dem SqlCommand in ADO.NET). SQL Server verlangt
> aber scheinbar, daß CREATE-TRIGGER-Anweisungen die ersten in einer
> Batch-Anweisung sind - so daß ich diesen nicht die entsprechenden
> CREATE TABLE voranstellen kann, in denen die Tabellen erzeugt werden,
> auf die sich der Trigger beziehen soll.

Soviel zu den erwähnten in der .NET NG erwähnten Kleinigkeiten...

> Muß ich etwa allen Ernstes deswegen zwei Befehle einzeln
> hintereinander absetzen?

... und einfache Architektur ist leichter gesagt als getan...
Der SQL Server möchte einen eigenen Batch haben, da gilt im übrigen
für CREATE DEFAULT, CREATE FUNCTION, CREATE PROCEDURE, CREATE RULE,
CREATE TRIGGER und CREATE VIEW - siehe "Batches" in der SQL Server
Dokumentation.

Praktisch sollte man allerdings jede Anweisung in einen eigenen Batch
packen, also für dort unten jedes CREATE TABLE und das CREATE Trigger
-> 3 * ExecuteNonQuery() ist besser.
Denn so funktioniert es auch bei einem Nicht-Batch-fähigen Treiber
(wie z. B. Jet/Access) durchgängiger.

> Warum?

Da diese Befehle mehrere SQL Anweisungen enthalten können.
Und der Parser eben keine ";" suchen will ;-).

Lösung sind also mehrere ExecuteNonQuery()
Und für die Architektur: In den SQL Text wie beim SQL Server,
die zu trennenden Batches in der Datei mit "GO" kennzeichnen.
Und dies via StreamReader trennen lassen - kleines C# Beispiel
unten dran.
Eine "richtige" Implementation könnte natürlich auch das ";"
verwenden, müsste aber auch den Inhalt parsen (um Semikola in
Zeichenketten aus dem Weg zu gehen).

> Oder hab ich einen Fehler drin
> irgendwo, und SQL Server weigert sich lediglich, mir diesen genauer
> zu erläutern?
>
> CREATE TABLE T (
> ID UNIQUEIDENTIFIER PRIMARY KEY);
>
> CREATE TABLE C(
> ID UNIQUEIDENTIFIER PRIMARY KEY,
> Foo UNIQUEIDENTIFIER,
> FOREIGN KEY (Foo) REFERENCES T (ID));
> -- (normally, ON DELETE SET NULL would be sufficient here...) :(
>
> CREATE TRIGGER TEST ON T FOR DELETE AS UPDATE C SET Foo = NULL FROM C
> AS CC JOIN T AS TT ON CC.Foo = TT.ID;

Und eine Mischung von FOREIGN KEY und Trigger funktioniert architektonisch
nicht. Denn der FOREIGN KEY wird immer zuerst geprüft und ist die dadurch
definierte Bedingung verletzt, wird ein Fehler erzeugt.
Der Trigger Code kommt gar nicht mehr zum Zuge, vergleiche:

INSERT INTO T (ID) VALUES (@guid)
INSERT INTO C (ID, Foo) VALUES (NEWID(), @guid)
DELETE FROM T WHERE ID = @guid

Lösung wäre hier: Alles via Trigger und der DELETE Trigger sollte
eher so aussehen:

CREATE TRIGGER TEST ON dbo.T
FOR DELETE
AS
 SET NOCOUNT ON
 UPDATE dbo.C
 SET Foo = NULL
 FROM dbo.C AS CC
 INNER JOIN deleted ON CC.Foo = deleted.ID;
GO

Und das "dbo" sollte man nicht nur der Schönheit halber ansehen,
sondern in seiner Architektur berücksichtigen, das es Datenbanken
mit Besitzern/Schemata gibt und simple (wie die Jet) die das nicht
kennen. Tipp: unten etwas mehr einbauen und oben %SCHEMAOWNER%.C
schreiben.

Gruss
Elmar

--

 public static void SqlExecuteScriptResource(
  SqlConnection connection,
  string resourceName)
 {
  System.Reflection.Assembly assembly = null;
  System.IO.StreamReader streamScript = null;
  try
  {
   assembly = System.Reflection.Assembly.GetExecutingAssembly();
   streamScript = new System.IO.StreamReader(
    assembly.GetManifestResourceStream(resourceName),
    true);

   SqlExecuteScriptStream(connection, streamScript);
  }
  finally
  {
   if (streamScript != null)
    streamScript.Close();
  }
 }

 public static void SqlExecuteScriptFile(
  SqlConnection connection,
  string scriptFilename)
 {
  // Optimist: Unicode oder ANSI (Windows) Codierung
  System.IO.StreamReader streamScript = null;
  try
  {
   streamScript = new System.IO.StreamReader(scriptFilename, true);

   SqlExecuteScriptStream(connection, streamScript);
  }
  finally
  {
   if (streamScript != null)
    streamScript.Close();
  }
 }

 public static void SqlExecuteScriptStream(
  SqlConnection connection,
  StreamReader streamScript)
 {
  System.Text.StringBuilder sb = new System.Text.StringBuilder(2048);
  string line;

  System.Data.ConnectionState IsClosed = connection.State;
  try
  {
   if (IsClosed == System.Data.ConnectionState.Closed)
    connection.Open();

   try
   {
    while(streamScript.Peek() > -1)
    {
     line = streamScript.ReadLine();
     if (String.Compare(line.TrimStart(), 0, "GO", 0, 2, true) == 0)
     {
      if (sb.Length > 0)
      {
       SqlExecuteBatch (connection, sb.ToString());
       sb.Length = 0;
      }
     }
     else
     {
      sb.Append(line);
      sb.Append("\r\n");
     }
    }
    if (sb.Length > 0)
     SqlExecuteBatch (connection, sb.ToString());
   }
   catch (Exception e)
   {
    // Console.WriteLine(e.ToString());
    throw e;
   }
   finally
   {
    streamScript.Close();
   }
  }
  finally
  {
   if (IsClosed == System.Data.ConnectionState.Closed
    && connection.State == System.Data.ConnectionState.Open)
    connection.Close();
  }
 }

 public static int SqlExecuteBatch(
  SqlConnection connection,
  string CommandText)
 {
  int recordsAffected = 0;
  if (CommandText == null || CommandText.Length == 0) return recordsAffected;

  System.Data.ConnectionState IsClosed = connection.State;
  try
  {
   if (IsClosed == System.Data.ConnectionState.Closed)
    connection.Open();

   using (SqlCommand cmd = new SqlCommand(CommandText, connection))
   {
    cmd.CommandTimeout = 0;
    recordsAffected = cmd.ExecuteNonQuery();
   }
  }
  catch(SqlException e)
  {
   // foreach (SqlError err in e.Errors)
   // Console.WriteLine ("Zeile {0}, Fehler {1}: {2}", err.LineNumber, err.Number, err.Message);
      throw e;
  }
  finally
  {
   if (IsClosed == System.Data.ConnectionState.Closed
    && connection.State == System.Data.ConnectionState.Open)
    connection.Close();
  }
  return recordsAffected;
 }



Relevant Pages

  • Re: on delete set null
    ... > für CREATE DEFAULT, CREATE FUNCTION, CREATE PROCEDURE, CREATE RULE, ... Alles klar - bei mir gibts nur Tables und Trigger, ... durch SQL Server! ... > SqlConnection connection, ...
    (microsoft.public.de.sqlserver)
  • Re: on delete set null
    ... Na gut, die Doku sagts noch mal ausführlich, bei Batchanfragen müssen CREATE ... Immer diese Extrawürste in SQL, ... im Trigger da unten hatte ich das DELETED versehentlich (bzw. ... > ID UNIQUEIDENTIFIER PRIMARY KEY); ...
    (microsoft.public.de.sqlserver)
  • Re: application roles
    ... I 've made a trigger on a SQL2000 database to test your solution with the ... But my login-account and database user appear in my logtable instead of the ... if you are on SQL 2005. ... Erland Sommarskog, SQL Server MVP, esquel@xxxxxxxxxxxxx ...
    (microsoft.public.sqlserver.security)
  • Re: CLR-Trigger und temp tables
    ... Meine Frage basiert auf dem Community-Webcast zum SQL Server 2005, ... Trigger nicht installieren kann. ... Ein kleines CLR Trigger Beispiel, dass ich mal in Beta Zeiten verwendet ...
    (microsoft.public.de.sqlserver)
  • Re: Trigger, Identify Insert, Update AND delete in one
    ... In Oracle you can use IF inserting... ... In practice you can write your trigger if you want to save some typing as: ... in SQL 2008, which uses the same mechanisms as replication, and is ... Erland Sommarskog, SQL Server MVP, esquel@xxxxxxxxxxxxx ...
    (comp.databases.ms-sqlserver)