Abschnitt 16 · Lektion 2
Anlegen und füllen
Ein Programm trägt eine Zeile in eine Tabelle ein, und nach dem Ende ist sie einfach weg. Ohne jede Meldung. Hineinbringen ist in SQL ein Befehl und in Python ein Aufruf, und dazwischen liegt ein Schritt, den man beim ersten Mal leicht vergisst.
Die Beispiele spielen wieder in Mias Laden. Jedes fängt mit derselben Zeile an. unlink löscht
laden.db vom letzten Lauf, damit bei jedem Start mit einer leeren Datenbank angefangen wird. In
einem echten Programm steht diese Zeile natürlich nicht, das war ja die Lehre der letzten Lektion.
Eine Zeile eintragen
import sqlite3
from pathlib import Path
Path("laden.db").unlink(missing_ok=True) # jeder Lauf fängt ohne laden.db an
verbindung = sqlite3.connect("laden.db")
verbindung.execute("CREATE TABLE artikel (name TEXT, preis INTEGER, bestand INTEGER)")
verbindung.execute("INSERT INTO artikel VALUES ('Brot', 249, 12)")
verbindung.commit()
verbindung.close()
verbindung = sqlite3.connect("laden.db")
print(verbindung.execute("SELECT count(*) FROM artikel").fetchone())
verbindung.close() INSERT INTO artikel VALUES (...) trägt eine Zeile in die Tabelle ein. Dabei steht ein Wert je
Spalte da, in der Reihenfolge, in der die Spalten angelegt sind. Text steht in SQL in einfachen
Anführungszeichen, 'Brot', Zahlen ohne. Weil der ganze Befehl in Python schon in doppelten
Anführungszeichen steht, kommen sich die beiden nicht in die Quere.
Danach steht verbindung.commit(), und um diese Zeile geht es in der Lektion. Erst damit ist die
Zeile festgeschrieben (commit), also wirklich in der Datei.
Zur Probe wird eine zweite Verbindung geöffnet, und SELECT count(*) FROM artikel zählt die Zeilen.
Was ein SELECT alles kann, kommt in der nächsten Lektion. Hier reicht, dass eine Zahl zurückkommt.
Abgeholt wird die Antwort mit fetchone(), und zwar als Tupel mit einem Wert darin, deshalb steht
dort (1,). Ein Tupel mit nur einem Wert schreibt Python mit Komma, sonst wären es bloß Klammern um
eine Zahl. Die Lektion über Platzhalter kommt darauf zurück.
Ohne commit
import sqlite3
from pathlib import Path
Path("laden.db").unlink(missing_ok=True) # jeder Lauf fängt ohne laden.db an
verbindung = sqlite3.connect("laden.db")
verbindung.execute("CREATE TABLE artikel (name TEXT, preis INTEGER, bestand INTEGER)")
verbindung.execute("INSERT INTO artikel VALUES ('Brot', 249, 12)")
print(verbindung.execute("SELECT count(*) FROM artikel").fetchone())
verbindung.close()
verbindung = sqlite3.connect("laden.db")
print(verbindung.execute("SELECT count(*) FROM artikel").fetchone())
verbindung.close() Dasselbe Programm, nur ohne commit(). Gleich nach dem Eintragen wird über dieselbe Verbindung
nachgefragt, und dort steht die Zeile: (1,). Nach dem Schließen fragt die zweite Verbindung nach,
und die Tabelle ist leer: (0,). Kein Fehler, keine Warnung, nichts.
Das ist der Fehler dieser Lektion. Er ist so tückisch, weil sich beim Ausprobieren alles richtig
anfühlt. Ein INSERT öffnet in Python einen Vorgang (transaction), so etwas wie einen Entwurf, und
den sieht nur die Verbindung, die ihn angefangen hat. Erst commit() macht daraus den Stand der
Datei, den jeder sieht. Wird die Verbindung vorher geschlossen oder endet das Programm, verwirft
SQLite den Entwurf.
Das gilt für INSERT und für die Befehle, die Zeilen ändern oder löschen. Ein CREATE TABLE, das
wie hier vor dem ersten INSERT steht, wirkt dagegen sofort. Deshalb ist die Tabelle selbst noch
da.
Viele Zeilen auf einmal
import sqlite3
from pathlib import Path
Path("laden.db").unlink(missing_ok=True) # jeder Lauf fängt ohne laden.db an
verbindung = sqlite3.connect("laden.db")
verbindung.execute("CREATE TABLE artikel (name TEXT, preis INTEGER, bestand INTEGER)")
verbindung.execute("INSERT INTO artikel VALUES (?, ?, ?)", ("Käse", 399, 5))
lieferung = [("Brot", 249, 12), ("Milch", 119, 30), ("Butter", 229, 8)]
verbindung.executemany("INSERT INTO artikel VALUES (?, ?, ?)", lieferung)
verbindung.commit()
print(verbindung.execute("SELECT count(*) FROM artikel").fetchone())
verbindung.close() Meistens stehen die Werte nicht im Befehl, sondern in Python, in Variablen oder in einer Liste. Dann
stehen im Befehl Fragezeichen, eins je Wert, und die Werte kommen als Tupel hinter dem Befehl dazu:
execute("INSERT INTO artikel VALUES (?, ?, ?)", ("Käse", 399, 5)). Die Fragezeichen heißen
Platzhalter (placeholder), und sqlite3 setzt die Werte an ihrer Stelle ein.
Warum man Werte nie anders in einen Befehl bringt, auch nicht mit einem f-String, ist so wichtig, dass es dafür eine eigene Lektion gibt, die 16.4.
Für eine ganze Liste gibt es executemany. Das ist derselbe Befehl, einmal für jedes Tupel der
Liste. Hier sind das die drei Artikel der Lieferung, zusammen mit dem Käse also vier Zeilen, und ein
einziges commit() am Ende schreibt alle fest.
Text, wo eine Zahl hingehört
import sqlite3
from pathlib import Path
Path("laden.db").unlink(missing_ok=True) # jeder Lauf fängt ohne laden.db an
verbindung = sqlite3.connect("laden.db")
verbindung.execute("CREATE TABLE artikel (name TEXT, preis INTEGER, bestand INTEGER)")
# So kommen die Zeilen aus einer CSV mit csv.reader: alles Text.
zeilen = [["Brot", "2,49", "12"], ["Milch", "1,19", "30"]]
verbindung.executemany("INSERT INTO artikel VALUES (?, ?, ?)", zeilen)
verbindung.commit()
print(verbindung.execute("SELECT name, preis, bestand FROM artikel").fetchall())
print(verbindung.execute("SELECT sum(preis) FROM artikel").fetchone())
verbindung.close() Wer eine CSV einliest, bekommt jeden Wert als Text, das war die Lehre aus Abschnitt 12. Im Beispiel
geht dieser Text so, wie er ist, an executemany. Eine Fehlermeldung gibt es nicht.
SQLite nimmt "12" und macht in der Spalte für ganze Zahlen eine 12 daraus, weil der Text wie
eine ganze Zahl aussieht. "2,49" sieht nicht so aus, also legt SQLite ihn eben als Text ab, mitten
in der Spalte für ganze Zahlen. Die Art einer Spalte ist in SQLite eine Empfehlung und keine
Bedingung.
In der zweiten Zeile der Ausgabe steht als Summe der Preise 3.0. Beim Zusammenzählen nimmt SQLite
von '2,49' nur das, was vor dem Komma wie eine Zahl aussieht, also die 2, und von '1,19' die 1.
Umgewandelt wird deshalb vorher, in Python. Aus "2,49" wird erst ein Decimal wie in Lektion
12.2, dann mal 100 und mit int() ganze Cent, also 249. Über float sollte das nicht gehen.
int(float("4.10") * 100) gibt 409, weil die Kommazahl ein winziges Stück unter 410 liegt und
int den Rest einfach abschneidet.
Ein Datum braucht denselben Schritt, mit isoformat() aus der letzten Lektion. Ein date selbst
nimmt sqlite3 zwar noch an. Seit Python 3.12 erscheint dann aber eine DeprecationWarning auf dem
Fehlerkanal, wenn der Aufruf in der gestarteten Datei steht. Diese Hilfe gilt als veraltet und soll
irgendwann wegfallen.
Alles oder nichts
import sqlite3
from pathlib import Path
Path("laden.db").unlink(missing_ok=True) # jeder Lauf fängt ohne laden.db an
verbindung = sqlite3.connect("laden.db")
verbindung.execute("CREATE TABLE artikel (name TEXT, preis INTEGER, bestand INTEGER)")
# Bei der Milch fehlt der Bestand.
lieferung = [("Brot", 249, 12), ("Milch", 119), ("Butter", 229, 8)]
try:
with verbindung:
verbindung.executemany("INSERT INTO artikel VALUES (?, ?, ?)", lieferung)
except sqlite3.ProgrammingError as fehler:
print("Abgebrochen:", fehler)
print(verbindung.execute("SELECT count(*) FROM artikel").fetchone())
verbindung.close() Mit with verbindung: musst du dich um das commit() nicht mehr kümmern, denn am Ende des Blocks
wird festgeschrieben. Bricht im Block etwas ab, wird stattdessen alles zurückgenommen, was seit dem
letzten commit() passiert ist (rollback).
Im Beispiel fehlt bei der Milch ein Wert. executemany bricht dort mit
Incorrect number of bindings supplied ab, weil der Befehl drei Platzhalter hat und das Tupel nur
zwei Werte. Das Brot davor war schon eingetragen, und trotzdem ergibt das Zählen danach (0,).
Beim Einlesen einer Datei will man genau das. Entweder kommt die ganze Datei hinein oder nichts. Nach einem Abbruch lässt sie sich einfach noch einmal einlesen, ohne dass die halbe schon drinsteht. Mit einer CSV, die man von Hand zurückschreibt, bekommt man das kaum hin.
Anders als bei open() schließt with hier die Verbindung nicht. verbindung.close() steht
deshalb trotzdem am Ende.
Was die Aufgabe will
Die Ausgaben des Haushaltsbuchs stehen in einer CSV. Sie sollen in die Tabelle aus der letzten
Lektion, mit dem Datum in der Form 2026-09-03 und dem Betrag in Cent.
Zum Mitnehmen
Mit INSERT INTO kommt eine Zeile in die Tabelle, mit executemany() eine ganze Liste, die Werte als Tupel und im Befehl Fragezeichen. Dauerhaft sind die Zeilen erst nach commit(). Ein with-Block um die Verbindung erledigt das am Ende selbst oder nimmt bei einem Fehler alles zurück, und was in eine Zahlenspalte gehört, wird vorher umgewandelt, weil SQLite auch Text annimmt.
Jetzt du
Basis Konto, kostenlosZu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.
Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.
In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.
Was in dieser Lektion steckt
-
Artikel mit 5 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe, dein Code läuft auf einem Server
Öffnet sich mit dem Basis Konto.