Datenbankprogrammierung
 

Axel Kohnert
Lehrstuhl Mathematik 2
WS 2000/2001
 
 
 
 
 
 

Teil 2  Embedded SQL - C

 

 
 
 
 
 
 
 
 
 
 
 
 

2.1 Einführung


Embedded SQL für C ermöglicht die Erstellung von Datenbankapplikationen durch die Einbettung von SQL-Anweisungen in C/C++  Programme. Eine ESQLC Applikation kommuniziert mit dem Datenbankserver und ermöglicht so den Zugriff auf eine Datenbank. Die Programme werden durch eine precompiler in ein C Programm umgewandelt. Dabei werden header Dateien (.h - Dateien) und eine Bibliothek zur Verfügung gestellt, damit kann der normale C Compiler ein lauffähiges Programm erstellen. Vorausetzung für das Funktionieren des Programms später ist natürlich, dass auf dem System auch ein Datenbankserver vorhanden ist. Ferner ist es möglich Netzwerkroutinen zu linken, damit der Zugriff über Netz auf einen Datenbankserver möglich ist. Auch die Syntax von Embedded SQL für C ist standardisiert   (ANSI,  X/Open).
 
 
 

2.2 Grundlagen


Um eine oder mehrere SQL-Anweisungen in ein C Programm einzubetten, muß die SQL-Anweisung speziell gekennzeichnet werden. Dies geschieht durch die Schlüsselwörter EXEC SQL, die vor den Beginn jeder SQL- Anweisung gestellt werden müssen. Eine SQL-Anweisung muß mit einem Semikolon abgeschlossen werden.
 

Eine SQL-Anweisung in einem C Programm hat dementsprechend folgende, allgemeine Syntax:
 
 

EXEC SQL SQL-Anweisung;
 
 

Eine so eingebette SQL-Anweisung kann mehrere Zeilen umfassen, wobei auch beliebig viele Leerstellen, Tabulatoren und Leerzeilen vorkommen können.

Beispiel:

EXEC SQL CREATE TABLE base_table
(
        nr      INTEGER,        /* Kundennummer */
        name    CHAR(20)        /* Kundenname */
);

Man sieht auch die Verwendung von Kommentaren innerhalb der EXEC SQL Anweisung. Das ist erlaubt, der Kommentar darf nur nicht zwischen EXEC und SQL stehen. Da die EXEC SQL Anweisungen in C Anweisungen umgewandelt werden, dürfen sie innerhalb des C Codes nur da gesetzt werden wo auch C Anweisungen stehen dürfen.
 
 
 
 
 

2.3 Aufbau des C Programms


Ein  Programm kann neben normalen C Anweisungen 2 Arten von SQL-Anweisungen enthalten:
Deklarative SQL-Anweisungen, die in dem deklarativen Teil eines Programmes angegeben werden. Hierzu gehören die folgenden Anweisungen:
 


SQL-Anweisungen, die in dem prozedualen Teil eines Programmes vorkommen. Hierzu gehören die folgenden Anweisungen:
 

 

 

2.3.1 SQL DECLARE

Damit werden Variablen (hostvariablen) deklariert, die sowohl innerhalb der SQL Anweisungen als auch innerhalb des normalen C Codes verwendet werden. Diese Anweisung muss im Deklarationsteil des entsprechen C Blocks stehen. Der DECLARE Abschnitt wird mit den Schlüsselwörtern
 

EXEC SQL BEGIN DECLARE SECTION
 

eingeleitet und endet mit
 

EXEC SQL END DECLARE SECTION.
 

Ein ESQLC Programm kann beliebig viele DECLARE Abschnitte enthalten. Das folgende Beispiel zeigt einen SQL DECLARE Abschnitt in der
Funktion main:
 
 

main(int argc, char **argv)
{
EXEC SQL BEGIN DECLARE SECTION
        int i = argc;
        char **args = argv;
        int x, y, z;
        char *dbuser = "rdb";
EXEC SQL END DECLARE SECTION
}
 
 

2.3.2 INCLUDE


Die INCLUDE Anweisung dient dazu, Header Dateien in ein ESQLC-Modul einzufügen. Die allgemeine Form der INCLUDE Anweisung ist wie folgt:
 
 

EXEC SQL INCLUDE header-datei;
 

Der Inhalt der Header Datei wird vom Precompiler gelesen und in die C Datei eingefügt. Sofern die Header Datei SQL-Anweisungen enthält, werden diese in C Code übersetzt.  Das würde nicht mit dem normalen #include Befehl aus dem C-Präprozessor funktionieren.
 
 

2.3.3 WHENEVER


Die WHENEVER Anweisung dient der Fehlerbehandlung in einem Embedded SQL C  Programm. Über die WHENEVER Anweisung kann das Verhalten eines Programmes beim Auftreten eines Fehlers, der durch eine SQL-Anweisung ausgelöst wurde, festgelegt werden. Soll z.B. ein Programm nach dem Auftreten eines Fehlers beendet werden, kann dies durch die Formulierung folgender  WHENEVER Anweisung erreicht werden:
 
 

EXEC SQL WHENEVER SQLERROR STOP

(später genauer)
 
 

2.3.4 CONNECT


Die CONNECT Anweisung ist eine SQL-Anweisung, die  eine besondere Bedeutung hat. Eine CONNECT Anweisung muß vor jeder anderen SQL-Anweisung mit Ausnahme eines DECLARE Abschnitts oder der WHENEVER Anweisung ausgeführt werden. Durch die CONNECT Anweisung wird eine Verbindung eines C Programmes (Clientprogramm) zum SQL Serverprogramm  hergestellt. Erst nach der Ausführung der CONNECT Anweisung können weitere SQL-Anweisungen ausgeführt werden. Das folgende Beispiel zeigt die Anmeldung des Benutzers data12   mit dem password data12 über eine CONNECT Anweisung:
 
 

