Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

SQL - relacyjne magazyny danych

Witaj ponownie, @name! Darwin tutaj z kluczową lekcją o przechowywaniu danych.

Do tej pory pracowałeś/aś z danymi tymczasowo - w zmiennych, plikach JSON, scrapowanych stronach. Ale co gdy masz miliony obserwacji gatunków? Co gdy potrzebujesz szybko wyszukać wszystkie zagrożone gatunki z Serengeti?

Wtedy potrzebujesz bazy danych - wyspecjalizowanego systemu do przechowywania i zarządzania dużymi zbiorami danych! 🗄️💾

Analogia Safari: Pliki JSON to jak notatnik biologa - działa dla 10 obserwacji. Baza danych to wielka biblioteka katalogowa - zorganizowane szuflady z kartotekami, indeksami, szybkim wyszukiwaniem dla milionów rekordów! 📚🔍

Czym są bazy danych?

Baza danych to uporządkowany zbiór danych + system zarządzania (DBMS - Database Management System).

Po co bazy danych?

  • Szybkie wyszukiwanie - indeksy, optymalizacja zapytań
  • Bezpieczeństwo - kontrola dostępu, uprawnienia
  • Integralność - walidacja, relacje, klucze obce
  • Skalowalność - miliony rekordów bez problemu
  • Wielodostęp - wielu użytkowników jednocześnie
  • Transakcje - atomowe operacje (albo wszystko, albo nic)

Relacyjne vs NoSQL

Relacyjne bazy danych (SQL)

Struktura: Tabele z wierszami i kolumnami (jak arkusz Excel)

Przykłady: PostgreSQL, MySQL, SQLite, Oracle, SQL Server

Cechy:

  • Sztywny schemat (kolumny zdefiniowane z góry)
  • Relacje między tabelami (klucze obce)
  • ACID (Atomicity, Consistency, Isolation, Durability)
  • Język SQL (Structured Query Language)
1TABELA: species
2+----+-----------------+------------+----------+
3| id | scientific_name | population | habitat  |
4+----+-----------------+------------+----------+
5| 1  | Panthera leo    | 120        | sawanna  |
6| 2  | Gorilla gorilla | 230        | las      |
7+----+-----------------+------------+----------+

NoSQL bazy danych

Struktura: Dokumenty, pary klucz-wartość, grafy, kolumny

Przykłady: MongoDB, Redis, Cassandra, Neo4j

Cechy:

  • Elastyczny schemat (JSON-like)
  • Skalowanie horyzontalne
  • Szybkie dla specyficznych zastosowań

W tej lekcji: Skupiamy się na SQL (relacyjne) - fundament większości aplikacji!

SQLite - baza danych w jednym pliku

SQLite to najpopularniejsza baza SQL na świecie:

  • Brak serwera - cała baza w jednym pliku .db
  • Zero konfiguracji - gotowe od razu
  • Wbudowane w Python - moduł
    sqlite3
  • Idealne na start - prototypy, małe/średnie aplikacje

Użycie: Aplikacje mobilne, przeglądarki, urządzenia IoT, prototypy

SQL - język zapytań

SQL (Structured Query Language) to uniwersalny język do komunikacji z bazami relacyjnymi.

Podstawowe operacje (CRUD)

  • CREATE - tworzenie tabel
  • INSERT - dodawanie rekordów
  • SELECT - pobieranie danych (Read)
  • UPDATE - aktualizacja rekordów
  • DELETE - usuwanie rekordów

CREATE TABLE - tworzenie tabeli

1-- Tabela gatunków
2CREATE TABLE species (
3    id INTEGER PRIMARY KEY AUTOINCREMENT,
4    scientific_name TEXT NOT NULL UNIQUE,
5    common_name TEXT NOT NULL,
6    population INTEGER DEFAULT 0,
7    habitat TEXT,
8    endangered BOOLEAN DEFAULT 0,
9    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
10);
11
12-- Tabela obserwacji
13CREATE TABLE observations (
14    id INTEGER PRIMARY KEY AUTOINCREMENT,
15    species_id INTEGER NOT NULL,
16    observation_date DATE NOT NULL,
17    location TEXT NOT NULL,
18    count INTEGER DEFAULT 0,
19    notes TEXT,
20    FOREIGN KEY (species_id) REFERENCES species(id)
21);

