17. Python – bazy danych – sqlite


Szybki quiz – przypomnienie poprzedniej lekcji

Temat poprzedniej lekcji: programowanie obiektowe.

Pytanie 1. Co zostanie wyświetlone?

class Gracz:
    def __init__(self, nick, hp=100):
        self.nick = nick
        self.hp = hp

g = Gracz("Mage", hp=80)
print(g.nick, g.hp)
Odpowiedź

Odpowiedź: Mage 80 – wartość podana przy tworzeniu obiektu nadpisuje wartość domyślną hp=100.

Pytanie 2. Znajdź błąd. Dlaczego program zgłasza AttributeError?

class Kot:
    def __init__(self, imie):
        imie = imie

    def info(self):
        print(self.imie)

Kot("Mruczek").info()
Odpowiedź

Odpowiedź: w konstruktorze imie = imie zmienia tylko zmienną lokalną. Atrybut obiektu trzeba zapisać jako self.imie = imie.

Pytanie 3. Dlaczego ten kod zgłosi AttributeError i jak go naprawić?

class Pojazd:
    def __init__(self, marka):
        self.marka = marka

class Samochod(Pojazd):
    def __init__(self, marka, model):
        self.model = model

auto = Samochod("Toyota", "Corolla")
print(auto.marka)
Odpowiedź

Odpowiedź: konstruktor klasy Samochod nie wywołuje konstruktora klasy Pojazd, więc atrybut marka nigdy nie powstał. Naprawa: dodać super().__init__(marka) w __init__ klasy Samochod.

Pytanie 4. Co zostanie wyświetlone?

class Ksiazka:
    def __init__(self, tytul):
        self.tytul = tytul

    def __str__(self):
        return f"Książka: {self.tytul}"

k = Ksiazka("Wiedźmin")
print(k)
Odpowiedź

Odpowiedź: Książka: Wiedźmin – print() wywołuje metodę __str__. Bez niej zobaczysz tylko typ obiektu i jego adres w pamięci.

Pytanie 5. Co się stanie po uruchomieniu tego kodu?

class Konto:
    def __init__(self):
        self.__saldo = 100

k = Konto()
print(k.__saldo)
Odpowiedź

Odpowiedź: AttributeError. Atrybut z dwoma podkreślnikami jest prywatny (Python zmienia jego nazwę – name mangling), więc nie można odczytać go spoza klasy.

Pytanie 6. Co się stanie po uruchomieniu tego kodu?

from abc import ABC, abstractmethod

class Zwierze(ABC):
    @abstractmethod
    def dzwiek(self):
        pass

z = Zwierze()
Odpowiedź

Odpowiedź: TypeError – z klasy abstrakcyjnej nie można tworzyć obiektów. Obiekt można utworzyć dopiero z klasy potomnej, która implementuje dzwiek().


Czym jest SQLite3?

  • SQLite3 to lekka, wbudowana relacyjna baza danych SQL
  • Nie wymaga osobnego serwera – cała baza to jeden plik
  • Python ma wbudowany moduł sqlite3 – nie trzeba nic instalować
  • Idealna do nauki, prototypów i małych/średnich aplikacji

SQLite3 to lekka, wbudowana relacyjna baza danych SQL

Nie wymaga osobnego serwera – cała baza to jeden plik

Python ma wbudowany moduł sqlite3 – nie trzeba nic instalować

Idealna do nauki, prototypów i małych/średnich aplikacji

Podstawowe pojęcia:

Połączenie (Connection) – łącze z plikiem bazy danych

Kursor (Cursor) – obiekt do wykonywania poleceń SQL

Commit – zatwierdzenie zmian w bazie

Rollback – cofnięcie niezatwierdzonych zmian

Analogia ze świata rzeczywistego:

Wyobraź sobie, że:

  • Baza danych = Biblioteka pełna książek
  • Connection (połączenie) = Karta biblioteczna (pozwala Ci wejść do biblioteki)
  • Cursor (kursor) = Bibliotekarz, który wykonuje dla Ciebie konkretne zadania
  • Dlaczego potrzebujemy kursora?

Nie możesz bezpośrednio rozmawiać z bazą danych! Potrzebujesz „pośrednika”, który:

  • Zaniesie Twoje polecenie do bazy danych
  • Wykona operację (znajdzie dane, doda coś, usunie)
  • Przyniesie Ci wyniki

Tym pośrednikiem jest właśnie KURSOR.

import sqlite3
# KROK 1: Otwierasz drzwi do biblioteki (połączenie)
conn = sqlite3.connect('mojabaza.db')
# Teraz jesteś w bibliotece, ale jeszcze nic nie możesz zrobić
# KROK 2: Wzywasz bibliotekarza (tworzysz kursor)
cursor = conn.cursor()
# Teraz masz "pomocnika", któremu możesz wydawać polecenia
# KROK 3: Wydajesz polecenie bibliotekarzowi
cursor.execute('SELECT * FROM ksiazki WHERE autor LIKE ?', ('%Tolkien%',))
# Bibliotekarz idzie, szuka książek Tolkiena
# KROK 4: Bibliotekarz przynosi Ci wyniki
wyniki = cursor.fetchall()
print(wyniki)
# KROK 5: Wychodzisz z biblioteki
conn.close()

PRZYKŁADY PRAKTYCZNE

Przykład 1: Podstawowe operacje CRUD

tworzymy plik o nazwie crud_basics.py

