Phase 1: Datenmodell, CRUD-API und Stammdaten
FastAPI mit SQLAlchemy und SQLite, Migrationen über Alembic. Die
Endpunkte aus projekt.md für items, categories und sizes stehen samt
Reservierung; Bilder und Anmeldung folgen in Phase 2 und 3.
Umgesetzt wie in Plan.md festgelegt:
- Feste Wertelisten als Enum UND als CHECK in der Datenbank. Die
CHECK-Bedingung ist der eigentliche Schutz: an SQLAlchemy vorbei (Import,
sqlite3 von Hand) käme sonst "Gril" durch, und die Filter griffen still
nicht mehr.
- Status "draft" für den Stapel-Import. Entwürfe erscheinen weder in der
Galerie noch in GET /items, solange nicht ausdrücklich status=draft
angefragt wird, und lassen sich nicht reservieren.
- Titel ist freiwillig und wird sonst beim ANZEIGEN aus Kategorie und
Grösse gebildet ("Jacken 98/104") - nicht beim Speichern, damit er einer
späteren Korrektur der Grösse folgt.
- reservation_token: nur wer es hat, kann die eigene Reservierung aufheben.
Verglichen mit compare_digest, gelöscht beim Freigeben und beim
Erledigen, damit ein alter Link nicht später eine fremde Reservierung
aufhebt.
- Eigene sizes-Tabelle mit sort_order statt SELECT DISTINCT: sonst stünde
"104" vor "56" und jeder Tippfehler würde zur Filteroption.
- Pagination auf GET /items, in projekt.md nicht vorgesehen.
Zwei SQLite-Eigenheiten, die leicht untergehen: foreign_keys ist
standardmässig AUS (ohne PRAGMA greift ON DELETE CASCADE nicht), und
check_same_thread muss für FastAPI abgeschaltet werden. Beides in
database.py, dazu WAL fürs gleichzeitige Lesen.
24 Tests, alle grün. Zusätzlich von Hand geprüft: Migration anwenden,
Eintrag anlegen, reservieren, ohne Token freigeben (403), mit Token
freigeben (200), Swagger UI erreichbar.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GR4bNaj9GtRu57J4Niii8o
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
"""Datenbank-Anbindung.
|
||||
|
||||
SQLite braucht zwei Einstellungen, die man leicht übersieht:
|
||||
|
||||
* ``check_same_thread=False`` - FastAPI bearbeitet Anfragen in mehreren
|
||||
Threads, SQLite verbietet das sonst für eine Verbindung.
|
||||
* ``PRAGMA foreign_keys=ON`` - Fremdschlüssel sind in SQLite standardmässig
|
||||
**aus**. Ohne diese Zeile greift ``ON DELETE CASCADE`` nicht, und beim
|
||||
Löschen eines Eintrags blieben dessen Bilder als Waisen zurück.
|
||||
"""
|
||||
|
||||
from collections.abc import Iterator
|
||||
|
||||
from sqlalchemy import create_engine, event
|
||||
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
|
||||
|
||||
from app.config import einstellungen
|
||||
|
||||
|
||||
class Basis(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
def engine_erzeugen(url: str | None = None):
|
||||
cfg = einstellungen()
|
||||
if url is None:
|
||||
cfg.datenbank_pfad.parent.mkdir(parents=True, exist_ok=True)
|
||||
url = cfg.datenbank_url
|
||||
|
||||
engine = create_engine(
|
||||
url,
|
||||
connect_args={"check_same_thread": False},
|
||||
# Wartet, statt sofort abzubrechen, wenn parallel geschrieben wird.
|
||||
pool_pre_ping=True,
|
||||
)
|
||||
|
||||
@event.listens_for(engine, "connect")
|
||||
def _sqlite_pragmas(dbapi_connection, _connection_record):
|
||||
cur = dbapi_connection.cursor()
|
||||
cur.execute("PRAGMA foreign_keys=ON")
|
||||
# WAL: Lesen wird nicht mehr durch einen laufenden Schreibvorgang
|
||||
# blockiert - bei einer Galerie, die viele gleichzeitig ansehen,
|
||||
# der spürbarere Unterschied.
|
||||
cur.execute("PRAGMA journal_mode=WAL")
|
||||
cur.execute("PRAGMA busy_timeout=5000")
|
||||
cur.close()
|
||||
|
||||
return engine
|
||||
|
||||
|
||||
engine = engine_erzeugen()
|
||||
SessionLocal = sessionmaker(bind=engine, autoflush=False, expire_on_commit=False)
|
||||
|
||||
|
||||
def get_db() -> Iterator[Session]:
|
||||
"""FastAPI-Abhängigkeit: eine Sitzung pro Anfrage, danach geschlossen."""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
Reference in New Issue
Block a user