Ana içeriğe geç

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şkiDavranış
device_connection_logsN:1Önce DELETE edilmeli
alarm_policiesN:1 (device_id nullable)NULL'a set veya DELETE
user_widget_layoutsN:1DELETE edilmeli (UI state, kayıp tolere)
device_asset_mapN:N junctionDELETE edilmeli
device_bms_identity1:1Cascade delete (aşağıda detay)
device_firmware_status1:1Cascade delete
device_transformer_config1:1Cascade 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 cascade
  • passive_deletes=True — ORM child satırları çekmez, DB-level ON DELETE CASCADE'i kullanır
  • ondelete="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).

DurumBeklenen Sonuç
POST yeni cihaz, bus_address boş slot201 Created
POST yeni cihaz, bus_address kullanımda409 Conflict
PUT mevcut cihaz, bus_address aynı200 OK (no-op)
PUT mevcut cihaz, bus_address başka cihazda409 Conflict
Race: iki POST aynı bus_addressİlk: 201, İkinci: 409 (DB layer)