import sqlite3
# Połączenie z bazą (tworzy plik jeśli nie istnieje)
conn = sqlite3.connect('mojabaza.db')
cursor = conn.cursor()
# CREATE - Tworzenie tabeli
cursor.execute('''
CREATE TABLE IF NOT EXISTS pracownicy (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    imie TEXT NOT NULL,
    nazwisko TEXT NOT NULL,
    wiek INTEGER,
    wynagrodzenie REAL
)
''')
# INSERT - Wstawianie pojedynczego rekordu
cursor.execute('''
    INSERT INTO pracownicy (imie, nazwisko, wiek, wynagrodzenie)
    VALUES (?, ?, ?, ?)
''', ('Jan', 'Kowalski', 35, 5500.50))
# INSERT - Wstawianie wielu rekordów
dane = [
    ('Anna', 'Nowak', 28, 6200.00),
    ('Kacper', 'Malinowski', 22, 4100.75),
    ('Maria', 'Wiśniewska', 45, 7800.00)
]
cursor.executemany('''
    INSERT INTO pracownicy (imie, nazwisko, wiek, wynagrodzenie)
    VALUES (?, ?, ?, ?)
''', dane)
# Zatwierdzenie zmian
conn.commit()
# SELECT - Pobieranie wszystkich danych
cursor.execute('SELECT * FROM pracownicy')
wyniki = cursor.fetchall()
print("Wszyscy pracownicy:")
for wiersz in wyniki:
    print(wiersz)
# SELECT - Pobieranie z warunkiem
cursor.execute('SELECT imie, nazwisko FROM pracownicy WHERE wiek > ?', (30,))
starsi = cursor.fetchall()
print("\nPracownicy powyżej 30 lat:")
for osoba in starsi:
    print(f"{osoba[0]} {osoba[1]}")
# UPDATE - Aktualizacja danych
cursor.execute('''
    UPDATE pracownicy
    SET wynagrodzenie = wynagrodzenie * 1.1
    WHERE wiek < 30
''')
conn.commit()
# DELETE - Usuwanie danych
cursor.execute('DELETE FROM pracownicy WHERE id = ?', (1,))
conn.commit()
# Zamknięcie połączenia
conn.close()

Przykład 2: Context Manager (zalecane podejście)

tworzymy plik o nazwie context_manager.py

import sqlite3
 # Automatyczny commit (lub rollback przy błędzie) na końcu bloku with
with sqlite3.connect('sklep.db') as conn:
    cursor = conn.cursor()
   
    # Tworzenie tabeli produktów
    cursor.execute('''
    CREATE TABLE IF NOT EXISTS produkty (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        nazwa TEXT NOT NULL,
        cena REAL NOT NULL,
        ilosc INTEGER DEFAULT 0
    )
    ''')
   
    # Dane są automatycznie commitowane przy wyjściu z bloku 'with'
    cursor.execute('INSERT INTO produkty (nazwa, cena, ilosc) VALUES (?, ?, ?)',
                   ('Laptop', 3500.00, 5))
   
    # Pobieranie z użyciem row_factory dla dostępu jak do słownika
    conn.row_factory = sqlite3.Row
    cursor = conn.cursor()
    cursor.execute('SELECT * FROM produkty')
   
    for row in cursor:
        print(f"Produkt: {row['nazwa']}, Cena: {row['cena']} zł")

Przykład 3: Zaawansowane zapytania

tworzymy plik o nazwie advanced_queries.py

import sqlite3
with sqlite3.connect('firma.db') as conn:
    cursor = conn.cursor()
   
    # Tworzenie dwóch powiązanych tabel
    cursor.execute('''
    CREATE TABLE IF NOT EXISTS dzialy (
        id INTEGER PRIMARY KEY,
        nazwa TEXT NOT NULL
    )
    ''')
   
    cursor.execute('''
    CREATE TABLE IF NOT EXISTS pracownicy (
        id INTEGER PRIMARY KEY,
        imie TEXT NOT NULL,
        nazwisko TEXT NOT NULL,
        dzial_id INTEGER,
        FOREIGN KEY (dzial_id) REFERENCES dzialy(id)
    )
    ''')
   
    # Wstawienie działów
    dzialy = [(1, 'IT'), (2, 'HR'), (3, 'Sprzedaż')]
    cursor.executemany('INSERT OR IGNORE INTO dzialy VALUES (?, ?)', dzialy)
   
    # Wstawienie pracowników
    pracownicy = [
        ('Jan', 'Kowalski', 1),
        ('Anna', 'Nowak', 2),
        ('Piotr', 'Wiśniewski', 1)
    ]
    cursor.executemany('INSERT INTO pracownicy (imie, nazwisko, dzial_id) VALUES (?, ?, ?)', pracownicy)
   
    # JOIN - łączenie tabel
    cursor.execute('''
    SELECT p.imie, p.nazwisko, d.nazwa
    FROM pracownicy p
    JOIN dzialy d ON p.dzial_id = d.id
    WHERE d.nazwa = 'IT'
    ''')
   
    print("Pracownicy działu IT:")
    for row in cursor.fetchall():
        print(f"{row[0]} {row[1]} - {row[2]}")
   
    # GROUP BY - agregacja
    cursor.execute('''
    SELECT d.nazwa, COUNT(p.id) as liczba_pracownikow
    FROM dzialy d
    LEFT JOIN pracownicy p ON d.id = p.dzial_id
    GROUP BY d.nazwa
    ''')
   
    print("\nLiczba pracowników w działach:")
    for row in cursor.fetchall():
        print(f"{row[0]}: {row[1]}")

Uwaga: SQLite domyślnie nie sprawdza kluczy obcych. Aby FOREIGN KEY faktycznie blokował wpisy z nieistniejącym id, po połączeniu wykonaj:

conn.execute("PRAGMA foreign_keys = ON")

Przykład 4: Obsługa błędów

tworzymy plik o nazwie error_handling.py

import sqlite3
try:
    with sqlite3.connect('test.db') as conn:
        cursor = conn.cursor()
       
        cursor.execute('''
        CREATE TABLE IF NOT EXISTS users (
            id INTEGER PRIMARY KEY,
            email TEXT UNIQUE NOT NULL
        )
        ''')
       
        # Próba wstawienia duplikatu
        cursor.execute('INSERT INTO users (email) VALUES (?)', ('jan@example.com',))
        cursor.execute('INSERT INTO users (email) VALUES (?)', ('jan@example.com',))
       
except sqlite3.IntegrityError as e:
    print(f"Błąd integralności danych: {e}")
except sqlite3.OperationalError as e:
    print(f"Błąd operacyjny: {e}")
except sqlite3.Error as e:
    print(f"Błąd bazy danych: {e}")