Typy danych SQLite:

  • INTEGER
    - liczba całkowita
  • REAL
    - liczba zmiennoprzecinkowa
  • TEXT
    - tekst
  • BLOB
    - dane binarne
  • NULL
    - brak wartości

Ograniczenia (constraints):

  • PRIMARY KEY
    - unikalny identyfikator wiersza
  • AUTOINCREMENT
    - automatyczny wzrost
  • NOT NULL
    - pole wymagane
  • UNIQUE
    - wartość unikalna
  • DEFAULT
    - wartość domyślna
  • FOREIGN KEY
    - relacja do innej tabeli

INSERT - dodawanie danych

1-- Dodaj jeden rekord
2INSERT INTO species (scientific_name, common_name, population, habitat, endangered)
3VALUES ('Panthera leo', 'Lew', 120, 'sawanna', 1);
4
5-- Dodaj wiele rekordów
6INSERT INTO species (scientific_name, common_name, population, habitat, endangered)
7VALUES
8    ('Loxodonta africana', 'Słoń afrykański', 450, 'sawanna', 1),
9    ('Gorilla gorilla', 'Goryl', 230, 'las tropikalny', 1),
10    ('Python regius', 'Pyton królewski', 85, 'dżungla', 0);
11
12-- Dodaj obserwację
13INSERT INTO observations (species_id, observation_date, location, count, notes)
14VALUES (1, '2024-01-15', 'Serengeti North', 12, 'Pride with 2 cubs');

SELECT - pobieranie danych

1-- Wszystkie gatunki
2SELECT * FROM species;
3
4-- Wybrane kolumny
5SELECT common_name, population FROM species;
6
7-- WHERE - filtrowanie
8SELECT * FROM species WHERE endangered = 1;
9SELECT * FROM species WHERE population > 100;
10SELECT * FROM species WHERE habitat = 'sawanna' AND endangered = 1;
11
12-- ORDER BY - sortowanie
13SELECT * FROM species ORDER BY population DESC;  -- Malejąco
14SELECT * FROM species ORDER BY common_name ASC;   -- Rosnąco
15
16-- LIMIT - ograniczenie wyników
17SELECT * FROM species LIMIT 10;
18SELECT * FROM species LIMIT 10 OFFSET 20;  -- Strona 3 (20-30)
19
20-- LIKE - wyszukiwanie wzorca
21SELECT * FROM species WHERE scientific_name LIKE 'Panthera%';  -- Zaczyna się od Panthera
22SELECT * FROM species WHERE habitat LIKE '%las%';  -- Zawiera "las"
23
24-- COUNT, SUM, AVG, MIN, MAX - agregacje
25SELECT COUNT(*) FROM species;
26SELECT AVG(population) FROM species;
27SELECT SUM(population) FROM species WHERE endangered = 1;
28SELECT MIN(population), MAX(population) FROM species;
29
30-- GROUP BY - grupowanie
31SELECT habitat, COUNT(*) as species_count
32FROM species
33GROUP BY habitat;
34
35SELECT habitat, AVG(population) as avg_population
36FROM species
37GROUP BY habitat
38HAVING AVG(population) > 100;  -- HAVING = WHERE dla grup

UPDATE - aktualizacja danych

1-- Zaktualizuj jeden rekord
2UPDATE species
3SET population = 125
4WHERE id = 1;
5
6-- Zaktualizuj wiele rekordów
7UPDATE species
8SET endangered = 1
9WHERE population < 50;
10
11-- Zaktualizuj wiele kolumn
12UPDATE species
13SET population = 130, habitat = 'sawanna i step'
14WHERE scientific_name = 'Panthera leo';

DELETE - usuwanie danych

1-- Usuń jeden rekord
2DELETE FROM species WHERE id = 10;
3
4-- Usuń wiele rekordów
5DELETE FROM species WHERE population = 0;
6
7-- UWAGA: Usuń wszystko (bez WHERE!)
8DELETE FROM species;  -- Usuwa WSZYSTKIE rekordy!

JOIN - łączenie tabel

