mitmario.dev

Anlegen und füllen

Python Sandbox 5 Min Lesezeit 5 BeispieleLektion 2 von 6

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

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

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

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

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

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, kostenlos

Zu 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.