From 08ce8efe559684631782488792a656a74d040e25 Mon Sep 17 00:00:00 2001 From: patrick Date: Tue, 8 Sep 2026 14:56:05 +0200 Subject: [PATCH] =?UTF-8?q?fix(backend):=20Hersteller/Modell-Duplikatpr?= =?UTF-8?q?=C3=BCfung=20vorab=20statt=20=C3=BCber=20UNIQUE-Constraint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Testsuite-get_db-Override macht kein Rollback bei Exceptions (dokumentierter Fund 2026-09-05) - ein Flush-Fehlschlag über IntegrityError hinterlässt die geteilte Test-Session im PendingRollbackError-Zustand für jeden folgenden Request im selben Test. Projekt-Konvention laut geraet_instanz.py::erstelle ist deshalb explizites Vorab-Prüfen statt Verlassen auf den DB-Constraint - hier nachgezogen. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV --- backend/app/api/v1/endpoints/stammdaten.py | 24 ++++++++++++++-------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/backend/app/api/v1/endpoints/stammdaten.py b/backend/app/api/v1/endpoints/stammdaten.py index 88ca18b..d11cba1 100644 --- a/backend/app/api/v1/endpoints/stammdaten.py +++ b/backend/app/api/v1/endpoints/stammdaten.py @@ -1,6 +1,5 @@ from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import func, select -from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import get_current_user, require_roles, require_roles_or_permission @@ -96,12 +95,17 @@ async def erstelle_hersteller( db: AsyncSession = Depends(get_db), _=Depends(require_roles(RolleTyp.administration)), ) -> Hersteller: + # Explizit vorab prüfen statt sich auf den UNIQUE-Constraint zu verlassen: + # ein Flush-Fehlschlag hinterlässt die Session im Zustand DEACTIVE + # (SQLAlchemy 2.0), was in einer über mehrere Requests geteilten Session + # (z.B. Testsuite) jeden folgenden Request mit PendingRollbackError zum + # Absturz bringt (gleicher Fund wie bei geraet_instanz.py::erstelle). + bestehender = await db.execute(select(Hersteller).where(Hersteller.name == payload.name)) + if bestehender.scalar_one_or_none() is not None: + raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Hersteller existiert bereits") hersteller = Hersteller(**payload.model_dump()) db.add(hersteller) - try: - await db.flush() - except IntegrityError as exc: - raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Hersteller existiert bereits") from exc + await db.flush() return hersteller @@ -126,12 +130,14 @@ async def erstelle_modell( ) -> Modell: if await db.get(Hersteller, payload.hersteller_id) is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Hersteller nicht gefunden") + bestehendes = await db.execute( + select(Modell).where(Modell.hersteller_id == payload.hersteller_id, Modell.name == payload.name) + ) + if bestehendes.scalar_one_or_none() is not None: + raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Modell existiert bereits") modell = Modell(**payload.model_dump()) db.add(modell) - try: - await db.flush() - except IntegrityError as exc: - raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Modell existiert bereits") from exc + await db.flush() return modell