Embedded SQL mit C und Oracle Holger Jakobs – [email protected], [email protected] 2008-06-26 Inhaltsverzeichnis 1 Einleitung 2 SQL in Programmen 2.1 SQL-Kommandos im Quelltext . . . . . 2.2 Verbindung zur Datenbank . . . . . . . . 2.2.1 Herstellen der Verbindung . . . . 2.2.2 Lösen der Verbindung . . . . . . 2.3 Kommunkation Datenbank ↔ Programm 2.3.1 Zeichenketten & Oracle . . . . . . 2.4 Transaktionen . . . . . . . . . . . . . . . 2 . . . . . . . 2 3 3 3 4 4 5 7 3 1. Beispielprogramm 3.1 Compilation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2 Precompiler-Ausgabe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 7 7 4 SELECT in Programmen 7 5 Weitere Embedded-SQL-Kommandos 5.1 Programmieraufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.2 Hinweise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 9 9 . . . . . . . . . . . . . . 6 Abfragen mit mehreren Ergebnistupeln 6.1 Bulk Select . . . . . . . . . . . . . . . . . . 6.2 Cursor-Select . . . . . . . . . . . . . . . . . 6.2.1 Einfache Cursor-Abfragen . . . . . . 6.2.2 Cursor-Abfragen mit where current 6.2.3 Oracle-Zusätze bei Cursor-Abfragen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 10 11 11 12 13 7 Implizite Fehlerbehandlung 13 8 weitere Aufgaben 14 Hinweis: Alle Beispielprogramme finden Sie im Datenbank-Portal unter Quelltexte“. ” 1 2 SQL IN PROGRAMMEN 1 Einleitung SQL kann nicht nur mit interaktiven Kommandos in sqlplus oder tora benutzt werden, sondern SQL-Kommandos können auch in Programme eingebettet werden, die in klassischen Programmiersprachen geschrieben wurden, z. B. C oder C++. Voraussetzung ist, dass neben dem Compiler für die jeweilige Sprache der passende Precompiler vorhanden ist, der die eingebetteten SQL-Kommandos in Quelltext der Wirtssprache (host language) umsetzt. Im folgenden wird nur C verwendet, aber es ist mit C++ genauso möglich. Für C++ bietet sich allerdings auch die Verwendung eines objektorientierten Interfaces an, sofern es das für die jeweilige Datenbank gibt. Für C (und C++) gibt es – je nach Datenbank – auch ein sogenanntes Call-Level-Interface, bei dessen Verwendung man direkt Bibliotheksaufrufe in den Quellcode einbaut. Embedded SQL ist allerdings bequemer in der Handhabung. Darüber hinaus ist der Zugriff auf Datenbanken über genormte Schnittstellen von Bedeutung, hierbei ist besonders das plattformunabhängige JDBC zu erwähnen. Dies wird aber in diesem Dokument nicht erläutert, da es hier nicht um Java geht. Grundsätzlich funktioniert die Programmierung mit Embedded SQL mit allen Programmiersprachen und mit allen SQL-Datenbanken im wesentlichen gleich – sofern sie Embedded SQL überhaupt unterstützen. In Details gibt es aber Unterschiede. Dieser Text bezieht sich ausdrücklich auf Oracle und C. 2 Verwendung von SQL in Programmen In Programmen werden vorwiegend Datenmanipulationsbefehle benutzt, seltener Administrationskommandos für die Datenbank, die die Struktur verändern. Es gibt davon vier: SELECT Suchen von Daten INSERT Einfügen von Tupeln DELETE Löschen von Tupeln UPDATE Ändern von Attributwerten Diese Befehle können auf verschiedene Arten Datenmanipulationen durchführen. Nicht alle Kombinationen sind möglich. Die möglichen Operationen sind angegeben (S=SELECT, F=FETCH, I=INSERT, D=DELETE, U=UPDATE, WC=DELETE/UPDATE WHERE CURRENT): 1. Einfache Datenmanipulation: (S, I, D, U) Alle Operationen betreffen die Übertragung von max. einem Tupel aus der Datenbank ins Programm oder umgekehrt. 2. Sequentielle Tabellenverarbeitung: (S, F, WC) Mit Hilfe eines Cursors (Tupelzeiger) können viele Tupel nacheinander bearbeitet werden. 2 2 SQL IN PROGRAMMEN 2.1 SQL-Kommandos im Quelltext 3. Bulk-Operation (Array-Operation): (S, F, I) Es werden viele Tupel mit einem Befehl bearbeitet. 4. Dynamische Operation: (S, F, I, D, U) Die Operation wird erst zur Laufzeit spezifiziert. Diese Art Programme werden in diesem Dokument nicht erläutert. 2.1 SQL-Kommandos im Quelltext Die eingebetteten SQL-Kommandos müssen als solche für den Precompiler kenntlich gemacht werden. Ein SQL-Kommando sieht so aus: exec sql ... ; Alles zwischen exec sql und dem Semikolon wird vom SQL-Precompiler in reinen CQuelltext umgewandelt. Das Original-Kommando wird also durch C-Code ersetzt. Gelegentlich kann man es noch im C-Code erkennen, aber das ist nicht immer der Fall. Der SQL-Precompiler-Output wird anschließend mit dem C-Compiler ganz normal compiliert und unter Verwendung datenbankspezifischer Bibliotheken gelinkt. 2.2 Verbindung zur Datenbank 2.2.1 Herstellen der Verbindung Vom fertigen Programm muss eine Verbindung zur Datenbank hergestellt werden, so wie das bei tora auch der Fall ist. Dazu muss man ausdrücklich ein connect-Statement verwenden, was bei Oracle in der Syntax vom ANSI-Standard abweicht. Bei ANSI-Datenbanken schreibt man connect to :datenbank , wobei :datenbank die Datenbank bzw. der Server ist, mit der man die Verbindung herstellen möchte. Bei Oracle sieht es im Programm so aus: exec sql connect :user identified by :password using :connectstring ;, wobei connectstring ; einen Oracle-Net8-Database Identification String enthält. Dieser wird mit Hilfe einer Datei namens tnsnames.ora einem Server und einer Datenbank zugeordnet. Wahlweise kann man statt des Benutzernamens und des Passwords separat exec sql connect :user_passwd angeben, wobei im user_passwd Benutzernamen und Password dann durch einen Slash getrennt werden. Die Variablen :datenbank , :user , :password ; und :connectstring ; sind sogenannte Hostvariablen (siehe Abschnitt 2.3 auf der nächsten Seite). Man kann sie im Quelltext mit festen Werten initialisieren (bietet sich für den Datenbanknamen an, bei uns ora10.bg.bib.de) oder auch zur Laufzeit mit Werten versehen, z. B. durch Einlesen. Das ist insbesondere für das Password anzuraten, wobei man da noch das Bildschirmecho abschalten muss, damit einem niemand das Password vom Bildschirm ablesen kann. Hierzu können Sie Routinen in /usr/local/include/liesterm.h finden. Die Bibliotheksdatei libbib.a in /usr/local/lib/ wird von unserem Script sqlproc automatisch dazugebunden. 3 2.3 Kommunkation Datenbank ↔ Programm 2 SQL IN PROGRAMMEN Tabelle 1: Nullwerte und ihre Bedeutung Wert −1 0 >0 Beschreibung Das Attribut hatte den Nullwert Es war ein Wert vorhanden, der übertragen wurde. Es war ein Zeichenkettenwert vorhanden, der aber abgeschnitten wurde. Es wurden nur so viele Zeichen übertragen wie in die Hostvariable passten. Wert gibt an, wieviele Zeichen hätten übertragen werden sollen. 2.2.2 Lösen der Verbindung Das Lösen der Verbindung zur Datenbank geschieht automatisch beim Beenden des Programms, d. h. man ist nicht gezwungen, dies explizit zu tun. Das ist also genauso wie bei Dateien, die beim Ende des Programms ebenfalls automatisch geschlossen werden. Trotzdem ist es natürlich guter Stil, die Verbindung zu schließen. Im ANSI-SQL-Standard geschieht das mittels exec sql disconnect;, was außerhalb von Transaktionen zu geschehen hat, bei Oracle mit exec sql commit release; (bei Durchführen einer Transaktion) bzw. exec sql rollback release; (bei Abbrechen einer Transaktion). 2.3 Kommunikation zwischen Datenbank und Programm Zwischen Programm und Datenbank werden Daten ausgetauscht, z. B. Datenbank → Programm beim select und umgekehrt beim insert. Dies geschieht über sogenannte HostVariablen, die in den SQL-Kommandos durch einen vorangestellten Doppelpunkt gekennzeichnet werden. Es kann sich bei den Variablen um globale oder auch lokale Variablen handeln. Nullwerte werden, da sie nicht in den Variablen selbst darstellbar sind, über zusätzliche Variablen vom Typ short int dargestellt (im Standard wird nur integer als Datentyp erwähnt, bei Oracle müssen Sie short sein). Diese werden Nullwert-Indikatoren genannt und werden in der Tabelle 1 erläutert. Beim Abfragen von Daten (SELECT) muss man stets Nullwert-Indikatoren verwenden, wenn ein Attribut Nullwerte zulässt, damit es zur Laufzeit keine unerwarteten Fehler gibt. Aber auch beim Einfügen von Daten (INSERT)verwendet man Nullwert-Indikatoren, die man mit −1 füllt, wenn man einen Nullwert übertragen möchte, und ansonsten mit 0. Man kann auch beim Abfragen von not-null-Zeichenketten-Attributen Indikatoren verwenden, aber dann ist der Indikatorwert 0, wenn alles gut gegangen ist. Für den Fall, dass die Variable zu kurz war, um den Wert des Attributs aufzunehmen, enthält die Indikatorvariable die Anzahl Zeichen, die hätten übertragen werden sollen. Das Datenbanksystem muss dem Programm über die reinen auszutauschenden Daten hinaus auch noch mitteilen, ob die gewünschten Operationen ausgeführt werden konnten. Dazu dient die SQL-Communication Area, kurz sqlca. Sie muss im Programm glo- 4 2 SQL IN PROGRAMMEN 2.3 Kommunkation Datenbank ↔ Programm Tabelle 2: ANSI-sqlcode-Werte und ihre Bedeutung Werte sqlcode = 0 sqlcode < 0 sqlcode = 100 Bedeutung kein Fehler, keine Warnung Fehler, Kommando nicht ausgeführt kein passendes Tupel gefunden Damit dieser im ANSI-Standard festgelegte Wert von Oracle gemeldet wird, muss man beim Preprocessing MODE=ANSI angeben, was unser Script sqlproc aber immer tut. Tabelle 3: Oracle-sqlcode-Werte und ihre Bedeutung sqlcode-Werte Bedeutung −1017 Autorisierung falsch, keine Anmeldung an der Datenbank möglich. −1861 Literal (z. B. für Datum/Zeit-Wert) entspricht nicht dem Format. Hinweis: Durch die im b.i.b. eingestellten Werte verlangt Oracle Datumswerte nach dem Schema von ISO8601, z. B. 1999-12-18“. ” −1 Primärschlüssel-/Uniqueklausel-Verletzung −2291 Fremdschlüssel-Verletzung, Eltern-Datensatz nicht gefunden −2292 Fremdschlüssel-Verletzung, Kind-Datensatz gefunden −2290 Check-Constraint-Verletzung −2112 Mehrere Ergebnistupel bei einem Single Select Weitere Werte schauen Sie bitte in den Oracle-Unterlagen nach (z. B. online über das Datenbanken-Portal). bal deklariert werden, was durch das Kommando exec sql include sqlca; geschieht. Es handelt sich dabei um eine Datenstruktur, von der u. a. folgende Komponenten zur Status-Überprüfung benutzt werden: sqlcode und sqlerrm.sqlerrmc. Bei Oracle sollte man vorher das Symbol SQLCA_INIT definieren, damit die sqlca initialisiert wird. Die Bedeutung der Variablen sqlcode nach ANSI ist in der Tabelle 2 erläutert. Weitere Werte sind datenbankabhängig, siehe Tabelle 3. Tip: In dem vom Precompiler erzeugten C-Code ist die SQL Communication Area schön kommentiert enthalten. Schauen Sie mal rein! 2.3.1 Zeichenketten & Oracle Bei Zeichenketten gibt es im Zusammenhang mit Oracle einige Besonderheiten zu berücksichtigen. Oracle verwaltet Zeichenketten wie die Programmiersprache Pascal, d. h. nicht als 5 2.3 Kommunkation Datenbank ↔ Programm 2 SQL IN PROGRAMMEN ASCII-NUL-terminierte Arrays, sondern mit einer separaten 16 Bit großen Längenangabe. Die Konvertierung zwischen der C-Darstellung und der Oracle-Darstellung geschieht nicht automatisch; der Precompiler fügt den hierzu notwendigen Code nicht in das Programm ein – im Gegensatz zu den Precompilern anderer Datenbanken. Verwendet man als Hostvariablen char-Arrays wie in C üblich, so werden diese beim Lesen von Daten aus der Datenbank immer mit Blanks bis zum Ende vollgeschrieben, auch wenn der Attributwert vom Typ varchar kürzer ist (blank-padded). Alternativ dazu kann man die Hostvariablen als varchar deklarieren, was vom Precompiler in eine Struktur umgewandelt wird. Hier ein Beispiel: varchar vorname [16]; wird vom Precompiler umgewandelt in struct { unsigned short len; unsigned char arr[16]; } vorname; Das muss man bei der folgenden C-Programmierung natürlich berücksichtigen. Der Versuch, den Vornamen jetzt mittels printf ("%s\n", vorname); auszugeben, ist zum Scheitern verurteilt (leider erst zur Laufzeit, weil der C-Compiler die Parameter von printf hinter der Formatangabe wegen der Ellipse nicht prüfen kann) – es gibt evtl. einen Segmentation Fault (nicht immer reproduzierbar). Statt dessen muss man folgendes verwenden: vorname.arr[vorname.len] = '\0'; printf ("Vorname: '%s'\n", vorname.arr); Diese Vorgehensweise entspricht soweit dem ANSI-SQL-Standard. Aber man muss beim Eintragen von Zeichenketten in die Datenbank vorher mittels strlen die Länge ermitteln und in das len-Element der Struktur eintragen: vorname.len = strlen (vorname.arr); exec sql insert into tabelle values (:nr, :vorname); Eine weitere Besonderheit ist, dass Oracle bei der Verarbeitung davon ausgeht, dass die leere Zeichenkette identisch ist mit dem NULL-Value. Das ist leider nicht so, denn die Information, dass eine Zeichenkette leer ist, ist nicht identisch mit der fehlenden oder nicht zutreffenden Information. Man kann also – im Gegensatz zu anderen, ANSI-konformen Datenbanken – in ein NOT NULL-Attribut keine leere Zeichenkette eintragen. Ein möglicher Workaround ist, eine Zeichenkette einzutragen, die aus einem Leerzeichen besteht. 6 4 SELECT IN PROGRAMMEN 2.4 Transaktionen 2.4 Transaktionen Bei Oracle behindern sich beliebig viele Lesetransaktionen und eine Schreibtransaktion nicht gegenseitig. Die Lesetransaktionen bekommen immer die Daten geliefert, wie sie zum Zeitpunkt des Beginns in der Datenbank gestanden haben. Sie werden nötigenfalls aus dem Rollback-Segment der Datenbank gelesen, wenn sie zwischenzeitlich durch eine Schreibtransaktion verändert wurden. Änderungen am Datenbestand müssen mittels commit work dauerhaft gemacht werden. Wahlweise können alle Änderungen einer Transaktion auch ungeschehen gemacht werden, indem man statt dessen rollback work verwendet. Das im Standard enthaltene Wort work hinter dem commit ist bei Oracle optional, sollte aber der Portabilität wegen verwendet werden. Transaktionen beginnen automatisch, sobald man Aktionen mit der Datenbank macht. Beendet werden sie durch eines der oben genannten Kommandos oder durch Beenden der Verbindung zur Datenbank. Änderungen an der Datenbankstruktur führen übrigens vorher und nachher automatisch und ungefragt ein commit work aus. 3 Erstes Beispielprogramm Das Beispielprogramm finden Sie in der Datei sqlprogc_beispiel1.pc; es wird die bekannte Sportlerdatenbank verwendet (bzw. die Tabelle sp_spieler darin). 3.1 Compilation eines SQL-Programms Ein C-Programm mit Embedded SQL muss mehrfach übersetzt werden. Daher haben wir auf unserer Maschine ein kleines Script für diesen Zweck mit dem Namen sqlproc. Schauen Sie es sich bitte genau an, denn schließlich sollen Sie auch ohne dieses Script in der Lage sein, ein SQL-Programm in eine ausführbare Datei umzuwandeln. 3.2 Vom SQL-Precompiler umgewandelte EXEC-SQL-Aufrufe Übersetzen Sie das Beispielprogramm sqlprogc_beispiel1.pc und drucken Sie den Quellcode sowie den erzeugen C-Quelltext sqlprogc_beispiel1.c aus. Vergleichen Sie die Dateien und suchen Sie die exec sql-Anweisungen im übersetzten Code. 4 Das SELECT-Kommando in Programmen Nach der Eingabe von Daten mit dem insert-Kommando wollen wir nun Daten aus dem Bestand abfragen. Vom select-Kommando gibt es mehrere Varianten: das Single Select“, ” das Bulk Select“ und das Sequential Select“. Das erste wird hier erläutert, das Bulk Se” ” ” lect“ in Abschnitt 6.1 auf Seite 10 und das Sequential Select“ (auch Cursor-Select genannt) ” in Abschnitt 6.2 auf Seite 11. 7 5 WEITERE EMBEDDED-SQL-KOMMANDOS Das Single Select“ muss so formuliert sein, dass das Ergebnis nicht mehr als ein einziges ” Tupel umfasst; andernfalls kommt es zu einem Fehler (−2112) in sqlca.sqlcode). Das Programm stellt für das Ergebnis des Kommandos nur einen Datensatz zur Verfügung, der sofort gefüllt wird, sofern ein Tupel gefunden wird. Beispiel für eine Deklaration: exec sql begin declare section; char SpielName [16]; /* alle Zeichenketten 1 Byte laenger, */ char Vorname [ 4]; /* damit Platz fuer Terminator \0 ist. */ char PLZ [ 6]; char Ort [21]; exec sql end declare section; Zeichenketten sollten grundsätzlich 1 Byte länger als in SQL deklariert werden, damit Platz für das Terminatorzeichen '\0' ist, was von Oracle bei char-Variablen leider nur ganz am Ende angehängt wird, so dass alle Zeichenketten bis zum Ende mit Blanks aufgefüllt geliefert werden. Dies kann man durch die Verwendung von varchar-Variablen verhindern, die vom Precompiler in Strukturen umgesetzt werden (siehe Abschnitt 2.3 auf Seite 5). Das Select-Statement sieht im Programm dann so aus: exec sql select spielname, vorname, plz, ort into :spielname, :vorname, :plz, :ort from spieler where spielnr = :spielnr; Dieses Select erzeugt wegen der Eindeutigkeit der Spielernummer garantiert höchstens ein Tupel. Die Variable sqlcode aus der sqlca kann den Wert 100 enthalten, wenn kein passendes Tupel enthalten ist; oder es kann ein Fehler auftreten, wenn mehr als ein passendes Tupel gefunden wurde. Die Select-Kommandos können auch bei einem Single Select beliebig komplex sein. Ein komplettes Beispielprogramm mit einem Single-Select finden Sie unter singleselect.pc. Schauen Sie es sich mit besonderem Augenmerk auf die Behandlung von Nullwerten an. Bei allen Attributen, die Nullwerte enthalten können (also nicht mit not null versehen sind), ist es zwingend erforderlich, dass ein Nullwert-Indikator verwendet wird. Dabei handelt es sich um eine Integer-Host-Variable (bei Oracle abweichend vom Standard zwingend short int), die hinter dem Attribut ohne Komma dazwischen angegeben wird. Diese Variable wird mit einer Null gefüllt, wenn ein gültiger Wert geliefert wurde (also logisch false). Falls im Attribut ein Nullwert stand, bekommt der Nullwert-Indikator einen Wert ungleich Null (also logisch true). 5 Weitere Embedded-SQL-Kommandos Im folgenden sollen Sie außer dem Single Select und dem Insert auch Delete und Update einsetzen. Delete unterscheidet sich nicht von der interaktiven Version. Bei Update lautet 8 5 WEITERE EMBEDDED-SQL-KOMMANDOS 5.1 Programmieraufgaben die Syntax: exec sql update tabellenname set attribut = attributwert /:hostvariable where bedingung ;. Es kann also wahlweise ein direkter Attributwert oder eine Hostvariable angegeben werden, deren Inhalt dem genannten Attribut aller Tupel zugewiesen wird, die auf die Bedingung passen. 5.1 Programmieraufgaben mit Embedded SQL zur Krankenhaus-Datenbank Schreiben Sie ein C-Programm – ggf. mit Unterprogrammen –, das folgendes leistet: 1. Erfassen von Daten (insert) Station Zimmer Medikament Patient (Zimmer darf nicht überbelegt werden!) Angestellter/Arzt (Fach + Telefonnummer oder keins von beiden!) Behandlung Verordnung 2. Lesen von Daten (single select) Daten für ein Zimmer anzeigen, d. h. Stationsname, Zimmernummer und Anzahl der freien und belegten Betten. 3. Verändern von Daten (update) Verlegen eines Patienten von einem Zimmer in ein anderes, wobei auch hier das neue Zimmer nicht überbelegt werden darf. Eintragen/Ändern/Löschen einer Diagnose 4. Löschen von Daten (delete) Entlassung eines Patienten, wobei nach Eingabe der Patientennummer sämtliche den Patienten betreffenden Daten aus der Datenbank gelöscht werden. Natürlich wird vorher gefragt, ob man den Patienten mit soundsoviel Behandlungen und soundsoviel Verordnungen wirklich löschen möchte, sofern der Patient noch in Behandlung ist. 5.2 Hinweise zu den Programmieraufgaben Arbeiten Sie mit Haupt- und Untermenüs, wobei das Hauptmenü die obigen Punkte zur Auswahl anbietet. Schließen Sie jede Transaktion (aber nicht unbedingt jedes SQL-Statement) mit commit work ab. Möchten Sie zwischendurch eine Transaktion abbrechen, so verwenden Sie rollback work. Vor der Programmierung sollen Struktogramme entworfen werden, die die SQL-Anweisungen bereits enthalten. Dies dient der Konzentration auf Abläufe und Datenbanken, ohne gleichzeitig an die Programmiersprache denken zu müssen. Bauen Sie Fehlerbehandlungen mit Klartext-Meldungen in Deutsch für alle Fehlersituationen ein, die Sie vorhersehen können, beispielsweise Station nicht vorhanden!“ bei der ” 9 6 ABFRAGEN MIT MEHREREN ERGEBNISTUPELN Zimmereingabe. Nur für nicht vorhersehbare Fehler soll die Standard-Fehlerbehandlung einer Funktion StatusAnzeige() oder ähnlich verwendet werden. 6 Abfragen mit mehreren Ergebnistupeln Wenn bei einem SELECT mehr als ein einziges Tupel herauskommen kann, reicht ein Single Select“ nicht aus. Man muss ein Bulk Select“ oder eine sequentielle Tabellenverar” ” beitung mit cursor und fetch machen. 6.1 Bulk Select – Abfragen mehrerer Tupel auf einmal Bei einem Bulk Select wird die Ergebnistabelle auf einmal in den Datenbereich des Programms übertragen. Das bedeutet, dass das Programm einen ausreichend großen Datenbereich zur Verfügung stellen muss. Der Programmierer muss also schon zur Compilationszeit abschätzen, wieviele Tupel/Datensätze beim Select entstehen können. Bulk Select ist im ANSI-SQL-Standard nicht enthalten, wird aber von vielen Datenbanksystemen angeboten – aber natürlich nicht auf exakt gleiche Art. Im Gegensatz zum Single Select werden hier nicht einzelne Host-Variablen angelegt, sondern Host-Arrays (siehe Kapitel 8 im Oracle .Pro*C/C++ Precompiler Programmer’s Guide). exec sql begin declare section; int nr [20]; /* 20 int-Werte */ char spielname[20][30]; /* 20 Zeichenketten der Länge 29 */ exec sql end declare section; Bei obiger Deklaration war keine Indikatorvariable notwendig, weil alle Attribute mit not null versehen sind. Werden Indikatorvariablen benötigt, so sind sie ebenfalls als Array zu deklarieren. Die Aufrufsyntax des Bulk Select unterscheidet sich vom gewöhnlichen Select kaum und sieht beispielsweise so aus: exec sql select spielnr, spielname into :nr, :spielname from spieler; In der Variablen sqlerrd [2] der SQL Communication Area teilt uns SQL mit, wieviele Tupel in unser Array übertragen worden sind. Dies ist eine weitere Statusinformation über den sqlcode hinaus. Zur Weiterverarbeitung in unserem Programm benötigen wir diese Angabe z. B. in einer for-Schleife: for (i=0; i<sqlca.sqlerrd[2]; i++) { printf ("Spieler[%d]: %d, %s\n", i, nr[i], spielname[i]); } 10 6 ABFRAGEN MIT MEHREREN ERGEBNISTUPELN 6.2 Cursor-Select Falls die Arrays groß genug waren, um alle Daten aufzunehmen, enthält sqlca.sqlcode den Wert 100, da das Ende der Daten erreicht wurde. Sollte es mehr Spieler geben als in die Arrays passen, so gibt es wieder den Fehlerstatus −2112 in sqlca.sqlcode, Die Variable sqlca.sqlerrd[2] enthält jetzt auch die Anzahl der passenden Tupel, aber es sind nicht alle Daten übertragen worden. Ein vollständiges Beispielprogramm finden Sie in bulkselect.pc 6.2 Sequentielle Tabellenverarbeitung mit Cursor 6.2.1 Einfache Cursor-Abfragen Bei der sequentiellen Tabellenverarbeitung wird von SQL im Hintergrund eine Tabelle erzeugt, die das Ergebnis des Select enthält. Das Programm benötigt wie beim Single Select nur die Variablen für ein einziges Tupel, weil diese aus der Hintergrundtabelle wie aus einer sequentiellen Datei gelesen werden. Ein Cursor dient als Satzzeiger auf die Hintergrundtabelle. Bei diesem sequential Select“ muss man nicht vorher wissen, wieviele Tupel die Ergeb” nistabelle enthalten wird. Dafür ist die Anzahl der Zugriffe auf die Datenbank viel höher, und Sperren auf den Datenbestand bestehen im Allgemeinen länger. Folgende Embedded-SQL-Kommandos gibt es für das Arbeiten mit sequential Selects: declare cursorname cursor for selectcommand ; verbindet einen Cursor über seinen Namen mit einem spezifizierten Select-Kommando. Der Cursor ist ein Zeiger auf ein Tupel der Ergebnistabelle, die durch das Select erzeugt werden kann. Die Ergebnistabelle wird durch declare cursor noch nicht erzeugt. open cursorname ; führt das mit dem Cursor verbundene Select-Kommando tatsächlich durch und erzeugt die Ergebnistabelle ( active set“) im Hintergrund. Der Cursor steht jetzt vor dem ersten Tupel. ” Ab jetzt können Sperren auf die Datenbank bestehen. fetch cursorname into hostvariablen ; bewegt den Cursor auf das nächste Tupel der Ergebnistabelle und überträgt die Werte des neuen Tupels in die Hostvariablen. delete from tablename where current of cursorname ; löscht das Tupel, auf das der Cursor zeigt. Der Cursor steht anschließend vor dem nächsten Tupel (nicht auf dem nächsten). Geht natürlich nur bei Abfragen auf eine Einzeltabelle, nicht bei Joins. update tablename set columnname = expression where current of cursorname ; verändert in dem Tupel, auf das der Cursor zeigt, das genannte Attribut entsprechend dem im Ausdruck angegebenen Wert. Es können mehrere Anweisungen columnname = expression verwendet werden, die durch Komma getrennt werden. expression kann ein SQL-Ausdruck, eine Hostvariable (ggf. mit Nullwertindikator), ein Literal oder NULL sein. 11 6.2 Cursor-Select 6 ABFRAGEN MIT MEHREREN ERGEBNISTUPELN close cursorname ; deaktiviert die Ergebnistabelle (active set). Nach close sind fetch, delete where current und update where current nicht mehr möglich. Durch commit work und rollback work werden alle Cursor ebenfalls geschlossen. Um die Strafentabelle mit sequentieller Tabellenverarbeitung anzuzeigen, programmiert man beispielsweise so (ohne die notwendige Fehlerprüfung): exec sql declare kursor cursor for select zahlnr, st.spielnr, spielname, datum, betrag from strafen st, spieler sp where st.spielnr = sp.spielnr; exec sql open kursor; while (1) { exec sql fetch kursor into :zahlnr, :spielnr, :spielname, :datum, :betrag; if (sqlca.sqlcode != 0) break; printf ("%4d %4d %-30s %10s %10.2f\n", zahlnr, spielnr, spielname, datum, betrag); } exec sql close kursor; Es finden bei der sequentiellen Tabellenverarbeitung dieselben Fehlercodes Anwendung wie beim Single Select. Es wird aus der Ergebnistabelle gelesen, bis kein Tupel mehr übrig ist – dann wird sqlca.sqlcode den Wert 100 erhalten. Das vollständige Programm (mit der notwendigen Fehlerprüfung) finden Sie in der Datei cursor_strafen.pc Aufgabe: Bauen Sie in die Patientenaufnahme ein, dass bei der Eingabe eines belegten Zimmers eine Liste aller anderen Zimmer derselben Station angezeigt wird, in denen noch Betten frei sind. Sind auf der Station keine Zimmer mehr frei, soll auch dies im Klartext gemeldet werden. 6.2.2 Cursor-Abfragen mit where current Möchte man am aktuellen Tupel nach dem FETCH Änderungen vornehmen oder das Tupel löschen, so kann man die besondere Klausel WHERE CURRENT OF cursorname benutzen. Das sieht dann so aus: exec sql update spieler set gebjahr = gebjahr +1 where current of kursor; 12 7 IMPLIZITE FEHLERBEHANDLUNG oder exec sql delete spieler where current of kursor; Ein vollständiges Beispiel finden Sie unter dem Namen cursor_update_where_current.pc. 6.2.3 Oracle-Zusätze bei Cursor-Abfragen Oracle erlaubt bei Cursor-Abfragen ein FETCH auch mit Arrays, was auch BULK FETCH“ ” genannt wird. Hierbei werden die Tupel nicht einzeln, sondern in Gruppen abgeholt. Näheres dazu finden Sie in der Oracle-Dokumentation unter der Überschrift Cursor Fetches“ ” im Kapitel 8 des Oracle .Pro*C/C++ Precompiler Programmer’s Guide. 7 Implizite Fehlerbehandlung Bislang haben wir alle Fehler, die in den Programmen auftreten konnten, explizit behandelt, d. h. die Rückmeldungen von SQL wurden von unseren Programmcode ausgewertet. SQL bietet aber noch eine andere Art der Fehlerbehandlung an, bei der man weniger schreiben muss, dafür aber auch weniger Möglichkeiten hat. Am Anfang des Programms wird eine whenever-Anweisung verwendet. Die Parameter dieser Anweisung sind in Tabelle 4 auf der nächsten Seite erläutert. exec sql whenever condition action ; Solange keine whenever-Anweisung im Programm verwendet wurde, gilt die Aktions-Voreinstellung continue. Das Kommando whenever condition continue wird nur dazu gebraucht, ein früheres whenever mit einer anderen Aktion wieder auf diesen voreingestellten Wert zurückzusetzen. Die mit whenever festgelegten Aktion gelten solange, bis ein erneutes whenever verwendet wird. Die whenever-Anweisungen werden vollständig vom Precompiler ausgewertet und bewirken die Erzeugung von entsprechenden C-Anweisungen. Eine stop-Aktion hält das Programm unter Zurückfahren der aktuellen Transaktion an, zeigt aber leider keinerlei Meldung über den Grund an. Eine continue-Aktion sollte man nur dann einstellen, wenn man die Fehlerbehandlung selbst übernimmt, denn ignorierte Fehler führen zu nicht absehbaren Reaktionen des Programms. Das Springen zu einer Marke sollte nur in Notfällen verwendet werden. Eine do break-Aktion gibt es nur bei wenigen Datenbanken; außerdem ist hierbei zu berücksichtigen, dass ein break nur in einer Schleife oder einem switch-Statement auftauchen darf. Fügt der Precompiler es an anderer Stelle ein, so erzeugt das einen Fehler beim C-Compiler. Gleiches gilt für do continue. Die Aktion do Funktionsaufruf (...) ist ebenfalls Oracle-spezifisch, sollte also mit Vorsicht eingesetzt werden. 13 8 WEITERE AUFGABEN Tabelle 4: WHENEVER-Übersicht Anweisungsteil Schlüsselwort condition sqlerror sqlwarning not found action stop continue goto label do break do continue do Funktionsaufruf Bedeutung sqlcode < 0 sqlwarn [0] = ’W’ (kein ANSI-SQL) sqlcode = 100 Programmende mit rollback work keine Aktion, wie ohne WHENEVER Sprung zur Marke (igitt!! ) führt break aus (kein ANSI-SQL) führt continue aus (kein ANSI-SQL) ruft genannte C-Funktion auf; nach Verlassen der Funktion läuft das Programm ganz normal weiter (kein ANSISQL) 8 weitere Aufgaben 1. Auch wenn Sie es außerhalb dieser Übung niemals verwenden: Schlagen Sie in einem C-Buch nach, wie man mit goto umgeht und bauen Sie es in ein Programm mit whenever ein. 2. Verwenden Sie whenever in einem Ihrer Programme und schauen Sie nach, was der SQL-Precompiler aus Ihren SQL-Statements macht. Was verändert sich gegenüber der Voreinstellung? Vergleichen Sie mit SQL-Precompiler-Outputs ohne whenever. Tip hierzu: Vergleichen Sie die zwei C-Quellcodes mit tkdiff (geht nur unter X). 3. Schauen Sie sich das Beispielprogramm whenever.pc zur Verwendung von whenever an. Bauen Sie in das Programm folgende Fehler ein und schauen Sie, wie das Programm darauf reagiert: a) connect-Problem: Geben Sie eine Datenbank an, die nicht existiert. b) select-Problem: Geben Sie eine Tabelle oder ein Attribut an, das nicht existiert. c) select-Problem: Verwenden Sie bei einem Attribut einen falschen Datentyp oder ein nicht existierendes Attribut. d) insert-Problem: Versuchen Sie, ein Tupel einzutragen, das nicht eingetragen werden darf, weil es gegen eine Bedingung (Primärschlüssel, Fremdschlüssel, Checkbedingung) vestößt. $Id: SQL-Programmierung-C-Oracle.tex,v 1.1 2008-03-28 10:56:02 hj Exp $ 14