Typy wyjątków:

sqlite3.WarningOstrzeżenie
sqlite3.ErrorBazowy wyjątek dla wszystkich błędów
sqlite3.InterfaceErrorBłąd interfejsu bazy
sqlite3.DatabaseErrorBłąd związany z bazą
sqlite3.DataErrorBłąd danych (np. dzielenie przez zero)
sqlite3.OperationalErrorBłąd operacyjny (np. brak tabeli)
sqlite3.IntegrityErrorNaruszenie integralności (np. UNIQUE)
sqlite3.InternalErrorBłąd wewnętrzny SQLite
sqlite3.ProgrammingErrorBłąd programowania (np. zły SQL)
sqlite3.NotSupportedErrorNieobsługiwana operacja

Przykład 5:

#https://pypi.org/project/sqlite-database/
#shift + ctrl + p -> wpisujemy sqlite i wybieramy naszą bazę (test.db) - po lewej na dole pokazuje się dostęp do bazy
#tworzymy połączenie z bazą
import sqlite3

conn = sqlite3.connect('test1.db') #tworzymy połączenie z bazą danych przechowywaną w pliku na dysku (test.db, nazwa pliku jest dowolona) lub w pamięci, jeśli podamy ':memory:'
conn.row_factory = sqlite3.Row #dostęp do kolumn (pól tabel) nie tylko przez indeksy, ale również przez nazwy
db = conn.cursor() #obiekt kursora do wykonywania operacji na bazie danych
print(db)
#tworzymy tabele
#pojedyncze polecenia SQL-a wykonujemy za pomocą metody .execute()
db.execute("DROP TABLE IF EXISTS klasa;")

db.execute("""
    CREATE TABLE IF NOT EXISTS klasa (
        id INTEGER PRIMARY KEY ASC,
        nazwa varchar(250) NOT NULL,
        profil varchar(250) DEFAULT ''
    )""")

#wiele instrukcji wykonujemy za pomocą metody .executescript()
db.executescript("""
    DROP TABLE IF EXISTS uczen;
    CREATE TABLE IF NOT EXISTS uczen (
        id INTEGER PRIMARY KEY ASC,
        imie varchar(250) NOT NULL,
        nazwisko varchar(250) NOT NULL,
        klasa_id INTEGER NOT NULL,
        FOREIGN KEY(klasa_id) REFERENCES klasa(id)
    )""")
#po uruchomieniu kodu baza pojawi się w katalogu

# wstawiamy jeden rekord danych
db.execute('INSERT INTO klasa VALUES(NULL, ?, ?);', ('1A', 'matematyczny'))
db.execute('INSERT INTO klasa VALUES(NULL, ?, ?);', ('1B', 'humanistyczny'))

# INSERT, UPDATE i DELETE zmieniają dane dopiero po conn.commit().
# Bez commit() zmiany przepadną po zamknięciu połączenia.
# Nie zakładaj, że CREATE TABLE zatwierdzi się samo - dobrą praktyką jest zawsze wywołać conn.commit().
conn.commit()

# Wyświetlamy zawartość tabeli klasa
print("Zawartość tabeli 'klasa':")
for row in db.execute('SELECT * FROM klasa'):
    print(dict(row))  # wypisujemy jako słownik, żeby było czytelniej


# wykonujemy zapytanie SQL, które pobierze id klasy "1A" z tabeli "klasa".
db.execute('SELECT id FROM klasa WHERE nazwa = ?', ('1A',))
klasa_id = db.fetchone()[0]

# tupla "uczniowie" zawiera tuple z danymi poszczególnych uczniów
uczniowie = (
    (None, 'Tomasz', 'Nowak', klasa_id),
    (None, 'Jan', 'Kos', klasa_id),
    (None, 'Piotr', 'Kowalski', klasa_id)
)

# wstawiamy wiele rekordów
db.executemany('INSERT INTO uczen VALUES(?,?,?,?)', uczniowie)



# pobieranie danych z bazy
def czytajdane():
    """Funkcja pobiera i wyświetla dane z bazy."""
    #poniżej potrójny cudzysłów został wykorzystany do zapisania długielo zapytania SQL
    db.execute(
        """
        SELECT uczen.id,imie,nazwisko,nazwa FROM uczen,klasa
        WHERE uczen.klasa_id=klasa.id
        """)
    
    uczniowie = db.fetchall()
    for uczen in uczniowie:
        print(uczen['id'], uczen['imie'], uczen['nazwisko'], uczen['nazwa'])
    print()    

czytajdane()

# zmiana klasy ucznia o identyfikatorze 2
db.execute('SELECT id FROM klasa WHERE nazwa = ?', ('1B',))
klasa_id = db.fetchone()[0]
db.execute('UPDATE uczen SET klasa_id=? WHERE id=?', (klasa_id, 2))

# usunięcie ucznia o identyfikatorze 3
# db.execute('DELETE FROM uczen WHERE id=?', (3,))

czytajdane()

# zatwierdzamy zmiany w bazie
conn.commit()
# Zamykamy połączenie
conn.close()

ĆWICZENIA

Ćwiczenie 1: Biblioteka książek (Podstawowe)

Utwórz bazę danych biblioteki z tabelą ksiażki zawierającą:

id (PRIMARY KEY)

tytul (TEXT)

autor (TEXT)

rok_wydania (INTEGER)

dostepna (INTEGER – 0 lub 1)

 

Zadania:

Dodaj 5 książek do tabeli

Wyświetl wszystkie książki wydane po 2000 roku

Oznacz książkę jako wypożyczoną (dostepna = 0)

Policz ile jest dostępnych książek

 

Ćwiczenie 2: Sklep internetowy (Średniozaawansowane)

Stwórz system zarządzania produktami z bazą o nazwie sklep.db. Utwórz dwie tabele: kategorie i produkty.

Tabela kategorie: id, nazwa

Tabela produkty: id, nazwa, cena, kategoria_id, stan_magazynowy

Utwórz obie tabele z odpowiednimi kluczami obcymi

 

Zadania:

Dodaj 3 kategorie (Elektronika, Odzież, Książki)