1-- INNER JOIN - tylko dopasowane rekordy
2SELECT
3    species.common_name,
4    observations.observation_date,
5    observations.location,
6    observations.count
7FROM observations
8INNER JOIN species ON observations.species_id = species.id;
9
10-- LEFT JOIN - wszystkie z lewej + dopasowane z prawej
11SELECT
12    species.common_name,
13    COUNT(observations.id) as observation_count
14FROM species
15LEFT JOIN observations ON species.id = observations.species_id
16GROUP BY species.id, species.common_name;
17
18-- Aliasy tabel (s, o)
19SELECT
20    s.common_name,
21    o.location,
22    o.count
23FROM observations o
24INNER JOIN species s ON o.species_id = s.id
25WHERE s.endangered = 1;

Python + SQLite - moduł sqlite3

Python ma wbudowany moduł

sqlite3
do pracy z SQLite.

Podstawy

1import sqlite3
2
3# 1. Połącz się z bazą (tworzy plik jeśli nie istnieje)
4conn = sqlite3.connect("safari.db")
5
6# 2. Utwórz kursor (obiekt do wykonywania zapytań)
7cursor = conn.cursor()
8
9# 3. Wykonaj zapytanie SQL
10cursor.execute("""
11    CREATE TABLE IF NOT EXISTS species (
12        id INTEGER PRIMARY KEY AUTOINCREMENT,
13        scientific_name TEXT NOT NULL UNIQUE,
14        common_name TEXT NOT NULL,
15        population INTEGER DEFAULT 0,
16        endangered BOOLEAN DEFAULT 0
17    )
18""")
19
20# 4. Zatwierdź zmiany (WAŻNE!)
21conn.commit()
22
23# 5. Zamknij połączenie
24conn.close()

INSERT - dodawanie danych

1import sqlite3
2
3conn = sqlite3.connect("safari.db")
4cursor = conn.cursor()
5
6# Metoda 1: Zapytanie z wartościami (⚠️ NIEBEZPIECZNE - SQL injection!)
7# NIE RÓB TAK:
8# name = "Lew'; DROP TABLE species; --"  # SQL injection!
9# cursor.execute(f"INSERT INTO species (common_name) VALUES ('{name}')")
10
11# Metoda 2: Parameteryzowane zapytania (✅ BEZPIECZNE)
12cursor.execute("""
13    INSERT INTO species (scientific_name, common_name, population, endangered)
14    VALUES (?, ?, ?, ?)
15""", ("Panthera leo", "Lew", 120, 1))
16
17# Wiele rekordów naraz
18species_data = [
19    ("Loxodonta africana", "Słoń afrykański", 450, 1),
20    ("Gorilla gorilla", "Goryl", 230, 1),
21    ("Python regius", "Pyton królewski", 85, 0)
22]
23
24cursor.executemany("""
25    INSERT INTO species (scientific_name, common_name, population, endangered)
26    VALUES (?, ?, ?, ?)
27""", species_data)
28
29conn.commit()
30
31# Pobierz ID ostatnio dodanego rekordu
32print(f"Dodano gatunek ID: {cursor.lastrowid}")
33
34conn.close()

SELECT - pobieranie danych

1import sqlite3
2
3conn = sqlite3.connect("safari.db")
4cursor = conn.cursor()
5
6# fetchall() - wszystkie wyniki jako lista tupli
7cursor.execute("SELECT * FROM species")
8all_species = cursor.fetchall()
9
10for species in all_species:
11    print(species)  # (1, 'Panthera leo', 'Lew', 120, 1)
12
13# fetchone() - jeden wynik
14cursor.execute("SELECT * FROM species WHERE id = ?", (1,))
15lion = cursor.fetchone()
16print(lion)  # (1, 'Panthera leo', 'Lew', 120, 1)
17
18# fetchmany(n) - n wyników
19cursor.execute("SELECT * FROM species")
20first_five = cursor.fetchmany(5)
21
22# Row factory - wyniki jako słowniki
23conn.row_factory = sqlite3.Row
24cursor = conn.cursor()
25
26cursor.execute("SELECT * FROM species")
27for row in cursor.fetchall():
28    print(row["common_name"], row["population"])
29
30conn.close()

UPDATE i DELETE

1import sqlite3
2
3conn = sqlite3.connect("safari.db")
4cursor = conn.cursor()
5
6# UPDATE
7cursor.execute("""
8    UPDATE species
9    SET population = ?
10    WHERE id = ?
11""", (125, 1))
12
13print(f"Zaktualizowano {cursor.rowcount} rekordów")
14
15# DELETE
16cursor.execute("DELETE FROM species WHERE population < ?", (10,))
17print(f"Usunięto {cursor.rowcount} rekordów")
18
19conn.commit()
20conn.close()