EXEC SQL CONNECT USER 'data12' USING  'data12';
 
 
 
 

2.4 Beispiel


Zuerst ein einfaches Beispiel um den Umgang mit Embedded SQL zu zeigen. Wir haben ein einfaches Programm mit Namen bsp1.ec:

main()
{
EXEC SQL whenever sqlerror stop;
EXEC SQL connect user 'axel' using '....';
EXEC SQL create database 'beispiel';
EXEC SQL close database;
}

Mit diesen SQL Befehlen wird lediglich eine Datenbank erstellt. Passiert ein Fehler (z.B. wenn es zweimal ausgeführt wird) wird das Programm abgebrochen. Der erste Schritt ist das Programm mit dem Präprozessor zu übersetzen und dann den resultierenden C Source Code zu übersetzen und mit den Datenbank routinen zu linken, Dies geschieht mit dem Befehl

yardpc bsp1.ec

dies erstellt ein ausführbares Programm a.out, was die gewünschten Dinge erledigt. Das zweite Beispiel ist schon etwas umfangreicher und bedarf dann einiger Erläuterungen:
 
 
 

/* bsp2.ec */
#include <stdio.h>
#include <stdlib.h>
#include <math.h>

isprim(int n)

 {
 int k,s;
 s = (int) sqrt((double)n) +1;
 for (k=2;k<=s ;k++)
              if (n % k == 0) return 0;
 return 1;
 }
main()
{
 int eingabe;
EXEC SQL BEGIN DECLARE SECTION;
 int i,kandidat,prim;
EXEC SQL END DECLARE SECTION;
EXEC SQL WHENEVER  SQLERROR STOP;
EXEC SQL CONNECT USER 'axel' USING '....';
 srand(getpid());
 again:
 printf("(1) anlegen der primzahl tabelle \n");
 printf("(2) loeschen der primzahl tabelle \n");
 printf("(3) zufaellig fuellen der primzahl tabelle \n");
 printf("(4) ab startwert fuellen der primzahl tabelle \n");
 printf("(5) anzeigen der primzahl tabelle \n");
 printf("(6) select der primzahl tabelle \n");
 printf("(7) suchen der primzahl zwillinge \n");
 printf("(8) ende \n");

 scanf("%d",&eingabe);

 switch(eingabe)
  {
  case 1:
   EXEC SQL DATABASE 'beispiel';
   EXEC SQL CREATE TABLE PRIMZAHLEN(zahl INTEGER UNIQUE,
                                       prim INTEGER CHECK (prim = 1 or prim = 0));
   EXEC SQL CLOSE DATABASE;
   goto again;
  case 2:
   EXEC SQL DATABASE 'beispiel';
   EXEC SQL DROP TABLE PRIMZAHLEN;
   EXEC SQL CLOSE DATABASE;
   goto again;


  case 8:

   break;
  default:
goto again;
  }
}
 

Dies Programm soll zur Manipulation einer Primzahltabelle dienen. Dazu gibt es eine Hilfsfunktion (isprim) die eine int Variable auf die Primzahleigenschaft testet. Der Rückgabewert ist 1 für Primzahl und 0 sonst. Das Hauptprogramm (main) erlaubt verschiedene Operationen mit dieser Tabelle, die der Benutzer über ein Menu auswählt. Punkt 1 legt die Tabelle an, Punkt 2 löscht sie wieder. Da verschiedene Variablen zur Kommunikation zwischen C Programm und SQL Befehlen benötigt werden gibt es eine DECLARE section, in der die entsprechenden Variablen deklariert werden. Mit der WHENEVER Anweisung wird festgelegt, dass bei einem SQL Fehler das Programm beendet wird. Der erste eigentliche SQL Befehl ist das Anmelden mit CONNECT. Des weiteren ist im Menupunkt 1 die CHECK Bedingung interessant, damit wird überwacht das nur die Werte 0 oder 1 in der Spalte eingetragen werden. Mit UNIQUE wird festgelegt, dass jede Zahl nur einmal eingetragen wird.
 
 
 
 
 
 

Menupunkt 3 dient zum Füllen mit zufälligen Werten:

case 3:

   EXEC SQL DATABASE 'beispiel';
   EXEC SQL  WHENEVER  SQLERROR CONTINUE;

   for (i=0;i<1000;i++)
    {
    kandidat = rand();
    prim = isprim(kandidat);
    EXEC SQL INSERT INTO PRIMZAHLEN VALUES (
     :kandidat, :prim );
    }
   EXEC SQL CLOSE DATABASE;
   EXEC SQL  WHENEVER  SQLERROR STOP;
   goto again;

Beim Füllen mit zufälligen Werten will man natürlich weitermachen wenn ein Wert schon da ist (Verletzung der UNIQUE Anweisung) dazu der Befehl WHENEVER .... CONTINUE. Bei der INSERT Anweisung sieht man die Verwendung der sog. hostvariabeln. Es wird der normale C-Variablen Name genommen mit einem vorangestellten : . Bei den Variablen ist auf Kompatibilität zu achten, was beim C Typ int und SQL Typ INTEGER gegeben ist. Am Ende wird wieder die normale Fehlerbehandlung eingeschaltet. Der nachfolgende Menupunkt 4 ist fast identisch:

case 4:

   again4:
   printf("bitte startwert (>0) eingeben:");
   scanf("%d",&eingabe);
   if (eingabe<1) goto again4;
   EXEC SQL DATABASE 'beispiel';
   EXEC SQL  WHENEVER  SQLERROR CONTINUE;
                        for (kandidat=eingabe+1000;kandidat>=eingabe; kandidat--)
                                {
                                prim = isprim(kandidat);
                                EXEC SQL INSERT INTO PRIMZAHLEN VALUES (
                                        :kandidat, :prim );
                                }
            EXEC SQL CLOSE DATABASE;
            EXEC SQL  WHENEVER  SQLERROR STOP;
            goto again;
 
 
 

Interessanter wird es beim Menupunkt 5, wenn es um die Verarbeitung von Ergebnismengen geht. Hierzu verwendet man sogenannte Cursor. Die Idee ist mit dem Cursor durch die Ergebnismenge zu navigieren.

  case 5:

   EXEC SQL DATABASE 'beispiel';
   EXEC SQL DECLARE c1 SCROLL CURSOR FOR
    SELECT * FROM PRIMZAHLEN;
   EXEC SQL OPEN c1;
   EXEC SQL  WHENEVER  NOT FOUND goto stop4;

   while(1){

   EXEC SQL FETCH c1 INTO :kandidat,:prim;
   printf("%10d %1d\n",kandidat,prim);
    }
   stop4:
   EXEC SQL  WHENEVER  NOT FOUND CONTINUE;
   EXEC SQL CLOSE c1;
   EXEC SQL CLOSE DATABASE;
   goto again;

 
 
 

Der erste Schritt ist die Definition des Cursors, dies geschieht mit dem SQL Befehl
 
 

DECLARE ... CURSOR FOR ...;
 
 

dieser Befehl ist nur in ESQLC sinnvoll. Der Cursor bekommt einen Namen, der innerhalb des ESQCL Programms eindeutig ist, d.h. braucht man verschiedene Cursor, so muss man verschiedene Namen vergeben. Der zweite Teil ist eine SELECT Anweisung, die mittels Cursor durchlaufen werden soll. Der nächste Schritt ist der Befehl
 
 

OPEN cursorname;
 
 