Dodaj po 3 produkty do każdej kategorii

Napisz zapytanie wyświetlające produkty wraz z nazwą kategorii

Oblicz średnią cenę produktów w każdej kategorii

Znajdź najdroższy produkt w bazie

 

Ćwiczenie 3: System zarządzania studentami z relacjami wiele-do-wielu: (Zaawansowane)

Utwórz bazę o nazwie uczelnia.db z trzema tabelami: studenci, kursy, zapisy (tabela łącząca)

Utwórz wszystkie trzy tabele z odpowiednimi kluczami obcymi i ograniczeniem UNIQUE(student_id, kurs_id)

Zaimplementuj funkcje:

dodaj_studenta(imie, nazwisko, rok_studiow)

dodaj_kurs(nazwa, punkty_ects)

zapisz_na_kurs(student_id, kurs_id, ocena)

wyswietl_kursy_studenta(student_id)

oblicz_srednia_studenta(student_id)

Użyj transakcji do zapisania studenta na wiele kursów jednocześnie

NAJWAŻNIEJSZE ZASADY

1. Bezpieczeństwo – SQL Injection

#ŹLE - podatne na SQL Injection
nazwa = input("Podaj nazwę: ")
cursor.execute(f"SELECT * FROM users WHERE name = '{nazwa}'")
 
#DOBRZE - używaj parametrów
nazwa = input("Podaj nazwę: ")
cursor.execute("SELECT * FROM users WHERE name = ?", (nazwa,))

2. Zawsze używaj commit()

cursor.execute('INSERT INTO ...')
conn.commit()  # BEZ TEGO dane nie zostaną zapisane!

3. Zamykaj połączenia

conn = sqlite3.connect('db.db') 
... operacje ... 
conn.close()

Uwaga: blok `with sqlite3.connect(…) as conn:` **nie zamyka** połączenia. Zatwierdza zmiany (`commit`), a gdy w bloku wystąpi wyjątek, cofa je (`rollback`). Połączenie zamykasz samodzielnie przez `conn.close()`:

with sqlite3.connect('db.db') as conn: 
... operacje ... # commit automatycznie, rollback przy błędzie
conn.close()

PRZYDATNE METODY

execute(sql, params)Wykonuje pojedyncze polecenie SQL z opcjonalnymi parametrami
executemany(sql, seq_of_parameters)Wstawia wiele rekordów
executescript(sql)Wykonuje wiele poleceń SQL
fetchone()Pobiera jeden wiersz
fetchall()Pobiera wszystkie wiersze
fetchmany(n)Pobiera n wierszy
lastrowidID ostatnio wstawionego rekordu
rowcountLiczba zmienionych wierszy

Dlaczego dwa kroki? (Connection + Cursor)

Dlaczego nie może być tak:

conn = sqlite3.connect('baza.db')
conn.execute('SELECT...')  # Czy to nie wystarczy?

Może być! Ale to jest skrót:

#Sposób 1: SKRÓT (Python robi to za Ciebie)
conn.execute('SELECT * FROM users')
 
# Sposób 2: PEŁNY (tak naprawdę działa)
cursor = conn.cursor()
cursor.execute('SELECT * FROM users')
 
# Python w skrócie automatycznie tworzy kursor, ale go nie widzisz!

Możesz mieć WIELU BIBLIOTEKARZY naraz (wiele kursorów) i każdy może wykonywać różne zadania, ale wszyscy korzystają z tej samej karty bibliotecznej (połączenia). To pozwala na równoczesną pracę z bazą danych.

conn = sqlite3.connect('baza.db')
 
# Pierwszy bibliotekarz zajmuje się użytkownikami
cursor1 = conn.cursor()
cursor1.execute('SELECT * FROM users')
 
# Drugi bibliotekarz zajmuje się zamówieniami
cursor2 = conn.cursor()
cursor2.execute('SELECT * FROM orders')
 
# Każdy robi swoją robotę niezależnie!
users = cursor1.fetchall()
orders = cursor2.fetchall()
 
# Kursor "pamięta" wyniki swoich zapytań, więc możesz mieć wielu kursorów, a każdy z nich będzie miał swoje własne wyniki.
cursor = conn.cursor()
cursor.execute('SELECT * FROM users')
 
# Pobierasz wyniki STOPNIOWO
pierwszy = cursor.fetchone()   # Pobiera 1 wiersz
drugi = cursor.fetchone()      # Pobiera kolejny wiersz
reszta = cursor.fetchall()     # Pobiera wszystkie pozostałe
 
# Gdybyś użył conn.execute(), nie miałbyś takiej kontroli nad kursorami i wynikami.

Pamiętaj

  • pojedyncze polecenia SQL-a wykonujemy za pomocą metody .execute() np. DROP TABLE IF EXISTS uczen;
  • wiele instrukcji wykonujemy za pomocą metody .executescript() np. DROP TABLE IF EXISTS uczen; CREATE TABLE IF NOT EXISTS uczen (...);
  • dodanie wielu rekordów do tabeli wykonujemy za pomocą metody .executemany()
  • Polecenia INSERT, UPDATE, DELETE zmieniają dane w bazie dopiero po jawnym wywołaniu conn.commit(). Bez tego zmiany znikną po zamknięciu połączenia. Polecenie SELECT tylko odczytuje dane i nie wymaga commit().

ROZWIĄZANIA

Rozwiązanie ćwiczenia 1: Biblioteka książek

import sqlite3
 