Context manager - bezpieczne zarządzanie połączeniem

1import sqlite3
2
3# Automatyczne commit i close
4with sqlite3.connect("safari.db") as conn:
5    cursor = conn.cursor()
6
7    cursor.execute("""
8        INSERT INTO species (scientific_name, common_name, population)
9        VALUES (?, ?, ?)
10    """, ("Acinonyx jubatus", "Gepard", 7100))
11
12    # commit() automatycznie przy wyjściu z bloku with
13# close() automatycznie

Safari przykład - kompletny system bazy danych

1import sqlite3
2from typing import List, Dict, Optional
3from datetime import datetime
4
5class SafariDatabase:
6    """Kompletny system zarządzania danymi Safari"""
7
8    def __init__(self, db_path: str = "safari.db"):
9        self.db_path = db_path
10        self.init_database()
11
12    def get_connection(self):
13        """Utwórz połączenie z bazą"""
14        conn = sqlite3.connect(self.db_path)
15        conn.row_factory = sqlite3.Row  # Wyniki jako słowniki
16        return conn
17
18    def init_database(self):
19        """Inicjalizuj schemat bazy danych"""
20        with self.get_connection() as conn:
21            cursor = conn.cursor()
22
23            # Tabela gatunków
24            cursor.execute("""
25                CREATE TABLE IF NOT EXISTS species (
26                    id INTEGER PRIMARY KEY AUTOINCREMENT,
27                    scientific_name TEXT NOT NULL UNIQUE,
28                    common_name TEXT NOT NULL,
29                    population INTEGER DEFAULT 0,
30                    habitat TEXT,
31                    endangered BOOLEAN DEFAULT 0,
32                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
33                    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
34                )
35            """)
36
37            # Tabela obserwacji
38            cursor.execute("""
39                CREATE TABLE IF NOT EXISTS observations (
40                    id INTEGER PRIMARY KEY AUTOINCREMENT,
41                    species_id INTEGER NOT NULL,
42                    observation_date DATE NOT NULL,
43                    location TEXT NOT NULL,
44                    count INTEGER DEFAULT 0,
45                    notes TEXT,
46                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
47                    FOREIGN KEY (species_id) REFERENCES species(id) ON DELETE CASCADE
48                )
49            """)
50
51            # Indeksy dla szybszego wyszukiwania
52            cursor.execute("""
53                CREATE INDEX IF NOT EXISTS idx_species_endangered
54                ON species(endangered)
55            """)
56
57            cursor.execute("""
58                CREATE INDEX IF NOT EXISTS idx_observations_species
59                ON observations(species_id)
60            """)
61
62            conn.commit()
63
64    # === SPECIES CRUD ===
65
66    def create_species(self, scientific_name: str, common_name: str,
67                      population: int = 0, habitat: str = "",
68                      endangered: bool = False) -> int:
69        """Dodaj nowy gatunek"""
70        with self.get_connection() as conn:
71            cursor = conn.cursor()
72            cursor.execute("""
73                INSERT INTO species (scientific_name, common_name, population, habitat, endangered)
74                VALUES (?, ?, ?, ?, ?)
75            """, (scientific_name, common_name, population, habitat, int(endangered)))
76
77            conn.commit()
78            return cursor.lastrowid
79
80    def get_species(self, species_id: int) -> Optional[Dict]:
81        """Pobierz gatunek po ID"""
82        with self.get_connection() as conn:
83            cursor = conn.cursor()
84            cursor.execute("SELECT * FROM species WHERE id = ?", (species_id,))
85            row = cursor.fetchone()
86            return dict(row) if row else None
87
88    def list_species(self, endangered: Optional[bool] = None,
89                    habitat: Optional[str] = None,
90                    min_population: int = 0) -> List[Dict]:
91        """Lista gatunków z filtrami"""
92        with self.get_connection() as conn:
93            cursor = conn.cursor()
94
95            query = "SELECT * FROM species WHERE population >= ?"
96            params = [min_population]
97
98            if endangered is not None:
99                query += " AND endangered = ?"
100                params.append(int(endangered))
101
102            if habitat:
103                query += " AND habitat = ?"
104                params.append(habitat)
105
106            query += " ORDER BY common_name"
107
108            cursor.execute(query, params)
109            return [dict(row) for row in cursor.fetchall()]
110
111    def update_species(self, species_id: int, **kwargs) -> bool:
112        """Zaktualizuj gatunek"""
113        if not kwargs:
114            return False
115
116        # Dynamiczne budowanie UPDATE
117        fields = ", ".join([f"{key} = ?" for key in kwargs.keys()])
118        values = list(kwargs.values())
119        values.append(species_id)
120
121        with self.get_connection() as conn:
122            cursor = conn.cursor()
123            cursor.execute(f"""
124                UPDATE species
125                SET {fields}, updated_at = CURRENT_TIMESTAMP
126                WHERE id = ?
127            """, values)
128
129            conn.commit()
130            return cursor.rowcount > 0
131
132    def delete_species(self, species_id: int) -> bool:
133        """Usuń gatunek"""
134        with self.get_connection() as conn:
135            cursor = conn.cursor()
136            cursor.execute("DELETE FROM species WHERE id = ?", (species_id,))
137            conn.commit()
138            return cursor.rowcount > 0
139
140    # === OBSERVATIONS ===
141
142    def create_observation(self, species_id: int, observation_date: str,
143                          location: str, count: int, notes: str = "") -> int:
144        """Dodaj obserwację"""
145        with self.get_connection() as conn:
146            cursor = conn.cursor()
147            cursor.execute("""
148                INSERT INTO observations (species_id, observation_date, location, count, notes)
149                VALUES (?, ?, ?, ?, ?)
150            """, (species_id, observation_date, location, count, notes))
151
152            conn.commit()
153            return cursor.lastrowid
154
155    def get_observations_for_species(self, species_id: int) -> List[Dict]:
156        """Pobierz obserwacje dla gatunku"""
157        with self.get_connection() as conn:
158            cursor = conn.cursor()
159            cursor.execute("""
160                SELECT
161                    o.*,
162                    s.common_name as species_name
163                FROM observations o
164                JOIN species s ON o.species_id = s.id
165                WHERE o.species_id = ?
166                ORDER BY o.observation_date DESC
167            """, (species_id,))
168
169            return [dict(row) for row in cursor.fetchall()]
170
171    # === STATISTICS ===
172
173    def get_statistics(self) -> Dict:
174        """Pobierz statystyki bazy"""
175        with self.get_connection() as conn:
176            cursor = conn.cursor()
177
178            # Łączna liczba gatunków
179            cursor.execute("SELECT COUNT(*) as count FROM species")
180            total_species = cursor.fetchone()["count"]
181
182            # Zagrożone gatunki
183            cursor.execute("SELECT COUNT(*) as count FROM species WHERE endangered = 1")
184            endangered_count = cursor.fetchone()["count"]
185
186            # Łączna populacja
187            cursor.execute("SELECT SUM(population) as total FROM species")
188            total_population = cursor.fetchone()["total"] or 0
189
190            # Gatunki po siedlisku
191            cursor.execute("""
192                SELECT habitat, COUNT(*) as count
193                FROM species
194                GROUP BY habitat
195                ORDER BY count DESC
196            """)
197            by_habitat = [dict(row) for row in cursor.fetchall()]
198
199            return {
200                "total_species": total_species,
201                "endangered_count": endangered_count,
202                "total_population": total_population,
203                "by_habitat": by_habitat
204            }
205
206
207# === DEMONSTRACJA ===
208
209print("=== SAFARI DATABASE SYSTEM ===\n")
210
211db = SafariDatabase("safari_demo.db")
212
213# 1. Dodaj gatunki
214print("1. Dodawanie gatunków...")
215lion_id = db.create_species("Panthera leo", "Lew", 120, "sawanna", True)
216elephant_id = db.create_species("Loxodonta africana", "Słoń afrykański", 450, "sawanna", True)
217gorilla_id = db.create_species("Gorilla gorilla", "Goryl", 230, "las tropikalny", True)
218python_id = db.create_species("Python regius", "Pyton królewski", 85, "dżungla", False)
219
220print(f"   Dodano {4} gatunki")
221
222# 2. Pobierz gatunek
223print("\n2. Pobieranie gatunku...")
224lion = db.get_species(lion_id)
225print(f"   {lion['common_name']} ({lion['scientific_name']})")
226print(f"   Populacja: {lion['population']}, Zagrożony: {'Tak' if lion['endangered'] else 'Nie'}")
227
228# 3. Lista gatunków z filtrem
229print("\n3. Lista zagrożonych gatunków...")
230endangered = db.list_species(endangered=True)
231for species in endangered:
232    print(f"   - {species['common_name']}: {species['population']} osobników")
233
234# 4. Aktualizacja
235print("\n4. Aktualizacja populacji lwa...")
236db.update_species(lion_id, population=125)
237lion = db.get_species(lion_id)
238print(f"   Nowa populacja: {lion['population']}")
239
240# 5. Dodaj obserwacje
241print("\n5. Dodawanie obserwacji...")
242db.create_observation(lion_id, "2024-01-15", "Serengeti North", 12, "Pride with 2 cubs")
243db.create_observation(lion_id, "2024-01-20", "Masai Mara", 8, "Male coalition")
244db.create_observation(elephant_id, "2024-01-16", "Amboseli", 35, "Large herd")
245
246print("   Dodano 3 obserwacje")
247
248# 6. Pobierz obserwacje
249print("\n6. Obserwacje lwa...")
250lion_obs = db.get_observations_for_species(lion_id)
251for obs in lion_obs:
252    print(f"   - {obs['observation_date']}: {obs['count']}x w {obs['location']}")
253
254# 7. Statystyki
255print("\n7. Statystyki bazy danych...")
256stats = db.get_statistics()
257print(f"   Łącznie gatunków: {stats['total_species']}")
258print(f"   Zagrożone: {stats['endangered_count']}")
259print(f"   Łączna populacja: {stats['total_population']}")
260print("   Według siedliska:")
261for habitat_stat in stats['by_habitat']:
262    print(f"     - {habitat_stat['habitat']}: {habitat_stat['count']} gatunków")
263
264print("\n✓ Demonstracja zakończona")

