Backend Kritik Notlar
Bu sayfa, backend tarafında üretimde tekrarlanan ve özel dikkat gerektiren pattern'leri toplar. Her madde gerçek bir incident veya near-miss sonucu üretilmiştir.
Cihaz Silme FK Cascading
/api/devices/{id} DELETE endpoint'i çağrıldığında, ilişkili tablolardaki FK kayıtları silinmeden cihaz silinmeye çalışılırsa PostgreSQL IntegrityError fırlatır. Bu durum frontend tarafında genel bir 500 olarak görünür ve kullanıcıya anlamsız hata mesajı döner.
Bağımlı Tablolar
Aşağıdaki tablolar devices.id foreign key referansı tutar:
| Tablo | İlişki | Davranış |
|---|---|---|
device_connection_logs | N:1 | Önce DELETE edilmeli |
alarm_policies | N:1 (device_id nullable) | NULL'a set veya DELETE |
user_widget_layouts | N:1 | DELETE edilmeli (UI state, kayıp tolere) |
device_asset_map | N:N junction | DELETE edilmeli |
device_bms_identity | 1:1 | Cascade delete (aşağıda detay) |
device_firmware_status | 1:1 | Cascade delete |
device_transformer_config | 1:1 | Cascade delete |
Service Layer Pattern
async def delete_device(session: AsyncSession, device_id: UUID) -> None:
try:
# 1. Bağımlı kayıtları sil
await session.execute(
delete(DeviceConnectionLog).where(DeviceConnectionLog.device_id == device_id)
)
await session.execute(
delete(UserWidgetLayout).where(UserWidgetLayout.device_id == device_id)
)
await session.execute(
delete(DeviceAssetMap).where(DeviceAssetMap.device_id == device_id)
)
# 2. Asıl device satırını sil
await session.execute(delete(Device).where(Device.id == device_id))
await session.commit()
except IntegrityError as exc:
await session.rollback()
raise HTTPException(
status_code=409,
detail={
"error_code": "DEVICE_DELETE_CONFLICT",
"message": "Cihaz başka kayıtlar tarafından referans alınıyor",
"constraint": str(exc.orig),
},
)
409 Conflict Yanıtı
Frontend 409 durumunda kullanıcıya "Bu cihaz silinemiyor: ilişkili kayıtlar var" mesajı gösterir; raw constraint name'i kullanıcıya gösterilmez, sadece log'a yazılır.
DeviceBmsIdentity Cascade
device_bms_identity tablosu devices ile 1:1 ilişkidedir. Cihaz silindiğinde BMS identity kaydı otomatik silinmeli, aksi halde NOT NULL violation alınır (BMS identity'nin device_id kolonu NOT NULL).
SQLAlchemy Relationship
class Device(Base):
bms_identity = relationship(
"DeviceBmsIdentity",
back_populates="device",
cascade="all, delete-orphan",
passive_deletes=True,
uselist=False,
)
class DeviceBmsIdentity(Base):
device_id = Column(
UUID,
ForeignKey("devices.id", ondelete="CASCADE"),
nullable=False,
)
Önemli Noktalar
cascade="all, delete-orphan"— ORM seviyesinde cascadepassive_deletes=True— ORM child satırları çekmez, DB-levelON DELETE CASCADE'i kullanırondelete="CASCADE"— DB constraint, ORM bypass edilse bile çalışır
passive_deletes=True olmazsa SQLAlchemy önce SELECT ile child satırları çeker, sonra teker teker DELETE eder; binlerce cihazda performans kabul edilemez seviyeye düşer.
NOT NULL Violation Önleme
DB-level ON DELETE CASCADE parent silindiğinde child satırları otomatik temizler. Bu şekilde:
DELETE FROM devices WHERE id = ?
↓ (CASCADE trigger)
DELETE FROM device_bms_identity WHERE device_id = ? -- otomatik
ORM ile manuel cleanup yapılırsa transaction sırası DELETE child → DELETE parent olmalıdır; aksi halde NOT NULL violation alınır.
Bus Address Uniqueness — 3-Katmanlı Savunma
Aynı MQTT gateway altında iki cihazın aynı bus_address'i kullanması imkansızdır (Modbus protokol gereği slave ID benzersiz olmalı). Bu invariant 3 katmanda enforce edilir.
Katman 1 — Service-level Pre-check
POST ve PUT /api/devices endpoint'leri service katmanında pre-validation yapar:
async def _validate_bus_address_uniqueness(
session: AsyncSession,
mqtt_gateway_id: UUID,
bus_address: int,
exclude_device_id: UUID | None = None,
) -> None:
query = select(Device).where(
Device.mqtt_gateway_id == mqtt_gateway_id,
Device.bus_address == bus_address,
)
if exclude_device_id is not None:
query = query.where(Device.id != exclude_device_id) # self-exclusion
existing = await session.scalar(query)
if existing:
raise HTTPException(
status_code=409,
detail={
"error_code": "BUS_ADDRESS_CONFLICT",
"message": f"Bus address {bus_address} bu gateway'de kullanımda",
"conflicting_device_id": str(existing.id),
},
)
exclude_device_id parametresi PUT (update) sırasında cihazın kendisini hariç tutar, aksi halde bus_address aynı kalsa bile çakışma olarak algılanır.
Katman 2 — DB Constraint (Migration 0035)
Service-level kontrol race condition'a açıktır (iki paralel POST aynı anda gelirse). DB seviyesinde UNIQUE constraint son söz hakkına sahiptir:
ALTER TABLE devices
ADD CONSTRAINT uq_mqtt_gateway_bus_address
UNIQUE (mqtt_gateway_id, bus_address);
Bu constraint sadece mqtt_gateway_id IS NOT NULL olan satırlar için anlamlıdır (Modbus TCP gateway cihazları için mqtt_gateway_id NULL'dır, NULL'lar UNIQUE kısıtından muaftır).
Katman 3 — IntegrityError Handler
DB constraint ihlali yakalandığında 500 yerine 409 döndürülür:
try:
session.add(device)
await session.commit()
except IntegrityError as exc:
await session.rollback()
if "uq_mqtt_gateway_bus_address" in str(exc.orig):
raise HTTPException(
status_code=409,
detail={
"error_code": "BUS_ADDRESS_CONFLICT",
"message": "Bus address çakışması (race condition)",
},
)
raise # Başka constraint ise tekrar fırlat
Self-exclusion Detayı
PUT endpoint'i _validate_bus_address_uniqueness(..., exclude_device_id=current_id) ile çağrılır. Bu sayede aynı cihazın aynı bus_address ile update edilmesi 409 vermez (no-op update mümkün olur).
| Durum | Beklenen Sonuç |
|---|---|
| POST yeni cihaz, bus_address boş slot | 201 Created |
| POST yeni cihaz, bus_address kullanımda | 409 Conflict |
| PUT mevcut cihaz, bus_address aynı | 200 OK (no-op) |
| PUT mevcut cihaz, bus_address başka cihazda | 409 Conflict |
| Race: iki POST aynı bus_address | İlk: 201, İkinci: 409 (DB layer) |