Datenbankprogrammierung mit Python ©Ralf Adams Version vom: 02.03.2017 Inhaltsverzeichnis 1 setup-Programm 1.1 Erstellen des ER-Modells . . . . . 1.2 Verbindung auf- und abbauen . . . 1.3 Datenbanken erstellen und löschen 1.4 Tabellen anlegen . . . . . . . . . . 1.5 Zwischenbericht . . . . . . . . . . 1.6 Tabellen löschen . . . . . . . . . . 1.7 Tabellen ändern . . . . . . . . . . Datenbankprogrammierung mit Python . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 2 4 7 12 18 19 20 1 1 setup-Programm 1 setup-Programm 1 setup-Programm Der Deutsche Fußballbund (DFB)1 möchte Spielergebnisse, Trainerverpflichtungen und Titelgewinne Online darstellen. Dazu soll eine relationale Datenbank verwendet werden. Sie sind Teil des Projektteams und für die Planung, Programmierung und den Test der Datenhaltungsschicht verantwortlich. Ihr erster Teilauftrag besteht darin, ein setup-Programm in Python zu erstellen, welches die Datenbank, die Tabellen und die Inhalte anlegt. Ebenso sollen die CSV-Dateien importiert werden. Zum Abschluss sollen zwei SQL-User angelegt werden: bundesliga_root und bundesliga_user. 1.1 Erstellen des ER-Modells Zunächst müssen wir über Reverse Engineering die vorhanden Daten analysieren und die Struktur in Form eines ER-Modells ermitteln. Dieses ER-Modell ist dann später die Basis für die entsprechenden SQL-Befehle zum Anlegen der Tabellen. Mit Hilfe des Arbeitsblattes DAB.2.1 soll im Unterricht die entsprechende Analyse betrieben werden. Die Hausaufgabe dient der Eigenkontrolle, ob Sie das Vorgehen auch an einem anderen Beispiel wiederholen können. Das ER-Modell sollte ungefähr dem in Abbildung 1 entsprechen. Hier Auszüge aus den CSV-Dateien: • vereine.csv 1 2 3 4 5 1;Meidericher SV 02 Duisburg;MSV Duisburg;17.09.1902;Blau ,→ Weiß;http://www.msv-duisburg.de 2;FC Gelsenkirchen-Schalke 04;FC Schalke 04;04.05.1904;Blau ,→ Weiß;http://www.schalke04.de 3;FC Hansa Rostock;Hansa Rostock;01.11.1954;Weiß ,→ Blau;http://www.fc-hansa-rostock.de 4;SV Werder Bremen;Werder Bremen;04.02.1899;Grün Weiß;http://www.werder-online.de 5;TSV München von 1860;1860 München;17.05.1860;Weiß Blau;http://www.tsv1860.de • trainer.csv 1 2 3 4 5 1;Ilia;Gruev;Bulgarien;1969-10-30 2;Gino;Lettieri;Italien;1966-12-23 3;Karsten;Baumann;Deutschland;1969-10-14 4;Kosta;Runjaic;Deutschland;1971-06-04 5;Ivica;Grlić;Bosnien-Herzegowina;1975-08-06 • trainer_verein.csv 1 2 3 4 5 1 1;1;2015-11-03;2017-06-30 1;2;2014-07-01;2015-11-01 1;3;2013-07-01;2014-06-14 1;4;2012-09-03;2013-06-30 1;5;2012-08-27;2012-08-31 Nicht wirklich ;-) Datenbankprogrammierung mit Python 2 reverse engineering DAB.2.1 1 setup-Programm 1.1 Erstellen des ER-Modells • titel.csv 1 2 3 4 5 27;Deutscher Meister;1902/03 27;Deutscher Meister;1905/06 29;Berliner Meister;1905/06 26;Süddeutscher Meister;1907/08 41;Norddeutscher Meister;1907/08 • paarungen.csv 1 2 3 4 5 5;41;1963-08-24 17:00:00.000;1963/64;1;1;1;1;0 104;12;1963-08-24 17:00:00.000;1963/64;1;1;1;0;0 106;16;1963-08-24 17:00:00.000;1963/64;1;0;2;0;2 6;1;1963-08-24 17:00:00.000;1963/64;1;1;4;0;3 32;37;1963-08-24 17:00:00.000;1963/64;1;1;1;1;1 Abbildung 1: ER-Modell der Bundesliga-Datenbank Datenbankprogrammierung mit Python 3 1 setup-Programm 1.2 Verbindung auf- und abbauen 1.2 Verbindung auf- und abbauen Beginnen wir damit, eine Verbindung zum Server herzustellen. Zunächst tun wir dies auf eine sehr einfache Art und Weise. 1 import mysql.connector # MySql-API einbinden verbindung = mysql.connector.connect(user="root", password="", host="127.0.0.1", database="test") verbindung.close() # Verbindungsparameter festlegen DAB.2.2 2 3 4 5 6 7 # Schließen nicht vergessen In Zeile 1 wird die MySql-API, genauer die connector-Klasse importiert. In Zeile 3 wird eine Verbindung hergestellt. Die Verbindung wird in der Objektvariablen verbindung abgelegt. Beim Aufruf der Methoden connect() werden die Verbindungsparameter festgelegt. Die Angaben können natürlich auf Ihrem Server variieren. Wichtig ist das Schließen der Verbindung in Zeile 7. Ohne dieses Schließen würde die Verbindung offen bleiben und damit alle Ressourcen weiter binden, bis der Server einen timeout erzeugt. connect() timeout Aufgabe 1.1 Finden Sie heraus, was ein timeout ist und wie hoch er auf Ihrem Server eingestellt ist. Was dieser Lösung fehlt, ist eine Fehlerbehandlung. Wenn beispielsweise die Verbindungsparameter falsch sind oder der Server nicht gestartet wurde, soll das Programm nicht einfach abbrechen, sondern eine vernünftige Fehlermeldung ausgeben. Aufgabe 1.2 Recherchieren Sie: • Welche Bedeutung hat die try-/except-/finally-Umgebung? • Welche Fehlerkodes stellt mysql.connector.Error zur Verfügung? 1 2 import mysql.connector from mysql.connector import errorcode # Errorcodes importieren 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 try: # Versuche Verbindungsaufbau verbindung = mysql.connector.connect(user="root", password="", host="128.0.0.1", database="bundesliga") except mysql.connector.Error as err: # Abfangen eines Fehlers if err.errno == errorcode.ER_ACCESS_DENIED_ERROR: # Zugriff verweigert print("Benutzername oder Passwort falsch.") elif err.errno == errorcode.ER_BAD_DB_ERROR: # DB unbekannt print("Datenbank nicht vorhanden.") else: # Alle anderen Fehler print(err) else: verbindung.close() Datenbankprogrammierung mit Python 4 exception 1 setup-Programm 1.2 Verbindung auf- und abbauen In Zeile 2 werden die Codes für die Fehlermeldungen importiert. Mit diesen können gezielt bestimmte Fehlerursachen abgefragt werden. Nun wird der Verbindungsaufbau nicht mehr einfach so ausgeführt, sondern in einem try-Block (siehe Zeile 4). Wirft der Server eine Exception, wird diese in Zeile 9 aufgefangen und in den Zeilen 10 und 12 werden mögliche Fehlerursachen abgefragt. Falls keine dieser Ursachen zutrifft, wird nach Zeile 14 die Fehlermeldung des Servers ausgegeben. Aufgabe 1.3 Provozieren Sie die Fehlmeldungen wie folgt: • Ändern Sie das Passwort so, dass es falsch ist. • Ändern Sie den Datenbanknamen so, dass er eine nicht existierende angibt. • Ändern Sie den Host so, dass er auf eine IP-Adresse ohne SQL-Server verweist. Natürlich haben Sie daran gedacht, die vorherige Fehlerquelle wieder zu beheben, bevor Sie die neue ausprobieren ;-) Etwas unschön ist, dass die Übergabeparameter beim Aufruf der connect()-Methode festgelegt werden. Spätestens, wenn man mehrmals eine Verbindung aufbauen möchte, will man die Parameter nicht jedesmal festlegen. Es bietet sich an, die Daten in einem dictionary abzulegen. 1 2 import mysql.connector from mysql.connector import errorcode 3 4 5 6 7 8 9 verbindung_parameter = dict( user="root", password="", host="127.0.0.1", database="bundesliga") # Verbindungsparameter 10 11 12 13 14 15 16 17 18 19 20 21 try: verbindung = mysql.connector.connect(**verbindung_parameter) # dictionary übergeben except mysql.connector.Error as err: if err.errno == errorcode.ER_ACCESS_DENIED_ERROR: print("Benutzername oder Passwort falsch.") elif err.errno == errorcode.ER_BAD_DB_ERROR: print("Datenbank nicht vorhanden.") else: print(err) else: verbindung.close() In Zeile 5 wird das dictionary verbindung_parameter mit den Verbindungsparametern erstellt. Das Wort vor dem Gleichheitszeichen ist der Schlüssel und das Wort dahinter der Wert. So kann der Wert bequem über einen sprechenden Namen abgerufen werden. Alternativ zur Initialisierung mit dem Konstruktor wäre auch folgende Schreibweise denkbar: Datenbankprogrammierung mit Python 5 dictionary 1 setup-Programm 5 6 7 8 9 1.2 Verbindung auf- und abbauen conn_parameter = { "user": "root", "password": "myroot", "host": "127.0.0.1", "database": "bundesliga"} In Zeile 12 wird die Verbindung nun unter Verwendung des dictionaries (keyword argument unpacking) aufgerufen. keyword argument unpacking Aufgabe 1.4 Recherchieren Sie: Was bedeutet keyword argument unpacking? Noch besser wäre eine externe Konfigurationsdatei, in welcher man die Verbindungsparameter ablegen kann. So muss bei geänderten Verbindungsparameter nicht der Quelltext neu übersetzt werden. Legen Sie eine Datei mit dem Namen settings.cfg im gleichen Verzeichnis an und fügen Sie folgenden Inhalt ein: 1 2 3 4 5 [VERBINDUNGSPARAMETER] user = root password = host = 127.0.0.1 database = bundesliga Unterhalb einer in eckigen Klammern benannten Sektion können die Verbindungsdaten abgespeichert werden. Der Name der Sektion wird üblicherweise in Großbuchstaben geschrieben. 1 2 3 import mysql.connector from mysql.connector import errorcode import configparser # Importieren des Moduls 4 5 6 7 config = configparser.ConfigParser() config.read("settings01.cfg") verbindung_parameter = config["VERBINDUNGSPARAMETER"] # Anlegen eines Objekts # Auslesen der Sektion 8 9 10 11 12 13 14 15 16 17 18 19 try: verbindung = mysql.connector.connect(**verbindung_parameter) except mysql.connector.Error as err: if err.errno == errorcode.ER_ACCESS_DENIED_ERROR: print("Benutzername oder Passwort falsch.") elif err.errno == errorcode.ER_BAD_DB_ERROR: print("Datenbank nicht vorhanden.") else: print(err) else: verbindung.close() Zeile 3 importiert das benötigte Modul und in Zeile 5 wird ein ConfigParser-Objekt angelegt. In der nachfolgenden Zeile wird die angegebene Datei eingelesen. In Zeile 7 wird der Inhalt der Sektion als dictionary zur Verfügung gestellt und in verbindung_parameter abgespeichert. Ab da ist alles wie gehabt. Datenbankprogrammierung mit Python 6 configparser 1 setup-Programm 1.3 Datenbanken erstellen und löschen 1.3 Datenbanken erstellen und löschen Sie sollen eine Funktionsbibliothek aufbauen, welche Funktionen zum Anlegen und Löschen von Datenbanken enthält. Diese Funktionen sollen in einer Klasse gekapselt werden. Die Klasse soll alle wichtigen Informationen als Eigenschaften verwalten. In Abbildung 2 ist der erste Entwurf des entsprechenden Klassendiagramms zu sehen. DAB.2.3 DatenbankTools + konfigurationsdatei : string = "settings.cfg" + dbname : string = "datenbank_test" + zeichensatz : string = "utf8" + sortierung : string = "utf8_general_ci" + anlegen(): bool Abbildung 2: Klassendiagramm DatenbankTools, Version 1 • Die Eigenschaft konfigurationsdatei enthält den Namen (und ggf. auch den Pfad) der Konfigurationsdatei. Die Vorbelegung ist settings.cfg. • Die Eigenschaft dbname enthält den Namen der anzulegenden Datenbank. Die Vorbelegung ist datenbank_test. • Die Eigenschaft zeichensatz enthält den Namen des Zeichensatzes, der als Vorbelegung bei allen zeichenbasierenden Datentypen verwendet wird. Die Vorbelegung ist utf8. Zeichensatz character set • Die Eigenschaft sortierung enthält den Namen der Sortierung des Zeichensatzes. Die Vorbelegung ist utf8_general_ci. Sortierung collation Aufgabe 1.5 Recherchieren Sie: 1. Was sind Zeichensätze und welche sind für deutschsprachige Anwendungen interessant? 2. Was bedeuten die Sortierungen und welche sind für deutschsprachige Anwendungen interessant? 3. Welche zwei Sortiernormen sind in Deutschland gültig? Die Umsetzung beginnt damit, dass wir zuerst eine neue Konfigurationsdatei settings02.cfg, welche keinen Datenbanknamen enthält, anlegen. 1 2 3 4 [VERBINDUNGSPARAMETER] user = root password = host = 127.0.0.1 Die Datei sqltools.py braucht nur zwei Module: Datenbankprogrammierung mit Python 7 1 setup-Programm 1 2 1.3 Datenbanken erstellen und löschen import mysql.connector import configparser Im Konstruktor werden die Eigenschaften (Objektvariablen, Attribute) der Klasse mit den Startwerten versehen. Meine Konfigurationsdatei heißt nur etwas anders, weil ich nicht mit meinen anderen durcheinander kommen möchte. 5 6 7 8 9 10 11 12 ################################################### class DatenbankTools: def __init__(self): # Instanzvariablen vorgeben self.konfigurationsdatei = "settings02.cfg" self.dbname = "datenbank_test" self.zeichensatz = "utf8" self.sortierung = "utf8_general_ci" Nun wird die Methode anlegen() implementiert. Zuerst werden ab Zeile 16 die Datentypen der Eigenschaften überprüft. Damit erfolgt keine inhaltliche Prüfung – beispielsweise darüber, ob es diesen Zeichensatz überhaupt gibt –, sondern nur, ob die Eigenschaft überhaupt ein String ist. Eine solche Überprüfung ist notwendig, da Python eine sogenannte schwach typisierte Sprache ist. Es ist dem Programmierer überlassen, ob und wie er die Datentypen überprüft. 14 15 16 17 18 19 20 #################################### def anlegen(self): # Sind die Eigenschaften vom richtigen Datentyp? assert isinstance(self.konfigurationsdatei, str) assert isinstance(self.dbname, str) assert isinstance(self.zeichensatz, str) assert isinstance(self.sortierung, str) Nun werden mit Hilfe der Konfigurationsdatei ab Zeile 24 die Verbindungsdaten eingelesen und die Verbindung aufgebaut. 22 23 24 25 26 try: config = configparser.ConfigParser() config.read(self.konfigurationsdatei) # Konfigdatei ohne Datenbankname verbindung_parameter = config["VERBINDUNGSPARAMETER"] verbindung = mysql.connector.connect(**verbindung_parameter) Alle Befehle werden über ein cursor-Objekt zum MySQL-Server gesendet. Dieses Objekt wird über das MySQLConnection-Objekt verbindung mit Hilfe der Methode cursor() in Zeile 28 ermittelt. Danach wird der SQL-Befehl zusammengebaut; der besseren Lesbarkeit wegen auf mehrere Zeilen verteilt. Das \n im Befehl ist eigentlich völlig überflüssig. Es hilft nur, bei einer Bildschirmausgabe des Befehls diesen besser lesen zu können, da die einzelnen Befehlsabschnitte auf Zeilen verteilt werden und durch ein Leerzeichen eingerückt werden. In Zeile 32 wird der SQL-Befehl ausgeführt. Dabei kann es auch wieder zu Fehlern kommen (Syntax falsch, keine Berechtigung, Verbindung abgebrochen usw.). Daher erfolgt das ganze in einem try-Block. Zum Schluss wird die Verbindung geschlossen. Datenbankprogrammierung mit Python 8 cursor 1 setup-Programm 28 29 30 31 32 33 1.3 Datenbanken erstellen und löschen cursor = verbindung.cursor() # Erzeugen eines Cursors befehl = "CREATE DATABASE IF NOT EXISTS {0} " \ "\n CHARACTER SET {1} " \ "\n COLLATE {2}".format(self.dbname, self.zeichensatz, ,→ self.sortierung) cursor.execute(befehl) # Befehl ausführen verbindung.close() War alles erfolgreich, wird der Wert True zurückgeliefert. Ist man in der exception gelandet, wird der Wert False zurückgeliefert. 34 35 36 37 except mysql.connector.Error as err: print("Konnte Datenbank '{0}' nicht anlegen: {1}".format(self.dbname, err)) return False return True Verbleibt nur noch der Einbau in das Programm setup.py: 1 from sqltools01 import DatenbankTools 2 3 4 5 6 7 8 9 10 11 12 13 14 try: db = DatenbankTools() db.dbname = "milchgesicht" db.zeichensatz = "latin1" db.sortierung = "latin1_general_ci" if db.anlegen(): print("Angelegt!") else: print("Nicht angelegt!") except Exception as err: print("Fehler: {0}".format(err)) Aufgabe 1.6 Erweitern Sie die Klasse um die Methode löschen() und überschreiben(). Die Methode überschreiben() soll eine ggf. vorhanden Datenbank vorher löschen und dann neu anlegen. Testen Sie, ob alle drei Methoden genau das tun, was Sie erwarten. Aufgabe 1.7 Es ist sicherlich nicht besonders schlau, bei der Methode überschreiben() jeweils eine Verbindung zum Server auf- und wieder abzubauen. Bauen Sie die Klasse und Methoden so um, dass in den Methoden löschen() und anlegen() nur dann eine Verbindung zum Server hergestellt wird, wenn nicht schon eine vorhanden ist. Zunächst brauchen wir eine neue Eigenschaft: verbindung (siehe Abbildung 3). In dieser Eigenschaft wird eine Verbindung abgelegt, die bei mehreren Methoden verwendet wird. So wird vermieden, dass jede Methode immer eine neue Verbindung aufbaut. Sie müssen nur noch prüfen, ob die Verbindung bereits vorhanden ist oder nicht (==None). Musterlösung: Im Konstruktor wird in Zeile 13 die neue Eigenschaft hinzugefügt und auf None initialisiert. Datenbankprogrammierung mit Python 9 1 setup-Programm 1.3 Datenbanken erstellen und löschen DatenbankTools + konfigurationsdatei : string = "settings.cfg" + dbname : string = "datenbank_test" + zeichensatz : string = "utf8" + sortierung : string = "utf8_general_ci" + verbindung : MySqlConnection = None + anlegen(): bool + löschen(): bool + überschreiben(): bool Abbildung 3: Klassendiagramm DatenbankTools, Version 2 13 self.verbindung = None # Die neue Eigenschaft! In der Methode anlegen() wird nun vorab gefragt, ob diese Eigenschaft den Wert None hat (siehe Zeile 23). Nur wenn keine offene Verbindung vorhanden ist, wird die Konfigurationsdatei eingelesen und eine lokale Verbindung hergestellt; ansonsten wird in Zeile 30 die bereits bestehende Verbindung verwendet. Ebenso wird auch nur die lokale Verbindung geschlossen. Die restlichen Zeilen der Methode bleiben gleich. 23 24 25 26 27 28 29 30 if self.verbindung == None: # verbinden, wenn keine vorhanden! config = configparser.ConfigParser() config.read(self.konfigurationsdatei) verbindung_parameter = config["VERBINDUNGSPARAMETER"] verbindung_lokal = mysql.connector.connect(**verbindung_parameter) cursor = verbindung_lokal.cursor() else: cursor = self.verbindung.cursor() # sonst verwende bestehende 31 32 33 34 35 36 37 befehl = "CREATE DATABASE IF NOT EXISTS {0} " \ "\n CHARACTER SET {1} " \ "\n COLLATE {2}".format(self.dbname, self.zeichensatz, ,→ self.sortierung) cursor.execute(befehl) if self.verbindung == None: verbindung_lokal.close() Die neue Methoden löschen() sieht fast genauso aus: 43 44 45 46 #################################### def löschen(self): assert isinstance(self.konfigurationsdatei, str) assert isinstance(self.dbname, str) 47 48 49 50 51 52 try: if self.verbindung == None: config = configparser.ConfigParser() config.read(self.konfigurationsdatei) verbindung_parameter = config["VERBINDUNGSPARAMETER"] Datenbankprogrammierung mit Python 10 1 setup-Programm 53 54 55 56 1.3 Datenbanken erstellen und löschen verbindung_lokal = mysql.connector.connect(**verbindung_parameter) cursor = verbindung_lokal.cursor() else: cursor = self.verbindung.cursor() 57 58 59 60 61 62 63 64 65 befehl = "DROP DATABASE IF EXISTS {0} ".format(self.dbname) cursor.execute(befehl) if self.verbindung == None: verbindung_lokal.close() except mysql.connector.Error as err: print("Konnte Datenbank '{0}' nicht löschen: {1}".format(self.dbname, err)) return False return True Die neue Methoden überschreiben() ist eigentlich nur ein Aufruf der beiden anderen Methoden. 68 69 70 71 72 73 74 #################################### def überschreiben(self): if not self.löschen(): return False if not self.anlegen(): return False return True 75 76 Aber wie sieht nun das Hauptprogramm aus? Ziel war ja, nicht jedes mal die Verbindung neu aufzubauen. 1 2 3 import mysql.connector import configparser from sqltools02 import DatenbankTools 4 5 6 7 8 9 10 try: db = DatenbankTools() config = configparser.ConfigParser() config.read(db.konfigurationsdatei) verbindung_parameter = config["VERBINDUNGSPARAMETER"] db.verbindung = mysql.connector.connect(**verbindung_parameter) 11 12 13 14 15 16 if db.überschreiben(): print("Überschrieben!") else: print("Nicht Überschrieben!") db.verbindung.close() 17 18 19 except Exception as err: print("Fehler: {0}".format(err)) Datenbankprogrammierung mit Python 11 1 setup-Programm 1.4 Tabellen anlegen 1.4 Tabellen anlegen TabellenTools + konfigurationsdatei : string = "settings.cfg" + dbname : string = "datenbank_test" + tabellenename : string = "test" + create : string = "" + verbindung : MySqlConnection = None + anlegen() : bool Abbildung 4: Klassendiagramm Tabelle, Version 1 Die Klasse TabellenTools im Modul sqltools.py ist sehr analog der Klasse DatenbankTools aufgebaut (siehe Abbildung 4). Es werden folgende die Eigenschaften benötigt: • konfigurationsdatei: Name der Konfigurationsdatei. • dbname: Name der Datenbank, in welcher die Tabelle angelegt werden soll. • tabellenname: Name der Tabelle • create: Die CREATE TABLE-Anweisung • verbindung: Eine ggf. schon vorhandene Verbindung Die Methode anlegen() holt sich alle Infos, die sie benötigt, aus den Eigenschaften. Die ersten Zeilen der Klasse sollten nun kein Problem mehr sein: 78 79 80 81 82 83 84 85 86 ################################################### class TabellenTools: def __init__(self): # Instanzvariablen vorgeben self.konfigurationsdatei = "settings02.cfg" self.dbname = "datenbank_test" self.tabellenname = "test" self.create_befehl = "" self.verbindung = None Die Eigenschaften werden grob auf ihre Datentyprichtigkeit hin überprüft. 89 90 91 92 93 def anlegen(self): assert isinstance(self.konfigurationsdatei, str) assert isinstance(self.dbname, str) assert isinstance(self.tabellenname, str) assert isinstance(self.create_befehl, str) Die Methode beginnt genauso so, wie die Methode anlegen() der Klasse DatenbankTools. Es wird lediglich neu festgelegt, auf welcher Datenbank die Tabellen angelegt werden sollen. Datenbankprogrammierung mit Python 12 DAB.2.4 1 setup-Programm 95 96 97 98 99 100 101 102 103 104 105 1.4 Tabellen anlegen try: if self.verbindung == None: config = configparser.ConfigParser() config.read(self.konfigurationsdatei) verbindung_parameter = config["VERBINDUNGSPARAMETER"] verbindung_lokal = mysql.connector.connect(**verbindung_parameter) verbindung_lokal.database = self.dbname cursor = verbindung_lokal.cursor() else: self.verbindung.database = self.dbname cursor = self.verbindung.cursor() In Zeile 107 wird lediglich der Befehl zum Server gesendet und danach wird das Ergebnis verwertet. 107 108 109 110 111 112 113 cursor.execute(self.create_befehl) # Anlegen der Tabelle if self.verbindung == None: verbindung_lokal.close() except mysql.connector.Error as err: print("Konnte Tabelle '{0}.{1}' nicht anlegen: {2}".format(self.dbname, ,→ self.tabellenname, err)) return False return True Aufgabe 1.8 Ermitteln Sie anhand der CSV-Dateien, welche Datentypen dort verwendet werden. Erstellen Sie anschließend für jede CSV-Datei entsprechend dem ER-Modell in Abbildung 1 das passende CREATE TABLE-Statement. Erweitern Sie die CREATE TABLE-Statements um die Primär- und Fremdschlüsselspezifikationen. Verbleibt der Einbau der CREATE TABLE-Anweisungen in das setup-Programm. In einer Variablen speicheren wir zunächst den Befehl zum Erzeugen einer Tabellen (siehe Zeile 6ff.). Bis Zeile 26 ist alles gleich geblieben. Nun wird ein TabellenTools-Objekt angelegt und die offene Verbindung weitergereicht. Anschließend wird die Tabelle angelegt. 1 2 3 4 import mysql.connector import configparser from sqltools03 import DatenbankTools from sqltools03 import TabellenTools 5 6 7 8 9 10 11 12 13 14 15 16 # Aufbau des Befehls create_befehl = '''CREATE TABLE verein ( verein_id INT AUTO_INCREMENT, name VARCHAR(255), kurzname VARCHAR(255), gegründet_am DATE, farben VARCHAR(255), homepage VARCHAR(255), PRIMARY KEY(verein_id) )''' Datenbankprogrammierung mit Python 13 Datentypen Spezifikation von PK und FK 1 setup-Programm 1.4 Tabellen anlegen 17 18 19 20 21 22 23 24 try: db = DatenbankTools() config = configparser.ConfigParser() config.read(db.konfigurationsdatei) verbindung_parameter = config["VERBINDUNGSPARAMETER"] db.verbindung = mysql.connector.connect(**verbindung_parameter) db.dbname = "bundesliga" 25 26 27 28 29 30 31 32 if db.überschreiben(): # Nur wenn DB-anlegen geklappt hat tab = TabellenTools() tab.verbindung = db.verbindung tab.dbname = db.dbname tab.tabellenname = "verein" tab.create_befehl = create_befehl; tab.anlegen() 33 34 db.verbindung.close() 35 36 37 except Exception as err: print("Fehler: {0}".format(err)) Aufgabe 1.9 Erweitern Sie das Setup-Programm so, dass es alle Tabellen anlegt. Testen Sie, ob die Tabellen auch wirklich angelegt wurden. Zusatz für Pfiffige: Versuchen Sie es mit Hilfe einer geeigneten Datenstruktur und einer Schleife. Musterlösung: Das Setup-Programm erweitert. Zunächst wird ab Zeile 6 eine Liste von TabellenTools-Objekt erstellt. 6 7 # Erstellen einer Tabellenliste liste_tabellen = [] 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 tab = TabellenTools() tab.tabellenname = "verein" tab.create_befehl = ''' CREATE TABLE verein ( verein_id INT AUTO_INCREMENT, name VARCHAR(255), kurzname VARCHAR(255), gegründet_am DATE, farben VARCHAR(255), homepage VARCHAR(255), PRIMARY KEY(verein_id) )''' liste_tabellen.append(tab) 23 24 25 26 27 28 29 tab = TabellenTools() tab.tabellenname = "paarung" tab.create_befehl = ''' CREATE TABLE paarung ( paarung_id INT AUTO_INCREMENT, Datenbankprogrammierung mit Python 14 1 setup-Programm 30 31 32 33 34 35 36 37 38 39 40 41 42 43 1.4 Tabellen anlegen heim_verein_id INT, gast_verein_id INT, anstoß DATETIME, saison CHAR(7), spieltag INT, tore_heim_ende INT, tore_gast_ende INT, tore_heim_halb INT, tore_gast_halb INT, PRIMARY KEY(paarung_id), FOREIGN KEY(heim_verein_id) REFERENCES verein(verein_id), FOREIGN KEY(gast_verein_id) REFERENCES verein(verein_id) )''' liste_tabellen.append(tab) 44 45 46 47 48 49 50 51 52 53 54 55 56 57 tab = TabellenTools() tab.tabellenname = "titelgewinn" tab.create_befehl = ''' CREATE TABLE titelgewinn ( titelgewinn_id INT AUTO_INCREMENT, titelname VARCHAR(255), saison CHAR(7), verein_id INT, PRIMARY KEY(titelgewinn_id), FOREIGN KEY(verein_id) REFERENCES verein(verein_id) )''' liste_tabellen.append(tab) 58 59 60 61 62 63 64 65 66 67 68 69 70 71 tab = TabellenTools() tab.tabellenname = "trainer" tab.create_befehl = ''' CREATE TABLE trainer ( trainer_id INT AUTO_INCREMENT, vorname VARCHAR(255), nachname VARCHAR(255), geboren_in VARCHAR(255), geboren_am DATE, PRIMARY KEY(trainer_id) )''' liste_tabellen.append(tab) 72 73 74 75 76 77 78 79 80 81 82 83 84 tab = TabellenTools() tab.tabellenname = "trainer_verein" tab.create_befehl = ''' CREATE TABLE trainer_verein ( trainer_id INT, verein_id INT, von DATE, bis DATE, FOREIGN KEY(trainer_id) REFERENCES trainer(trainer_id), FOREIGN KEY(verein_id) REFERENCES verein(verein_id) )''' Datenbankprogrammierung mit Python 15 1 setup-Programm 85 1.4 Tabellen anlegen liste_tabellen.append(tab) Nun können alle Tabellen bequem durch eine Schleife den gleichen Datenbanknamen erhalten (siehe Zeile 87): 87 88 for t in liste_tabellen: # Durchlaufen der Liste t.dbname = "bundesliga" Mit Hilfe der gleichen Schleifenkonstruktion (siehe Zeile 99) werden nun die Tabellen angelegt. 90 91 92 93 94 95 96 try: db = DatenbankTools() config = configparser.ConfigParser() config.read(db.konfigurationsdatei) verbindung_parameter = config["VERBINDUNGSPARAMETER"] db.verbindung = mysql.connector.connect(**verbindung_parameter) db.dbname = "bundesliga" 97 98 99 100 101 if db.überschreiben(): for t in liste_tabellen: # Durchlaufen der Liste t.verbindung = db.verbindung t.anlegen() 102 103 db.verbindung.close() 104 105 106 except Exception as err: print("Fehler: {0}".format(err)) Bitte beachten Sie die Reihenfolge. Es könnte sonst passierten, dass die Tabelle paarung vor der Tabelle verein angelegt werden würde. Dies würde zu einer Fehlermeldung führen, weil dann die Fremdschlüsselreferenzen ins leere verweisen. Noch schöner wäre es, wenn wir die Anwendung so umbauen würden, dass die Tabellendefinitionen nicht im Quelltext, sondern in externen Textdateien stehen würden. Sie erinnern sich? Trennung von Daten und Logik! Aufgabe 1.10 Legen Sie das Unterverzeichnis def_tab an und erstellen Sie dort 5 SQL-Dateien. Die Namen der Dateien soll beispielhaft folgenden Aufbau haben: 01_verein.sql, 02_paarung.sql ... In jeder Datei steht nur der CREATE TABLE-Befehl der entsprechenden Tabelle: 1 2 3 4 5 6 7 8 CREATE TABLE verein ( verein_id name kurzname gegründet_am farben homepage INT AUTO_INCREMENT, VARCHAR(255), VARCHAR(255), DATE, VARCHAR(255), VARCHAR(255), Datenbankprogrammierung mit Python 16 Modul os 1 setup-Programm 9 10 ) 1.4 Tabellen anlegen PRIMARY KEY(verein_id) Die Nummerierung der Dateinamen soll sicherstellen, dass die Tabellen in der richtigen Reihenfolge angelegt werden. Nun kopieren Sie die Datei settings02.cfg nach settings03.cfg und erweitern diese wie folgt: 1 2 3 4 [VERBINDUNGSPARAMETER] user = root password = host = 127.0.0.1 5 6 7 8 [VERZEICHNISSE] projekt = . tabellen = def_tab Nun sind das Projektverzeichnis und das Unterverzeichnis mit den Tabellendefinitionen frei wählbar. Verbleibt nur noch die Implementierung. 1 2 3 4 5 import mysql.connector import configparser from sqltools04 import DatenbankTools from sqltools04 import TabellenTools import os # Importieren der os-Funktionen 6 7 8 9 10 11 12 13 14 15 try: db = DatenbankTools() db.konfigurationsdatei = "settings03.cfg" config = configparser.ConfigParser() config.read(db.konfigurationsdatei) verbindung_parameter = config["VERBINDUNGSPARAMETER"] db.verbindung = mysql.connector.connect(**verbindung_parameter) db.dbname = "bundesliga" In Zeile 5 werden Betriebssystemfunktionen importiert. Diese brauchen wir zum Auslesen von Verzeichnis- und Dateiinhalten. Vergessen Sie nicht den Namen der Konfigurationsdatei nach settings03.cfg zu ändern! 17 18 19 20 # Einlesen der Dateinamen des Tabellenverzeichnisses tabellenverzeichnis = config["VERZEICHNISSE"]["projekt"] + os.sep + ,→ config["VERZEICHNISSE"]["tabellen"] dateiliste = os.listdir(tabellenverzeichnis) dateiliste.sort() # Nummerierung bestimmt Reihenfolge Ab der Zeile 17 werden die Verzeichnisinhalte eingelesen. Im ersten Schritt wird der Verzeichnisname aus dem Projektverzeichnis und dem Tabellendefinitionsverzeichnis zusammengebastelt. Anschließend werden mit os.listdir() die Dateinamen ermittelt. Sie müssen selbst sicherstellen, Datenbankprogrammierung mit Python 17 1 setup-Programm 1.5 Zwischenbericht dass nur die Definitionsdateien im Verzeichnis liegen. Zum Schluss werden in Zeile 20 die Dateinamen noch sortiert, da wir nicht davon ausgehen können, dass die Dateinamen immer in richtigen Reihenfolge eingelesen werden. 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 # Erstellen der Tabellenliste nun aus den Dateien liste_tabellen = [] for dateiname in dateiliste: länge = len(dateiname) tabname = dateiname[3:länge - 4] dateiname_komplett = tabellenverzeichnis + os.sep + dateiname try: befehl = open(dateiname_komplett, encoding="utf-8").read() t = TabellenTools(); t.dbname = db.dbname t.tabellenname = tabname t.create_befehl = befehl liste_tabellen.append(t) except OSError as err: print("Fehler beim Lesen der Datei {0}: {1}".format(dateiname, err)) Ab Zeile 22 wird nun die Tabellenliste aufgebaut. Im ersten Teil muss aus dem Dateinamen der Tabellenname herausgeschnitten werden. Dazu wird die Gesamtlänge des Dateinamens ermittelt und der vordere und hintere Teil weggeschnitten. Jetzt wird die Datei geöffnet und dabei ist das Encoding zu beachten. Da unsere Datenbank in Unicode utf-8 kodiert ist, sollten die Strings der Datei auch so ausgelesen werden. Mit read() wird der Inhalt komplett ausgelesen und in die String-Variable befehl abgespeichert. Verbleibt der Aufruf des Konstruktors von Tabelle und das Anhängen an die Liste. Der Rest des Quelltextes bleibt unverändert. 38 39 40 41 if db.überschreiben(): for t in liste_tabellen: t.verbindung = db.verbindung t.anlegen() 42 43 db.verbindung.close() 44 45 46 except Exception as err: print("Fehler: {0}".format(err)) 1.5 Zwischenbericht Unser Setup-Programm kann schon folgendes: • Die Zugangsparameter zum Datenbankserver werden aus einer Konfigurationsdatei ausgelesen. • Es kann eine neue Datenbank mit einem vorgegeben Namen angelegt werden. Dieser Datenbank kann eine Zeichenkodierung und eine Sortierung zugewiesen werden. • Eine ggf. vorhanden Datenbank gleichen namens kann überschrieben werden. Datenbankprogrammierung mit Python 18 1 setup-Programm 1.6 Tabellen löschen • In einem Verzeichnis können SQL-Skripte mit entsprechenden CREATE TABLE-Anweisungen abgelegt werden. Der Verzeichnisname ist über eine Konfigurationsdatei einstellbar. • Die SQL-Dateien werden sortiert und entsprechend ihrer Reihenfolge ausgeführt. Somit stehen uns nach Ausführung des Setup-Programmes die Bundesliga-Datenbank und die entsprechenden Tabellen zur Verfügung. Wir können also schon eine ganze Menge, oder? 1.6 Tabellen löschen Aufgabe 1.11 Erweitern Sie die Klasse TabellenTools um die Methode löschen() (siehe Abbildung 5). Legen Sie zum Test dazu eine neue sinnlose Tabellen an. TabellenTools + konfigurationsdatei : string = "settings.cfg" + dbname : string = "datenbank_test" + tabellenename : string = "test" + create : string = "" + verbindung : MySqlConnection = None + anlegen() : bool + löschen() : bool Abbildung 5: Klassendiagramm Tabelle, Version 2 115 116 117 118 119 #################################### def löschen(self): assert isinstance(self.konfigurationsdatei, str) assert isinstance(self.dbname, str) assert isinstance(self.tabellenname, str) 120 121 122 123 124 125 126 127 128 129 130 131 try: if self.verbindung == None: config = configparser.ConfigParser() config.read(self.konfigurationsdatei) verbindung_parameter = config["VERBINDUNGSPARAMETER"] verbindung_lokal = mysql.connector.connect(**verbindung_parameter) verbindung_lokal.database = self.dbname cursor = verbindung_lokal.cursor() else: cursor = self.verbindung.cursor() self.verbindung.database = self.dbname 132 133 134 135 136 137 befehl = "DROP TABLE IF EXISTS {0} ".format(self.tabellenname) cursor.execute(befehl) if self.verbindung == None: verbindung_lokal.close() except mysql.connector.Error as err: Datenbankprogrammierung mit Python 19 1 setup-Programm 138 139 140 1.7 Tabellen ändern print("Konnte Tabelle '{0}.{1}' nicht löschen: {2}".format(self.tabellenname, ,→ self.dbname, err)) return False return True Der Test könnte wie folgt aussehen: 143 144 145 146 147 148 149 150 ################################################### # Hauptprogramm if __name__ == "__main__": db = DatenbankTools() if db.anlegen(): print("DB Angelegt!") else: print("DB nicht angelegt!") 151 152 153 154 155 if db.löschen(): print("DB Gelöscht!") else: print("DB nicht gelöscht!") 156 157 158 159 160 if db.überschreiben(): print("DB überschrieben!") else: print("DB nicht überschrieben!") 161 162 163 164 165 166 167 168 169 170 171 172 173 174 tab = TabellenTools() tab.konfigurationsdatei = "settings03.cfg" tab.dbname = "bundesliga"; tab.tabellenname = "wurst" tab.create_befehl = "CREATE TABLE IF NOT EXISTS wurst(i int, x varchar(100))" if tab.anlegen(): print("TAB angelegt") if tab.löschen(): print("TAB gelöscht") else: print("TAB nicht angelegt") else: print("TAB nicht angelegt") 1.7 Tabellen ändern Ich möchte die Tabellen so geändert haben, dass diese ein Löschkennzeichen (deleted vom Typ BOOL) besitzen. Dieses soll nicht NULL werden können und die Vorbelegung FALSE haben. Dazu muss die Klasse TabellenTools um die Methode ändern() erweitert werden (siehe Abbildung 6). Aufgabe 1.12 Schreiben Sie die Methode ändern(). Hinweis: Die Aufgabe ist einfach. Musterlösung: Datenbankprogrammierung mit Python 20 1 setup-Programm 1.7 Tabellen ändern TabellenTools + konfigurationsdatei : string = "settings.cfg" + dbname : string = "datenbank_test" + tabellenename : string = "test" + create : string = "" + verbindung : MySqlConnection = None + anlegen() : bool + löschen() : bool + ändern(alteroption : string) : bool Abbildung 6: Klassendiagramm Tabelle, Version 3 142 143 144 145 146 147 #################################### def ändern(self, alteroption): assert isinstance(self.konfigurationsdatei, str) assert isinstance(self.dbname, str) assert isinstance(self.tabellenname, str) assert isinstance(alteroption, str) 148 149 150 151 152 153 154 155 156 157 158 159 try: if self.verbindung == None: config = configparser.ConfigParser() config.read(self.konfigurationsdatei) verbindung_parameter = config["VERBINDUNGSPARAMETER"] verbindung_lokal = mysql.connector.connect(**verbindung_parameter) verbindung_lokal.database = self.dbname cursor = verbindung_lokal.cursor() else: cursor = self.verbindung.cursor() self.verbindung.database = self.dbname 160 161 162 163 164 165 166 167 168 befehl = "ALTER TABLE {0} {1}".format(self.tabellenname, alteroption) cursor.execute(befehl) if self.verbindung == None: verbindung_lokal.close() except mysql.connector.Error as err: print("Konnte Tabellenstruktur von '{0}.{1}' nicht verändern: ,→ {2}".format(self.tabellenname, self.dbname, err)) return False return True Jetzt muss nur noch das Setup-Programm erweitert werden: 44 45 46 47 # Anhängen des Löschkennzeichens alteroption = "ADD deleted BOOL NOT NULL DEFAULT FALSE" for t in liste_tabellen: t.ändern(alteroption) Ab Zeile 44 wird allen Tabellen – wieder kommt unsere supertolle Schleifenkonstruktion zum tragen – das entsprechende alteroption zugewiesen und die Änderungen werden ausgeführt. Datenbankprogrammierung mit Python 21