with sqlite3.connect('biblioteka.db') as conn:
    c = conn.cursor()
   
    # 1. Tworzenie tabeli
    c.execute('''
    CREATE TABLE IF NOT EXISTS ksiazki (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        tytul TEXT NOT NULL,
        autor TEXT NOT NULL,
        rok_wydania INTEGER,
        dostepna INTEGER DEFAULT 1
    )
    ''')
   
    # 2. Dodawanie książek
    ksiazki = [
        ('Władca Pierścieni', 'J.R.R. Tolkien', 1954, 1),
        ('Harry Potter', 'J.K. Rowling', 1997, 1),
        ('Wiedźmin', 'Andrzej Sapkowski', 1990, 1),
        ('Metro 2033', 'Dmitry Glukhovsky', 2005, 1),
        ('Kod Da Vinci', 'Dan Brown', 2003, 1)
    ]
    c.executemany('INSERT INTO ksiazki (tytul, autor, rok_wydania, dostepna) VALUES (?, ?, ?, ?)', ksiazki)
   
    # 3. Książki po 2000 roku
    print("Książki wydane po 2000 roku:")
    c.execute('SELECT tytul, autor, rok_wydania FROM ksiazki WHERE rok_wydania > 2000')
    for row in c.fetchall():
        print(f"- {row[0]} ({row[1]}, {row[2]})")
   
    # 4. Wypożyczenie książki
    c.execute('UPDATE ksiazki SET dostepna = 0 WHERE tytul = ?', ('Metro 2033',))
   
    # 5. Liczba dostępnych książek
    c.execute('SELECT COUNT(*) FROM ksiazki WHERE dostepna = 1')
    liczba = c.fetchone()[0]
    print(f"\nDostępnych książek: {liczba}")

Rozwiązanie ćwiczenia 2: Sklep internetowy

import sqlite3
 
with sqlite3.connect('sklep.db') as conn:
    c = conn.cursor()
   
    # 1. Tworzenie tabel
    c.execute('''
    CREATE TABLE IF NOT EXISTS kategorie (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        nazwa TEXT NOT NULL UNIQUE
    )
    ''')
   
    c.execute('''
    CREATE TABLE IF NOT EXISTS produkty (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        nazwa TEXT NOT NULL,
        cena REAL NOT NULL,
        kategoria_id INTEGER,
        stan_magazynowy INTEGER DEFAULT 0,
        FOREIGN KEY (kategoria_id) REFERENCES kategorie(id)
    )
    ''')
   
    # 2. Dodawanie kategorii
    kategorie = [('Elektronika',), ('Odzież',), ('Książki',)]
    c.executemany('INSERT OR IGNORE INTO kategorie (nazwa) VALUES (?)', kategorie)
   
    # 3. Dodawanie produktów
    produkty = [
        ('Laptop', 3500.00, 1, 10),
        ('Smartphone', 2200.00, 1, 15),
        ('Słuchawki', 450.00, 1, 25),
        ('Koszulka', 89.99, 2, 50),
        ('Spodnie', 199.99, 2, 30),
        ('Kurtka', 499.00, 2, 20),
        ('Python dla każdego', 59.99, 3, 100),
        ('Clean Code', 89.00, 3, 75),
        ('Algorytmy', 120.00, 3, 40)
    ]
    c.executemany('INSERT INTO produkty (nazwa, cena, kategoria_id, stan_magazynowy) VALUES (?, ?, ?, ?)', produkty)
   
    # 4. Produkty z kategoriami
    print("Produkty z kategoriami:")
    c.execute('''
    SELECT p.nazwa, p.cena, k.nazwa
    FROM produkty p
    JOIN kategorie k ON p.kategoria_id = k.id
    ORDER BY k.nazwa
    ''')
    for row in c.fetchall():
        print(f"{row[0]} ({row[2]}) - {row[1]} zł")
   
    # 5. Średnia cena w kategoriach
    print("\nŚrednia cena w kategoriach:")
    c.execute('''
    SELECT k.nazwa, ROUND(AVG(p.cena), 2) as srednia
    FROM kategorie k
    JOIN produkty p ON k.id = p.kategoria_id
    GROUP BY k.nazwa
    ''')
    for row in c.fetchall():
        print(f"{row[0]}: {row[1]} zł")
   
    # 6. Najdroższy produkt
    c.execute('SELECT nazwa, cena FROM produkty ORDER BY cena DESC LIMIT 1')
    najdrozszy = c.fetchone()
    print(f"\nNajdroższy produkt: {najdrozszy[0]} ({najdrozszy[1]} zł)")

Rozwiązanie ćwiczenia 3: System zarządzania studentami

import sqlite3