Transakcje - atomowość operacji

1import sqlite3
2
3conn = sqlite3.connect("safari.db")
4
5try:
6    cursor = conn.cursor()
7
8    # Rozpocznij transakcję (domyślnie)
9    cursor.execute("INSERT INTO species (...) VALUES (...)")
10    cursor.execute("UPDATE observations SET ...")
11
12    # Zatwierdź transakcję
13    conn.commit()
14    print("✓ Transakcja zatwierdzona")
15
16except Exception as e:
17    # Wycofaj transakcję przy błędzie
18    conn.rollback()
19    print(f"❌ Transakcja wycofana: {e}")
20
21finally:
22    conn.close()

Podsumowanie

W tej lekcji nauczyłeś/aś się:

  • ✅ Czym są bazy danych i po co służą
  • ✅ Relacyjne (SQL) vs NoSQL
  • ✅ SQLite - baza w jednym pliku
  • ✅ SQL: CREATE TABLE, INSERT, SELECT, UPDATE, DELETE
  • ✅ WHERE, ORDER BY, LIMIT, JOIN
  • ✅ Python sqlite3: connect, cursor, execute, fetchall
  • ✅ Parameteryzowane zapytania (SQL injection prevention)
  • ✅ Transakcje i rollback
  • ✅ Kompletny system CRUD z relacjami

Checkpoint

Przed przejściem dalej:

  • [ ] Potrafisz stworzyć tabelę (CREATE TABLE)
  • [ ] Potrafisz dodać dane (INSERT)
  • [ ] Potrafisz pobrać dane z filtrem (SELECT WHERE)
  • [ ] Rozumiesz JOIN (łączenie tabel)
  • [ ] Potrafisz używać sqlite3 w Pythonie

Analogia Safari: Baza danych SQL to wielka biblioteka katalogowa z milionami kart gatunków - szybkie wyszukiwanie, relacje, integralność danych. JSON to notatnik - świetny na 10 wpisów, niewystarczający na miliony! 📚🗄️

W następnej lekcji Darwin pokaże Ci SQLAlchemy ORM - jak pracować z bazami danych używając pythonowych klas zamiast surowego SQL! 🐍🔗

Przejdź do CodeWorlds