damit wird die konkrete SELECT Anweisung ausgeführt. Nächster Schritt ist eine Schleife die solange ausgeführt wird, bis ein Fehler auftritt. Sie wird über den WHENEVER Befehl beendet. (Schöner programmiert noch mal später in einem weiteren Beispiel. Innerhalb  der Schleife wird ein einzelner Datensatz aus der Cursormenge geholt, dies geschieht mit dem SQL Befehl
 
 

FETCH ... INTO ...;
 
 

Fetch holt jeweils den nächsten Datensatz. Wenn mit FETCH ausserhalb der Ergebnismenge positioniert wird wird das Ereignis NOT FOUND erzeugt. Dann wird durch WHENEVER zur Marke stop4 gesprungen. Wird der Cursor als sog. SCROLL CURSOR definiert, wie im Beispiel hat man noch weitere Möglichkeiten die Ergebnismenge zu durchlaufen:
 
 
 
 
Fetchrichtung Positionierung
NEXT Positioniert den Cursor auf den nächsten Satz der Ergebnismenge.
PREVIOUS
PREV
PRIOR
Positioniert den Cursor auf den vorhergehenden Satz der Ergebnismenge.
CURRENT Positioniert den Cursor auf den aktuellen Satz der Ergebnismenge.
FIRST Positioniert den Cursor auf den ersten Satz der Ergebnismenge.
LAST Positioniert den Cursor auf den letzten Satz der Ergebnismenge.
ABSOLUTE
ABS
Positioniert den Cursor auf den Satz, der durch cursor-position angegeben wird. Hierbei wird vom Anfang der Ergebnismenge aus positioniert.
RELATIVE
REL
Positioniert den Cursor auf den Satz, der durch cursor-position angegeben wird. Hierbei wird relativ zur aktuellen Cursorposition aus positioniert.

 

Der INTO Teil legt fest in welchen Variablen das Ergebnis (eine einzelne Zeile) gespeichert wird. Als ein weiteres Beispiel wollen wir jetzt Primzahl Zwillinge suchen. Dabei untersuchen  wir zwei verschiedene Methoden:
 

case 7:
                        zeit = time(NULL);
                        EXEC SQL DATABASE 'beispiel';
                        EXEC SQL DECLARE c2 CURSOR FOR
                            SELECT * INTO  :kandidat,:prim
                                        FROM PRIMZAHLEN
                                        WHERE PRIM=1 AND ZAHL+2 IN (
                                SELECT * FROM PRIMZAHLEN WHERE PRIM=1);
                        EXEC SQL OPEN c2;
                        while(1){
                        EXEC SQL FETCH c2 ;
                        if (SQLCODE == 100) /* not found */ goto stop5;
                        printf("%10d %10d %1d\n",kandidat,kandidat+2,prim);
                                }
                        stop5:
                         EXEC SQL CLOSE c2;
                        EXEC SQL CLOSE DATABASE;
                        zeit = time(NULL)-zeit;printf("Dauer = %d Sekunden\n",zeit);goto again;
case 8:
                        zeit = time(NULL);
                        EXEC SQL DATABASE 'beispiel';
                        EXEC SQL DECLARE c3 CURSOR FOR
                            SELECT A.ZAHL,A.PRIM
                                         INTO  :kandidat,:prim
                                         FROM PRIMZAHLEN A,
                                        PRIMZAHLEN B
                                        WHERE A.PRIM=1 AND B.ZAHL=A.ZAHL-2
                                        AND B.PRIM=1;
                        EXEC SQL OPEN c3;
                        while(1){
                        EXEC SQL FETCH c3 ;
                        if (SQLCODE == 100) /* not found */ goto stop6;
                        printf("%10d %10d %1d\n",kandidat,kandidat+2,prim);
                                }
                        stop6:
                        EXEC SQL CLOSE c3;
                        EXEC SQL CLOSE DATABASE;
                        zeit = time(NULL)-zeit;printf("Dauer = %d Sekunden\n",zeit);goto again;

In diesen Beispielen sieht man die Fehlerbehandlung mittels der Variable SQLCODE. Diese dient zur Kommunikation (auch innerhalb des normalen SQL) von Ereignissen. SQLCODE == 100 entspricht der  NOT FOUND Situation, d.h. FETCH wurde ausserhalb der Ergenismenge positioniert. Ferner wird in diesen beiden Beispielen die Zeit gemessen (sog. wall clock time, nicht Rechenzeit). Betrachtet man die CURSOR Definition sieht man, dass die INTO Klausel auch direkt bei der Definition angegeben werden kann. Es ist die Reihenfolge zu beachten, d.h. direkt vor der FROM Klausel. Der Hauptunterschied ist, dass im ersten Fall mit subquery gearbeitet wird und im zweiten Fall mit autojoin.  Bei 40000 Einträgen benötigte Variante 1 2 Sekunden und Variante 2 128 Sekunden!
 
 
 
 
 
 
 
 

3. Transaktionen

Das Programm soll so verändert werden, dass Änderungen nur dann vollzogen werden, wenn sie fehlerfrei durchgeführt werden können.
 
 

                        EXEC SQL DATABASE 'beispiel';
                        EXEC SQL  WHENEVER  SQLERROR CONTINUE;
nochmal:
                        kandidat = rand()%100000;
                        printf("Startwert %d\n",kandidat);
                        EXEC SQL SAVEPOINT CC;
                        for (i=0;i<100;i++)
                                {
                                prim = isprim(kandidat+i);
                                EXEC SQL INSERT INTO PRIMZAHLEN VALUES (
                                        :kandidat+:i, :prim, 0 );
                                if (SQLCODE < 0)  {
                                        printf("Fehler, neuer Versuch\n");
                                        EXEC SQL ROLLBACK TO SAVEPOINT CC;
                                        goto nochmal;
                                        }
                                }
                        EXEC SQL CLOSE DATABASE;
                        EXEC SQL  WHENEVER  SQLERROR STOP;
                        goto again;
 
 

Das Programm startet einen neuen Versuch wenn es bei dem Versuch Zahlen einzutragen einen Fehler hat (wahrscheinlich sollte eine Zahl zum zweiten mal eingetragen werden). Ein COMMIT am Ende ist überflüssig da dies automatisch beim Schliessen der DATABASE passiert. Ist ein Fehler aufgetretten werden auch alle bisherigen Einfügeoperationen Rückgängig gemacht. Um die Schleife herum muss WHENEVER SQLERROR auf CONTINUE geschaltet werden, da sonst keine einzelne Fehlerverarbeitung möglich ist. Die Embedded SQL Anweisung WHENEVER hat lediglich zur Folge, dass um jede einzelne Anweisung ein Abfrage nach dem SQLCODE gemacht wird. SQLCODE und die weitere Variable SQLMAINWARN sind eine vereinfachte Möglichkeit auf eine globale C-Struktur sqlca zuzugreifen, die zur Kommunikation von Fehlersituationen dient.

Folgende Möglichkeiten hat man bei der WHENEVER Anweisung
 
 

WHENEVER <bedingung> <aktion>
 
 

Folgende Bedingungen (condition) können in der WHENEVER Anweisung angegeben werden:
 
 
Bedingung Eintreffen der Bedingung
SQLERROR Fehler, d.h., SQLCODE und sqlca.sqlcode ist kleiner 0.
SQLWARNING Warnung, d.h. SQLMAINWARN und sqlca.sqlwarn0 haben den Wert 'W'. In diesem Fall ist mindestens ein weiteres Element (sqlwarn1 - sqlwarn10) der sqlca Struktur auf 'W' gesetzt.
NOT FOUND No data, d.h., SQLCODE und sqlca.sqlcode haben den Wert +100.

 
 

Trifft einer dieser Bedingungen zu, wird die durch exception-action definierte Aktion ausgeführt. Hierbei können folgende Aktionen angegeben werden:
 
 
 
Aktion Resultat
CONTINUE Die Programmausführung wird fortgesetzt. Wird keine WHENEVER Anweisung verwendet, ist dies das Defaultverhalten.
STOP Das Programm wird mit dem Exitcode 1 verlassen. Zusätzlich werden alle gesetzten Elemente der sqlca Struktur auf stderr ausgegeben.
GO TO or GOTO host-label Die Programmausführung wird bei host-label fortgesetzt. Hierbei muß host-label in demselben Programmblock (oder höher) wie die SQL-Anweisung, für die die Aktion ausgeführt wird, definiert sein.
CALL function-name Die C-Funktion function-name wird aufgerufen. Hier können alternativ Funktionsparameter angegeben werden. Die C-Funktion muß von der Anwendung bereitgestellt werden.

 
 
 
 
 
 
 

4. Trigger


Um die Suche nach den Primzahlzwillingen schneller zu machen kann man folgende Idee haben. Beim Einfügen von Zahlen soll im Falle von Primzahlen p sofort überprüft werden ob p-2 oder p+2 auch als Primzahl eingetragen ist. Dazu soll die Tabelle um eine Spalte  primzwilling erweitert werden, und dort soll der Wert 1 stehen falls die Zahl der kleinere Partner eines Primzahlzwillingspaars ist. Dazu muss die Routine zum Einfügen geändert werden.

case 4:
                        again4:
                       printf("bitte startwert (>0) eingeben:");
                        scanf("%d",&eingabe);
                        if (eingabe<1) goto again4;
                        EXEC SQL DATABASE 'beispiel';
                        for (kandidat=eingabe+1000;kandidat>=eingabe;
                                kandidat--)
                                {
                                prim = isprim(kandidat);
                                EXEC SQL INSERT INTO PRIMZAHLEN VALUES (
                                        :kandidat, :prim, 0 );
                                if (prim == 1)
                                        {
                                        EXEC SQL UPDATE PRIMZAHLEN SET
                                                PRIMZWILLING=1 WHERE
                                                        ZAHL=:kandidat-2
                                                        and PRIM=1;
                                        EXEC SQL UPDATE PRIMZAHLEN SET
                                                PRIMZWILLING=1 WHERE
                                                        ZAHL=:kandidat
                                                        and :kandidat+2 in
                                ( select zahl from primzahlen where prim=1 );
                                         }
                                }
                        EXEC SQL COMMIT;
                        EXEC SQL CLOSE DATABASE;
                        EXEC SQL  WHENEVER  SQLERROR STOP;
                        goto again;

Im Fall einer Primzahl werden die Einträge geändert, dabei wird eine subquery gestartet um zu schauen ob die um zwei grössere Zahl auch eine Primzahl ist. Das geht auch anders (schneller), wenn man mit den sog. Einzelsatzzugriff arbeitet. Dazu muss ein Index oder ein PRIMARY KEY vorhanden sein, d.h. beim Anlegen der Tabelle wird die Spalte Zahl als PRIMARY KEY vereinbart. Hier wird jetzt nur die Einfüge Schleife betrachtet:
 

                       prim = isprim(kandidat);
                                EXEC SQL INSERT INTO PRIMZAHLEN VALUES (
                                        :kandidat, :prim, 0 );
                                if (prim == 1)
                                        {
                                        EXEC SQL UPDATE PRIMZAHLEN SET
                                                        PRIMZWILLING=1 WHERE
                                                        ZAHL=:kandidat-2 and PRIM=1;
                                        kandidat +=2;
                                        EXEC SQL SELECT DIRECT PRIM into :prim
                                                        FROM PRIMZAHLEN
                                                        PRIMARY KEY ZAHL=:kandidat
                                                        WHERE PRIM=1;
                                        kandidat -= 2;
                                        if (SQLCODE!=100) {
                                                            EXEC SQL UPDATE PRIMZAHLEN SET
                                                                                PRIMZWILLING=1 WHERE
                                                                                ZAHL=:kandidat and PRIM=1;
                                                                        }
                                        }

Beim Einzelsatzzugriff wird nur maximal ein Datensatz geliefert. Er kann dann in Embedded C in host Variablen gespeichert werden. Die allgemeine Syntax ist

SELECT { FIRST | DIRECT | PREV | PREVIOUS | NEXT | LAST } .... [INTO .. ] FROM ...
  { PRIMARY KEY | INDEX index-name }
 [ column-name = index-value [ , column-name = index-value ] ... ]
       [ WHERE ..... ]

Die Suchrichtung muss angegeben werden (ähnlich dem FETCH). Der Zugriff auf die Tabelle erfolgt über den angegebenen Index oder den Primärschlüssel. Wird nicht  der Primärschlüssel, sondern ein anderer Index verwendet, muss dieser Index eindeutig sein. In Abhängigkeit von der angegebenen SELECT Richtung wird der selektierte Datensatz wie folgt bestimmt:

     FIRST
          Der nach der logischen Reihenfolge im Index erste Datensatz wird
          geliefert.

     LAST
          Der nach der logischen Reihenfolge im Index letzte Datensatz
          wird geliefert.

Bei FIRST und LAST darf  keine Indexbedingung, sondern nur ein Indexname angegeben werden, da der erste bzw. letzte Satz eines Index verwendet wird. Die anderen Positionierungen:

    DIRECT
          Der Datensatz, auf den über die angegebenen Indexwerte
          positioniert wird, wird geliefert.

     PREV
          Der nach indexsequentieller Tabellenreihenfolge vorhergehende
          Satz zu dem mit den Indexwerten identifizierten Satz wird
          geliefert.

     NEXT
          Der nach indexsequentieller Tabellenreihenfolge nächste Satz zu
          dem mit den Indexwerten identifizierten Satz wird geliefert.

Dabei muss ein Indexwert angegeben werden (columnname=...), der eindeutig ist und für die Positionierung verwendet wird. Wird ein Index über mehrere Spalten, muss für alle Spalten ein Wert angegeben werden.In jedem Fall kann eine WHERE Bedingung mit einer search-condition angegeben werden, über die die Suche eingeschränkt werden kann. Bei Verwendung von PREV und NEXT wird solange der vorhergehende bzw. folgende Satz gelesen, bis er der WHERE Bedingung entspricht oder das Ende der Tabelle erreicht wurde. Bei FIRST, DIRECT und LAST wird der SQLCODE +100 geliefert, wenn der Datensatz nicht der WHERE Bedingung entspricht.

Ein anderer Ansatz wäre dieses Problem mit einem Trigger zu lösen, d.h. bei jedem Einfügen soll automatisch geschaut werden, und die Spalte primzwilling aktualisiert werden. Ein entsprechender Codeabschnitt:

                        EXEC SQL CREATE TABLE PRIMZAHLEN
                                (
                                zahl INTEGER UNIQUE,
                                prim INTEGER CHECK (prim = 1 or prim = 0),
                                primzwilling INTEGER DEFAULT 0
                                CHECK (primzwilling=0 or primzwilling=1)
                                );

                        EXEC SQL CREATE TRIGGER ZWILLINGCHECK AFTER INSERT ON
                                PRIMZAHLEN FOR EACH ROW WHEN (PRIM=1)
                                        CALL CHECKZWILLING(ZAHL);
                        EXEC SQL CLOSE DATABASE;
                        goto again;
 
 
 

Dabei wird zum erstenmal ein CALL Aufruf verwendet, dazu muss noch die SQL Funktion Checkzwilling geschrieben werden:

CREATE PROCEDURE CHECKZWILLING (wert INTEGER)
        BEGIN
        UPDATE PRIMZAHLEN SET PRIMZWILLING=1 where ZAHL=wert-2 and PRIM=1;
        UPDATE PRIMZAHLEN SET PRIMZWILLING=1 where ZAHL=wert and wert+2 in
                        ( select zahl from primzahlen2 where prim=1 );
        END
END PROCEDURE

Es ist leider so, dass diese Routine fälschlicherweise bei YARD als rekursiv klassifiziert wird, obwohl der Auslöser ein CREATE Event ist und innerhalb des Triggers ein UPDATE gemacht wird. Rekursive Trigger sind nicht erlaubt. Da SQL Endlosschleifen ausschliessen möchte.
 
 
 
 
 
 
 
 
 
 
 
 
 

2.5 Dynamisches SQL


Oft ist der Ablauf von Datenbankprogrammen festgelegt. Z.b. aus dem Web wird eine ISBN Nummer eingelesen, die Datenbank durchsucht und die Ausgabe wird abgeliefert. Es sind aber auch Situationen denkbar, wo der Ablauf nicht klar ist. Das einfachste Beispiel ist dem Benutzer zu erlauben selber SQL Anfragen einzugeben.Solche Anforderungen können durch den Einsatz dynamischer SQL-Anweisungen erfüllt werden, so daß dynamische SQL dem Entwickler mehr Flexibilität in der Erstellung seiner Applikationen und damit für die Applikation selbst zur Verfügung stellt. Dynamisch definierte SQL-Anweisungen sind SQL-Anweisungen, die zur Kompilierzeit eines ESQLC- Programmes nicht oder nur teilweise bekannt sind und somit zur Laufzeit erstellt bzw. vervollständigt und ausgeführt werden. Dabei dürfen bis auf ein paar Ausnahmen alle SQL Anweisungen zur Erstellung dynamischer SQL Anweisungen verwendet werden.
 

2.5.1 Syntax einer dynamischen SQL Anweisung
 

Die SQL-Anweisung muß in einfachen oder doppelten Hochkommata eingeschlossen sein oder in einer Hostvariablen vom Typ char oder varchar gespeichert sein. Das  Präfix EXEC SQL darf nicht angegeben werden.
Das folgende Beispiel zeigt eine diesbezüglich ungültige Anweisung:

strcpy(sqlstmt,
"exec sql delete from film where film_nr = 1");
 

Das Ende der SQL-Anweisung darf nicht mit einem Semikolon abgeschlossen sein. Das folgende Beispiel zeigt eine diesbezüglich ungültige Anweisung:
 

strcpy(sqlstmt, "delete from film where film_nr = 1;");
 

Die SQL-Anweisung darf keine Kommentare enthalten. Die SQL-Anweisung darf keine Hostvariablen enthalten. Das folgende Beispiel zeigt eine diesbezüglich ungültige Anweisung:
 

strcpy(sqlstmt,
                "delete from film where film_nr = :v_film_nr");
 

Sollen in der Anweisung Hostvariablen als Parameter angegeben werden, so muß die Anweisung Fragezeichen als Platzhalter für die bei der Ausführung (EXECUTE) der Anweisung anzugebenden Hostvariablen enthalten. Das folgende Beispiel zeigt eine diesbezüglich gültige Anweisung:
 

strcpy(sqlstmt, "delete from film where film_nr = ?");
 

Handelt es sich bei der dynamisch definierten Anweisung um ein SELECT, darf keine INTO Klausel angegeben werden. Die INTO Klausel wird beim FETCH oder EXECUTE angegeben.
 
 
 
 

2.5.2 Ausführen von dynamischen SQL Anweisungen


Man hat verschiedene Methoden.

2.5.2.1 EXECUTE IMMEDIATE (nicht bei SELECT)
 

Eine dynamisch definierte SQL-Anweisung kann mit EXECUTE IMMEDIATE in einem Schritt bearbeitet und ausgeführt werden. Nach Ausführung der Anweisung wird der für die Anweisung allokierte Speicher automatisch freigegeben. Mit EXECUTE IMMEDIATE können alle Anweisungen ausgeführt werden, die kein SELECT sind und keine Input oder Output Werte erwarten.

Das folgende Beispiel zeigt gültige EXECUTE IMMEDIATE Anweisungen:
 
 

 
EXEC SQL BEGIN DECLARE SECTION;

#define STMT_SIZE               1024

char sqlstmt[STMT_SIZE];

EXEC SQL END DECLARE SECTION;

/*
Ausführung von Anweisungen, die in einer
Stringkonstante stehen
*/
EXEC SQL EXECUTE IMMEDIATE 'lock table film in exclusive mode';

EXEC SQL EXECUTE IMMEDIATE 'delete from film where film_nr = 1';
/*
Eingabe und Speicherung einer Anweisung
*/
printf("Geben Sie Ihre SQL-Anweisung ein: ");
gets(sqlstmt);

/*
Ausführung der Anweisung, die in der Hostvariablen
sqlstmt gespeichert ist.
*/

EXEC SQL EXECUTE IMMEDIATE :sqlstmt;
 


Durch die EXECUTE IMMEDIATE Anweisung können dynamisch definierte SQL-Anweisungen ohne Parameter auf einfache Weise ausgeführt werden.
 
 
 

2.5.2.2 PREPARE/EXECUTE

 

 
 

Eine Alternative zur EXECUTE IMMEDIATE Anweisung ist die Verwendung von PREPARE und EXECUTE zur Bearbeitung und Ausführung von dynamisch definierten SQL-Anweisungen. Obwohl die Verwendung von PREPARE und EXECUTE im Prinzip zum gleichen Ergebnis wie die EXECUTE IMMEDIATE
Anweisung führt, sind hiermit einige Vorteile gegenüber EXECUTE IMMEDIATE verbunden.

Die allgemeine Form der PREPARE Anweisung lautet:
 
 

PREPARE statement-id FROM sql-anweisung
 

Durch die PREPARE Anweisung wird die in der FROM Klausel angegebene SQL-Anweisung sql-anweisung geparst, auf syntaktische und semantische Richtigkeit überprüft und optimiert. Eine so bearbeitete Anweisung wird unter einer vom Benutzer anzugebenden statement-id durch den SQL-Server im Speicher abgelegt und zur Ausführung bereitgestellt.

Statement-Id

Eine Statement-Id ist standardmäßig global innerhalb eines ESQLC-Programmes,
das heißt, sie kann in einem Modul definiert (PREPAREd) und von anderen Modulen
aus ausgeführt (EXECUTE) werden. Zudem kann eine Statement-Id als Hostvariable
definiert werden.

SQL-Anweisungstyp

Um den Typ einer dynamisch formulierten SQL-Anweisung zu bestimmen, enthält das Element sqlca.sqlerrd[3] der sqlca Struktur den Opcode der mit PREPARE bearbeiteten Anweisung.
Man hat  die Möglichkeit, den Typ einer dynamisch definierten SQL-Anweisung zu bestimmen, um zum Beispiel, je nach Anweisung, bestimmte Aktionen zur Laufzeit einzuleiten.
 
 

 
/*
Die in der Hostvariable stmt gespeicherte Anweisung
ist nicht bekannt
*/
EXEC SQL PREPARE dynid FROM :stmt;

/*
Bestimmung des Anweisungstyp falls PREPARE erfolgreich
*/
if (SQLCODE == 0)
{
switch(sqlca.sqlerrd[3])
{
case OP_DROPDB:
case OP_DROPTAB:
printf("Ungültige Anweisung\n");
break;

case ...
}
}
 


 

Dynamische Parameter

Die SQL-Anweisung sql-anweisung kann dynamische Parameter enthalten. Dynamische Parameter sind Werte, die bei der Ausführung der SQL-Anweisung durch Hostvariablen oder Konstanten in der USING Klausel der EXECUTE Anweisung angegeben werden müssen.
Dynamische Parameter müssen mit einem Fragezeichen angegeben werden.
 
 

 
sprintf(stmt, "insert into dyn_table values (?, ?, ?)");

EXEC SQL PREPARE stmtid FROM :stmt;

EXEC SQL EXECUTE stmtid USING :var1, 'STRING', 123.89;
 


Ressourcenkontrolle (FREE)

Bei einer PREPARE Anweisung wird sowohl vom Laufzeitsystem als auch vom SQL-Server Speicher allokiert, um die dynamisch definierte Anweisung und deren Spezifika zu speichern. Dieser Speicher bleibt solange allokiert, bis das jeweilige Programm beendet wird. Soll der Speicher vorher freigegeben werden, zum Beispiel, weil die Anweisung nicht mehr benötigt wird, kann dies durch die FREE Anweisung erreicht werden. Die FREE Anweisung wird hierbei auf die jeweilige Statement-Id ausgeführt:
 

 

EXEC SQL PREPARE p_id1 FROM ...
EXEC SQL FREE STATEMENT p_id1
 


 

Ausführung über EXECUTE

Wurde die PREPARE Anweisung ohne Fehler (SQLCODE ist 0) bearbeitet, kann die Anweisung durch EXECUTE beliebig oft ausgeführt werden. Enthält die SQL-Anweisung dynamische Parameter, so müssen diese durch Angabe von Werten in der USING Klausel einer EXECUTE Anweisung ersetzt werden. Hierbei muß die Anzahl der USING Werte gleich der Anzahl der dynamischen Parameter sein. Die allgemeine Form der EXECUTE Anweisung lautet:
 
 

EXECUTE statement-id [ using_clause ]
 

Die EXECUTE Anweisung führt die durch statement-id referenzierte Anweisung aus. Die Vorteile von PREPARE und EXECUTE gegenüber EXECUTE IMMEDIATE lassen sich folgendermaßen zusammenfassen:

  •      Eine mit PREPARE bearbeitete Anweisung wird einmal geparst und optimiert und kann dann beliebig oft ausgeführt werden. (wie stored procedures)
  •      Durch die PREPARE Anweisung werden Informationen verfügbar gemacht (Anweisungstyp), die für individuelle Aktionen innerhalb eines Programmes verwendet werden können.
  •      Eine mit PREPARE bearbeitete Anweisung kann dynamische Parameter als Platzhalter für bei der Ausführung (EXECUTE) zu übergebende Werte enthalten.
  •      Mit PREPARE und EXECUTE können auch SELECT Anweisungen ausgeführt werden. Die SELECT Anweisung muß hierbei ein Einzelsatzselect sein oder darf nur eine Zeile als Ergebnis liefern (Single Row SELECT). Innerhalb der SELECT Anweisung darf keine INTO Klausel angegeben werden. Die INTO Klausel muß beim EXECUTE angegeben werden.
  •      Die mit einer PREPARE Anweisung verbundenen Speicherressourcen können explizit freigegeben werden.

  •  

     
     
     

    Das folgende Beispiel zeigt die Verwendung von PREPARE mit dynamischen
    Parametern und EXECUTE mit USING auf eine DELETE Anweisung.
     

     

    EXEC SQL BEGIN DECLARE SECTION;

    #define STMT_SIZE       512

    long film_nr;
    char del_stmt[STMT_SIZE];

    EXEC SQL END DECLARE SECTION;

    /*
    Anweisung in character array kopieren
    */
    sprintf(del_stmt, "delete from film where film_nr = ?");

    /*
    Anweisung mit PREPARE vorbereiten
    */
    EXEC SQL PREPARE p_delete_film FROM :del_stmt;
     

    /*
    Benutzereingabe
    */
    printf("Geben Sie die zu löschende Filmnummer ein: ");
    scanf("%ld", &film_nr);

    /*
    Anweisung ausführen, Hostvariable für dynamische
    Parameter zur Laufzeit mit USING übergeben
    */
    EXEC SQL EXECUTE p_delete_film USING :film_nr;
     
     

    PREPARE und EXECUTE auf eine SELECT Anweisung
     

    Soll mit PREPARE und EXECUTE eine SELECT Anweisung bearbeitet werden, so ist die EXECUTE Anweisung um eine INTO Klausel zu erweitern, um das Ergebnis der SELECT Anweisung zu speichern. Neben der INTO Klausel kann weiterhin eine USING Klausel für Input Werte angegeben werden.

    Das folgende Beispiel illustriert die Vorgehensweise von PREPARE und EXECUTE auf eine SELECT Anweisung.
     

     

    EXEC SQL BEGIN DECLARE SECTION;

    #define STMT_SIZE       512
    #define TITLE_SIZE      81

    long film_nr;
    char sel_stmt[STMT_SIZE];
    varchar v_title[TITLE_SIZE];

    EXEC SQL END DECLARE SECTION;

    /*
    SELECT Anweisung in character array kopieren
    */
    sprintf(sel_stmt,
    "select titel from film where film_nr = ?");

    /*
    Anweisung mit PREPARE vorbereiten
    */
    EXEC SQL PREPARE p_select_film FROM :sel_stmt;

    /*
    Anweisung beliebig oft ausführen
    */
    for (;;)

    {
    /*
    Benutzereingabe
    */
    printf("Geben Sie die zu suchende Filmnummer ein oder -1 für Ende: ");
    scanf("%ld", &film_nr);
    if (film_nr == -1)
    break;
     

    /*
    Anweisung ausführen, Hostvariable für
    dynamischen Parameter zur Laufzeit mit USING
    übergeben.
    INTO Klausel für Ergebnis angegeben.
    */

    EXEC SQL EXECUTE p_select_film INTO :v_title USING :film_nr;

    /*
    Ergebnis anzeigen
    */
    printf("Der Titel für Filmnummer %ld ist: %s\n", film_nr, v_otitle);
    }
     
     

    2.5.2.3 EXECUTE bei SELECT mit Ergebnismengen/CURSOR

     

     

    Um SELECT Anweisungen, die mehr als eine Zeile als Ergebnismenge liefern,
    dynamisch zu bearbeiten, gilt folgende allgemeine Vorgehensweise:

         SELECT Anweisung mit PREPARE bearbeiten.

         Cursor mit DECLARE CURSOR auf die Statement-Id deklarieren.

         Cursor mit OPEN öffnen. Enthält die SELECT Anweisung dynamische
         Parameter müssen diese über USING ersetzt werden.

         Ergebnismenge mit FETCH bearbeiten.

         Cursor nach der Bearbeitung mit CLOSE schließen. Durch eine COMMIT
         oder ROLLBACK Anweisung werden alle bis dahin offenen Cursor (außer
         WITH HOLD Cursor) implizit geschlossen.

         Gegebenenfalls intern allokierten Speicher mit FREE STATEMENT und
         FREE CURSOR freigeben.

    Das folgende Beispiel zeigt die Bearbeitung einer dynamischen SELECT Anweisung:
     
     

     

    EXEC SQL BEGIN DECLARE SECTION;

    long v_film_nr;
    varchar v_title[81];
    char selstmt[512];
    long nr1, nr2;

    EXEC SQL END DECLARE SECTION

    /*
    SELECT Anweisung mit dynamischen Parametern
    */
    sprintf(selstmt,
    "select film_nr, titel from film where film_nr \
    between ? and ?");
    /*
    PREPARE auf die dynamische Anweisung
    */
    EXEC SQL PREPARE p_sel FROM :selstmt;

    /*
    DECLARE CURSOR auf die Statement-Id
    */
    EXEC SQL DECLARE c_sel CURSOR FOR p_sel;

    /*
    Benutzereingaben speichern
    */
    printf("Geben Sie eine untere und eine obere Grenze für \
    die Filmnummer ein\n");
    printf("Untere Grenze: ");
    scanf("%ld", &nr1);
    printf("\nObere Grenze: ");
    scanf("%ld", &nr2);

    /*
    Cursor öffnen und dynamische Parameter durch Benutzereingaben ersetzen
    */

    EXEC SQL OPEN c_sel USING :nr1, :nr2;

    /*
    Ergebnismenge bearbeiten
    */
    while (1)
    {
    EXEC SQL FETCH c_sel INTO :v_film_nr, v_title;
    if (SQLCODE == SQLNOTFOUND)
    break;
    printf("Filmnr: %ld Filmtitel: %s\n",
    v_film_nr, v_title);
    }

    /*
    Cursor schließen und Speicher freigeben
    */
    EXEC SQL CLOSE c_sel;

    EXEC SQL FREE CURSOR c_sel;

    EXEC SQL FREE STATEMENT p_sel;