class SystemStudentow:
    def __init__(self, db_name='uczelnia.db'):
        # Polaczenie z baza danych
        self.conn = sqlite3.connect(db_name)
        # Ustawienie row_factory dla dostepu do kolumn jak do slownika
        self.conn.row_factory = sqlite3.Row
        self._utworz_tabele()
    
    def _utworz_tabele(self):
        # Utworzenie kursora
        cursor = self.conn.cursor()
        
        # Tworzenie tabeli studenci
        cursor.execute('''
        CREATE TABLE IF NOT EXISTS studenci (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            imie TEXT NOT NULL,
            nazwisko TEXT NOT NULL,
            rok_studiow INTEGER
        )
        ''')
        
        # Tworzenie tabeli kursy
        cursor.execute('''
        CREATE TABLE IF NOT EXISTS kursy (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            nazwa TEXT NOT NULL,
            punkty_ects INTEGER
        )
        ''')
        
        # Tworzenie tabeli zapisy (relacja wiele-do-wielu)
        cursor.execute('''
        CREATE TABLE IF NOT EXISTS zapisy (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            student_id INTEGER,
            kurs_id INTEGER,
            ocena REAL,
            FOREIGN KEY (student_id) REFERENCES studenci(id),
            FOREIGN KEY (kurs_id) REFERENCES kursy(id),
            UNIQUE(student_id, kurs_id)
        )
        ''')
        
        # Zatwierdzenie zmian
        self.conn.commit()
    
    def dodaj_studenta(self, imie, nazwisko, rok_studiow):
        cursor = self.conn.cursor()
        # Wstawienie studenta
        cursor.execute(
            'INSERT INTO studenci (imie, nazwisko, rok_studiow) VALUES (?, ?, ?)',
            (imie, nazwisko, rok_studiow)
        )
        self.conn.commit()
        # Zwrocenie ID dodanego rekordu
        return cursor.lastrowid
    
    def dodaj_kurs(self, nazwa, punkty_ects):
        cursor = self.conn.cursor()
        # Wstawienie kursu
        cursor.execute(
            'INSERT INTO kursy (nazwa, punkty_ects) VALUES (?, ?)',
            (nazwa, punkty_ects)
        )
        self.conn.commit()
        return cursor.lastrowid
    
    def zapisz_na_kurs(self, student_id, kurs_id, ocena=None):
        cursor = self.conn.cursor()
        try:
            # Wstawienie zapisu studenta na kurs
            cursor.execute(
                'INSERT INTO zapisy (student_id, kurs_id, ocena) VALUES (?, ?, ?)',
                (student_id, kurs_id, ocena)
            )
            self.conn.commit()
            return True
        except sqlite3.IntegrityError:
            print("Student jest juz zapisany na ten kurs")
            return False
    
    def ustaw_ocene(self, student_id, kurs_id, ocena):
        cursor = self.conn.cursor()
        # Aktualizacja oceny
        cursor.execute('''
            UPDATE zapisy 
            SET ocena = ? 
            WHERE student_id = ? AND kurs_id = ?
        ''', (ocena, student_id, kurs_id))
        
        # Sprawdzenie czy rekord zostal zaktualizowany
        if cursor.rowcount == 0:
            print("Student nie jest zapisany na ten kurs")
            return False
        
        self.conn.commit()
        return True
    
    def wyswietl_kursy_studenta(self, student_id):
        cursor = self.conn.cursor()
        # Zapytanie JOIN laczace tabele zapisy i kursy
        cursor.execute('''
        SELECT k.nazwa, k.punkty_ects, z.ocena
        FROM zapisy z
        JOIN kursy k ON z.kurs_id = k.id
        WHERE z.student_id = ?
        ''', (student_id,))
        
        print(f"\nKursy studenta (ID: {student_id}):")
        # Odczyt wszystkich wierszy
        for row in cursor.fetchall():
            ocena_str = f"{row['ocena']}" if row['ocena'] else "brak"
            print(f"- {row['nazwa']} ({row['punkty_ects']} ECTS) - Ocena: {ocena_str}")
    
    def oblicz_srednia_studenta(self, student_id):
        cursor = self.conn.cursor()
        # Zapytanie z funkcja agregujaca AVG
        cursor.execute('''
        SELECT AVG(ocena) as srednia
        FROM zapisy
        WHERE student_id = ? AND ocena IS NOT NULL
        ''', (student_id,))
        
        # Odczyt jednego wiersza
        wynik = cursor.fetchone()
        return round(wynik['srednia'], 2) if wynik['srednia'] else 0
    
    def zapisz_na_wiele_kursow(self, student_id, kursy_ids):
        try:
            cursor = self.conn.cursor()
            # Transakcja - wstawienie wielu rekordow
            for kurs_id in kursy_ids:
                cursor.execute(
                    'INSERT INTO zapisy (student_id, kurs_id) VALUES (?, ?)',
                    (student_id, kurs_id)
                )
            self.conn.commit()
            print(f"Student zapisany na {len(kursy_ids)} kursow")
        except sqlite3.Error as e:
            # Wycofanie zmian w przypadku bledu
            self.conn.rollback()
            print(f"Blad: {e}")
    
    def __del__(self):
        # Zamkniecie polaczenia
        self.conn.close()


if __name__ == '__main__':
    import os
    
    # Usuniecie starej bazy danych
    if os.path.exists('uczelnia.db'):
        os.remove('uczelnia.db')
    
    system = SystemStudentow()
    
    # Dodawanie studentow
    student1 = system.dodaj_studenta('Jan', 'Kowalski', 2)
    student2 = system.dodaj_studenta('Anna', 'Nowak', 1)
    student3 = system.dodaj_studenta('Piotr', 'Wisniewski', 3)
    
    # Dodawanie kursow
    kurs1 = system.dodaj_kurs('Bazy Danych', 6)
    kurs2 = system.dodaj_kurs('Algorytmy', 5)
    kurs3 = system.dodaj_kurs('Sieci Komputerowe', 4)
    
    # Zapisanie studenta 1 na wszystkie kursy
    system.zapisz_na_wiele_kursow(student1, [kurs1, kurs2, kurs3])
    
    # Ustawienie ocen dla studenta 1
    system.ustaw_ocene(student1, kurs1, 4.5)
    system.ustaw_ocene(student1, kurs2, 5.0)
    system.ustaw_ocene(student1, kurs3, 3.5)
    
    # Zapisanie studenta 2 na 2 kursy z ocenami
    system.zapisz_na_kurs(student2, kurs1, 4.0)
    system.zapisz_na_kurs(student2, kurs2, 5.0)
    
    # Zapisanie studenta 3 na 1 kurs bez oceny
    system.zapisz_na_kurs(student3, kurs1)
    
    # Wyswietlanie kursow studentow
    system.wyswietl_kursy_studenta(student1)
    srednia1 = system.oblicz_srednia_studenta(student1)
    print(f"\nSrednia ocen: {srednia1}")
    
    system.wyswietl_kursy_studenta(student2)
    srednia2 = system.oblicz_srednia_studenta(student2)
    print(f"\nSrednia ocen: {srednia2}")
    
    system.wyswietl_kursy_studenta(student3)
    srednia3 = system.oblicz_srednia_studenta(student3)
    print(f"\nSrednia ocen: {srednia3}")

PODSUMOWANIE

Jak działa praca z bazą w Pythonie – zawsze ta sama kolejność: połączenie → kursor → polecenie SQL → odbiór wyniku (fetch…) → commit() → close()

  • SQLite to relacyjna baza w jednym pliku. Moduł sqlite3 jest wbudowany w Pythona, nic nie instalujesz. ':memory:' tworzy bazę tylko w pamięci.
  • Connection to połączenie z plikiem bazy. Cursor wykonuje polecenia SQL i przechowuje ich wyniki. Na jednym połączeniu możesz mieć wiele kursorów. conn.execute(...) to skrót, który sam tworzy kursor.
  • Dane do zapytania przekazujesz przez ? i krotkę parametrów. Nigdy nie wstawiaj ich do SQL-a przez f-string (SQL Injection).
  • Krotka z jednym elementem wymaga przecinka: (nazwa,). Zapis (nazwa) to zwykły napis i da błąd.
  • INSERT, UPDATE, DELETE zmieniają dane tylko w trakcie transakcji. Bez conn.commit() zmiany przepadną po zamknięciu połączenia. rollback() cofa zmiany niezatwierdzone.
  • with sqlite3.connect(...) as conn: zatwierdza zmiany (commit) po poprawnym wyjściu z bloku i cofa je (rollback), jeśli w bloku wystąpił wyjątek. Samo with nie zamyka połączenia – zamykasz je przez conn.close().
  • Wyniki zapytania to krotki, nawet gdy pytasz o jedną kolumnę: [('Ola',), ('Ewa',)]. fetchone() zwraca jeden wiersz albo None, gdy wierszy już nie ma. fetchall() zwraca listę wszystkich pozostałych wierszy.
  • conn.row_factory = sqlite3.Row pozwala czytać kolumny po nazwie: row['imie'], dict(row).
  • Metody: execute (jedno polecenie), executemany (jedno polecenie dla wielu zestawów danych), executescript (wiele poleceń SQL naraz).
  • lastrowid to id ostatnio wstawionego rekordu. rowcount to liczba zmienionych wierszy (0 = nic nie zmieniono).
  • Relacje między tabelami: FOREIGN KEY ... REFERENCES, łączenie tabel przez JOIN ... ON. LEFT JOIN zachowuje też wiersze bez dopasowania (np. dział bez pracowników). GROUP BY z COUNT, AVG grupuje wyniki.
  • Błędy bazy to wyjątki z sqlite3. sqlite3.Error jest bazowy dla wszystkich. Konkretne wyjątki (IntegrityError, OperationalError) wpisuj w except przed ogólnym sqlite3.Error.
  • Typowe pułapki: brak commit(), ("Ola") zamiast ("Ola",), f-string w SQL, UPDATE/DELETE bez WHERE (zmienia lub usuwa wszystkie wiersze), wielokrotne uruchamianie programu dubluje rekordy (dlatego przykład 5 zaczyna się od DROP TABLE IF EXISTS).

Minimum, które powinno wejść w pamięć

Jeżeli masz zapamiętać tylko kilka rzeczy z tej lekcji, zapamiętaj te:

  1. Kolejność pracy: connect() → cursor() → execute() → fetch… → commit() → close().
  2. Dane do SQL-a zawsze przez ? i krotkę: execute("... WHERE x = ?", (x,)). Nigdy przez f-string.
  3. Jeden parametr w krotce wymaga przecinka: (x,).
  4. Po INSERT, UPDATE, DELETE wywołaj commit(), inaczej zmiany przepadną.
  5. with sqlite3.connect(...) as conn zatwierdza lub cofa zmiany, ale nie zamyka połączenia.
  6. Wynik zapytania to krotki: fetchone() zwraca jeden wiersz lub None, fetchall() listę krotek.
  7. UPDATE i DELETE bez WHERE działają na całej tabeli.
  8. Wyjątki: najpierw konkretne (IntegrityError, OperationalError), na końcu ogólny sqlite3.Error.

Ściągawka – najważniejsze polecenia i składnia

Schemat programu

import sqlite3

conn = sqlite3.connect('baza.db')   # ':memory:' = baza w pamięci
conn.row_factory = sqlite3.Row      # opcjonalnie: row['kolumna']
c = conn.cursor()

c.execute('POLECENIE SQL', (param1, param2))
conn.commit()                       # po INSERT / UPDATE / DELETE
conn.close()

Metody kursora i połączenia

Co chcesz zrobićJak
jedno poleceniec.execute(sql, (a, b))
wiele rekordów narazc.executemany(sql, lista_krotek)
kilka poleceń SQL narazc.executescript(sql)
jeden wiersz / wszystkie / n wierszyc.fetchone() / c.fetchall() / c.fetchmany(n)
id wstawionego rekorduc.lastrowid
ile wierszy zmienionoc.rowcount
zatwierdzić / cofnąćconn.commit() / conn.rollback()

Najczęściej używany SQL

OperacjaSkładnia
tabelaCREATE TABLE IF NOT EXISTS t (id INTEGER PRIMARY KEY AUTOINCREMENT, nazwa TEXT NOT NULL, cena REAL, kat_id INTEGER, FOREIGN KEY (kat_id) REFERENCES kategorie(id))
usunięcie tabeliDROP TABLE IF EXISTS t
wstawianieINSERT INTO t (nazwa, cena) VALUES (?, ?)
odczytSELECT a, b FROM t WHERE a > ? ORDER BY b DESC LIMIT 1
zmianaUPDATE t SET cena = ? WHERE id = ?
usuwanieDELETE FROM t WHERE id = ?
łączenie tabelSELECT ... FROM a JOIN b ON a.b_id = b.id
grupowanieSELECT k.nazwa, COUNT(p.id) FROM k LEFT JOIN p ON k.id = p.k_id GROUP BY k.nazwa
agregatyCOUNT(*), AVG(kolumna), ROUND(wartosc, 2)

Ograniczenia kolumn: PRIMARY KEY, NOT NULL, UNIQUE, DEFAULT wartość.

Obsługa błędów

try:
    ...
except sqlite3.IntegrityError as e:    # np. naruszenie UNIQUE
    ...
except sqlite3.OperationalError as e:  # np. brak tabeli
    ...
except sqlite3.Error as e:             # każdy inny błąd bazy
    ...

Szybka diagnoza

ObjawNajczęstsza przyczyna
dane znikają po zakończeniu programubrak conn.commit()
ProgrammingError: Incorrect number of bindings supplied("Ola") zamiast ("Ola",) albo zła liczba ?
OperationalError: no such tabletabela nie została utworzona albo literówka w nazwie
IntegrityErrorduplikat w kolumnie UNIQUE lub brak wartości w kolumnie NOT NULL

Quiz – sprawdź się

Do pytań 4–6 i 11 przyjmij, że wykonano już ten kod:

import sqlite3
conn = sqlite3.connect(':memory:')
c = conn.cursor()
c.execute("CREATE TABLE o (id INTEGER PRIMARY KEY, nazwa TEXT, ocena INTEGER)")
c.executemany("INSERT INTO o (nazwa, ocena) VALUES (?, ?)",
              [("Ola", 5), ("Jan", 3), ("Ewa", 4)])

Pytanie 1. Dopasuj metodę do opisu.

  1. connect()
  2. cursor()
  3. commit()
  4. rollback()

A. cofa niezatwierdzone zmiany B. zatwierdza zmiany w bazie C. otwiera połączenie z plikiem bazy D. tworzy obiekt, który wykonuje polecenia SQL i przechowuje wyniki

Pytanie 2. Co dzieje się z danymi i z połączeniem po wyjściu z bloku with? Czy trzeba jeszcze wywołać conn.close()?

with sqlite3.connect('sklep.db') as conn:
    conn.execute("INSERT INTO produkty (nazwa, cena) VALUES ('Mysz', 59.9)")

Pytanie 3. Która wersja jest poprawna i dlaczego? Co zwróci wersja A, jeśli użytkownik wpisze ' OR '1'='1?

# wersja A
nazwa = input("Podaj nazwę: ")
cursor.execute(f"SELECT * FROM users WHERE name = '{nazwa}'")

# wersja B
nazwa = input("Podaj nazwę: ")
cursor.execute("SELECT * FROM users WHERE name = ?", (nazwa,))

Pytanie 4. Co zostanie wyświetlone?

c.execute("SELECT nazwa FROM o WHERE ocena >= 4")
print(c.fetchall())

Pytanie 5. Co zostanie wyświetlone w dwóch kolejnych liniach?

c.execute("SELECT COUNT(*) FROM o")
wynik = c.fetchone()
print(wynik)
print(wynik[0])

Pytanie 6. Znajdź błąd. Dlaczego ten kod zgłasza ProgrammingError i jak go poprawić?

c.execute("SELECT * FROM o WHERE nazwa = ?", ("Ola"))

Pytanie 7. Znajdź błąd. Program uruchomiono, ale po ponownym otwarciu pliku szkola.db nie ma w nim rekordu „Kasia”. Dlaczego? (Tabela o istniała już w pliku.)

conn = sqlite3.connect('szkola.db')
c = conn.cursor()
c.execute("INSERT INTO o (nazwa, ocena) VALUES (?, ?)", ("Kasia", 5))
conn.close()

Pytanie 8. Uzupełnij kod, aby wstawić dwa rekordy jednym wywołaniem i zapisać je w bazie.

dane = [("Kasia", 5), ("Adam", 2)]
c.________("INSERT INTO o (nazwa, ocena) VALUES (?, ?)", dane)
conn.________()

Pytanie 9. Co zostanie wyświetlone po wykonaniu tego kodu? Dlaczego?

c.execute("CREATE TABLE u (id INTEGER PRIMARY KEY, email TEXT UNIQUE NOT NULL)")
try:
    c.execute("INSERT INTO u (email) VALUES (?)", ("a@x.pl",))
    c.execute("INSERT INTO u (email) VALUES (?)", ("a@x.pl",))
except sqlite3.IntegrityError:
    print("duplikat")
except sqlite3.Error:
    print("inny błąd")

Pytanie 10. Uzupełnij zapytanie tak, aby zwracało imiona uczniów i nazwy ich klas, ale tylko dla klasy 1A. Tabela uczen ma kolumnę klasa_id, a tabela klasa ma kolumny id i nazwa.

c.execute("""
    SELECT u.imie, k.nazwa
    FROM uczen u
    JOIN klasa k ON u.klasa_id = k.________
    WHERE k.nazwa = ?
""", ('1A',))

Pytanie 11. Zadanie praktyczne. Napisz kod, który dla tabeli o wyświetli nazwy i oceny uczniów z oceną niższą niż wartość zmiennej prog (prog = 4), każdego w osobnej linii w formacie Jan: 3. Użyj zapytania z parametrem.

Odpowiedzi

1. 1 – C, 2 – D, 3 – B, 4 – A.

2. Dane zostaną zatwierdzone (commit). Gdyby w bloku wystąpił wyjątek, zmiany zostałyby cofnięte (rollback). Blok with nie zamyka połączenia, więc conn.close() nadal jest potrzebne.

3. Poprawna jest wersja B. Parametr ? przekazuje dane jako wartość, a nie jako część polecenia SQL. W wersji A zapytanie przyjmie postać WHERE name = '' OR '1'='1', czyli warunek jest zawsze prawdziwy i zwrócone zostaną wszystkie wiersze tabeli (SQL Injection).

4. [('Ola',), ('Ewa',)] – wynik to lista krotek, nawet gdy zapytanie dotyczy jednej kolumny.

5. (3,) oraz 3. fetchone() zwraca krotkę, więc liczbę wyciągasz przez wynik[0].

6. ("Ola") to zwykły napis, nie krotka. Python traktuje go jak trzy znaki (O, l, a) i zgłasza błąd liczby parametrów. Poprawnie: ("Ola",).

7. Brak conn.commit(). INSERT nie został zatwierdzony, więc po close() zmiana przepadła. Przed conn.close() trzeba dodać conn.commit().

8. executemany, commit

9. Wyświetli się duplikat. Drugi INSERT łamie ograniczenie UNIQUE, co zgłasza sqlite3.IntegrityError, a ten wyjątek jest obsłużony w pierwszym except. Konkretny wyjątek musi stać przed ogólnym sqlite3.Error.

10. id, czyli JOIN klasa k ON u.klasa_id = k.id. Zapytanie zwróci imiona uczniów z klasy 1A razem z nazwą klasy.

11. Przykładowe rozwiązanie:

prog = 4
c.execute("SELECT nazwa, ocena FROM o WHERE ocena < ?", (prog,))
for nazwa, ocena in c.fetchall():
    print(f"{nazwa}: {ocena}")

Wynik dla przykładowych danych: Jan: 3.