845 lines
30 KiB
Markdown
845 lines
30 KiB
Markdown
# Schritt 2: Interface erstellen
|
|
|
|
[← Zurück: Datenmodell erstellen](02-datamodels.md) | [Weiter: Feature-Logik implementieren →](04-feature-logic.md)
|
|
|
|
## Übersicht: Was sind Interfaces?
|
|
|
|
**Interfaces** sind aktive Klassen, die den **Datenbankzugriff** implementieren. Sie unterscheiden sich von **Datamodels** (die nur die Datenstruktur definieren):
|
|
|
|
| **Aspekt** | **Datamodels** | **Interfaces** |
|
|
|------------|----------------|----------------|
|
|
| **Zweck** | Definiert **WAS** (Datenstruktur) | Implementiert **WIE** (Datenzugriff) |
|
|
| **Inhalt** | Pydantic-Modelle mit Feldern und Validierung | Klassen mit CRUD-Methoden (`create`, `get`, `update`, `delete`) |
|
|
| **Beispiel** | `class Projekt(BaseModel): ...` | `def createProjekt(...) -> Projekt: ...` |
|
|
| **Aktivität** | Passiv (nur Struktur) | Aktiv (führt Operationen aus) |
|
|
|
|
**Analogie:**
|
|
- **Datamodel** = Bauplan (beschreibt das Haus)
|
|
- **Interface** = Bauunternehmer (baut das Haus)
|
|
|
|
---
|
|
|
|
## Struktur: Real Estate CRUD-Interface
|
|
|
|
Da das Feature **stateless** arbeitet, benötigen wir nur **ein Interface** für CRUD-Operationen auf Real Estate-Entitäten:
|
|
|
|
### Real Estate-Datenmodelle → Real Estate CRUD-Interface
|
|
|
|
**Datamodel:** `datamodelRealEstate.py`
|
|
- `Projekt`
|
|
- `Parzelle`
|
|
- `Dokument`
|
|
- `Kanton`, `Gemeinde`, `Land`
|
|
- `GeoPolylinie`, `GeoPunkt`
|
|
- `Kontext`
|
|
- etc.
|
|
|
|
**Interface:** `interfaceDbRealEstateObjects.py`
|
|
- `RealEstateObjects` (Haupt-Interface)
|
|
- `RealEstateAccess` (Zugriffskontrolle)
|
|
- Methoden: `createProjekt()`, `getParzelle()`, `updateDokument()`, etc.
|
|
|
|
**Optional:** `interfaceDbRealEstateChatObjects.py` (nur für direkte SQL-Queries)
|
|
- `RealEstateChatObjects` - Für direkte Query-Ausführung ohne Session
|
|
- Methoden: `executeQuery()` - Führt SQL direkt aus
|
|
|
|
---
|
|
|
|
## Warum nur ein Haupt-Interface?
|
|
|
|
1. **Stateless Design**:
|
|
- Keine Session-Verwaltung notwendig
|
|
- Direkte CRUD-Operationen auf Real Estate-Modellen
|
|
|
|
2. **Einfache Architektur**:
|
|
- Ein Interface für alle CRUD-Operationen
|
|
- Weniger Komplexität, bessere Wartbarkeit
|
|
|
|
3. **Optionales Query-Interface**:
|
|
- Nur für direkte SQL-Queries (stateless)
|
|
- Keine Session-Management-Funktionen
|
|
|
|
---
|
|
|
|
## Zu erstellende Dateien
|
|
|
|
### Schritt 2a: Real Estate CRUD-Interface (ERFORDERLICH)
|
|
|
|
**Zwei separate Dateien** (wie bei anderen Features):
|
|
|
|
#### Datei 1: `modules/interfaces/interfaceDbRealEstateAccess.py`
|
|
|
|
**Enthält:**
|
|
- `RealEstateAccess` - Zugriffskontrolle für Real Estate-Entitäten
|
|
- Methoden: `uam()`, `canModify()`
|
|
|
|
**Zweck:** Prüft Zugriffsrechte und filtert Daten basierend auf Benutzerprivilegien
|
|
|
|
#### Datei 2: `modules/interfaces/interfaceDbRealEstateObjects.py`
|
|
|
|
**Enthält:**
|
|
- `RealEstateObjects` - Haupt-Interface für CRUD-Operationen
|
|
- `getInterface()` - Factory-Funktion
|
|
- Nutzt `RealEstateAccess` aus der Access-Datei
|
|
|
|
**Zweck:** Verwaltet Real Estate-Entitäten (Projekt, Parzelle, Dokument, etc.)
|
|
|
|
**Nutzt:**
|
|
- `datamodelRealEstate.py` (Projekt, Parzelle, Dokument, etc.)
|
|
- `interfaceDbRealEstateAccess.py` (für Zugriffskontrolle)
|
|
|
|
**Wann benötigt:** Für alle CRUD-Operationen auf Real Estate-Entitäten (z.B. Projekte erstellen/bearbeiten, Parzellen verwalten). Dies ist das Haupt-Interface für das Feature.
|
|
|
|
---
|
|
|
|
### Schritt 2b: Query-Interface (OPTIONAL - nur für direkte SQL-Queries)
|
|
|
|
**Eine Datei** für stateless Query-Ausführung:
|
|
|
|
#### Datei: `modules/interfaces/interfaceDbRealEstateChatObjects.py`
|
|
|
|
**Enthält:**
|
|
- `RealEstateChatObjects` - Interface für direkte SQL-Query-Ausführung
|
|
- `getInterface()` - Factory-Funktion
|
|
- Methoden: `executeQuery()` - Führt SQL direkt aus (stateless)
|
|
|
|
**Zweck:** Direkte SQL-Query-Ausführung ohne Session-Management
|
|
|
|
**Nutzt:**
|
|
- `connectorDbPostgre.DatabaseConnector` für direkte SQL-Ausführung
|
|
- Keine Chat-Modelle (stateless)
|
|
|
|
**Wann benötigt:** Nur wenn Sie direkte SQL-Queries ausführen möchten (z.B. für komplexe SELECT-Queries). Für CRUD-Operationen verwenden Sie das Real Estate CRUD-Interface.
|
|
|
|
---
|
|
|
|
## Übersicht: Dateien und ihre Beziehungen
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ DATAMODELS (Struktur) │
|
|
├─────────────────────────────────────────────────────────────┤
|
|
│ datamodelRealEstate.py │
|
|
│ ├── Projekt │
|
|
│ ├── Parzelle │
|
|
│ ├── Dokument │
|
|
│ ├── Kanton, Gemeinde, Land │
|
|
│ ├── GeoPolylinie, GeoPunkt │
|
|
│ ├── Kontext │
|
|
│ └── ... │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
│
|
|
│ nutzt
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ INTERFACES (Zugriff) │
|
|
├─────────────────────────────────────────────────────────────┤
|
|
│ REAL ESTATE CRUD-INTERFACE (ERFORDERLICH) │
|
|
│ │
|
|
│ interfaceDbRealEstateAccess.py │
|
|
│ └── RealEstateAccess │
|
|
│ ├── uam() │
|
|
│ └── canModify() │
|
|
│ │
|
|
│ interfaceDbRealEstateObjects.py │
|
|
│ ├── RealEstateObjects │
|
|
│ │ ├── createProjekt() │
|
|
│ │ ├── getProjekt() │
|
|
│ │ ├── updateProjekt() │
|
|
│ │ ├── deleteProjekt() │
|
|
│ │ ├── createParzelle() │
|
|
│ │ ├── getParzelle() │
|
|
│ │ └── ... (CRUD für alle Entitäten) │
|
|
│ └── getInterface() │
|
|
│ └── nutzt RealEstateAccess │
|
|
│ │
|
|
│ QUERY-INTERFACE (OPTIONAL - nur für direkte SQL) │
|
|
│ │
|
|
│ interfaceDbRealEstateChatObjects.py │
|
|
│ ├── RealEstateChatObjects │
|
|
│ │ └── executeQuery() # Direkte SQL-Ausführung │
|
|
│ └── getInterface() │
|
|
│ └── Keine Access-Klasse (stateless) │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## Interface-Struktur: Access vs. Objects
|
|
|
|
Jedes Interface besteht aus **zwei Klassen**:
|
|
|
|
### 1. `*Access` Klasse (Zugriffskontrolle)
|
|
|
|
**Zweck:** Prüft, wer was sehen/dürfen darf
|
|
|
|
**Methoden:**
|
|
- `uam()` - Filtert Daten basierend auf Benutzerprivilegien
|
|
- `canModify()` - Prüft, ob Benutzer ändern darf
|
|
|
|
**Beispiel:** `RealEstateChatAccess`
|
|
|
|
### 2. `*Objects` Klasse (Haupt-Interface)
|
|
|
|
**Zweck:** Führt CRUD-Operationen aus
|
|
|
|
**Methoden:**
|
|
- `create*()` - Erstellt neue Einträge
|
|
- `get*()` - Lädt Einträge
|
|
- `update*()` - Aktualisiert Einträge
|
|
- `delete*()` - Löscht Einträge
|
|
|
|
**Nutzt:** `*Access` für Zugriffskontrolle
|
|
|
|
**Beispiel:** `RealEstateChatObjects`
|
|
|
|
**Warum getrennt?**
|
|
- Separation of Concerns: Zugriffskontrolle ist separate Verantwortlichkeit
|
|
- Wiederverwendbarkeit: Access-Klasse kann von mehreren Interfaces genutzt werden
|
|
- Testbarkeit: Zugriffskontrolle kann unabhängig getestet werden
|
|
|
|
---
|
|
|
|
## Implementierung: Real Estate CRUD-Interface
|
|
|
|
Das Real Estate CRUD-Interface besteht aus **zwei separaten Dateien**, genau wie bei anderen Features (`interfaceDbAppObjects.py` + `interfaceDbAppAccess.py`).
|
|
|
|
### Datei 1: Access-Implementierung
|
|
|
|
**Datei:** `modules/interfaces/interfaceDbRealEstateAccess.py`
|
|
|
|
```python
|
|
"""
|
|
Access control for Real Estate interface.
|
|
Handles user access management and permission checks.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Dict, Any, List, Optional
|
|
from modules.datamodels.datamodelUam import User, UserPrivilege
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class RealEstateAccess:
|
|
"""
|
|
Access control class for Real Estate interface.
|
|
Handles user access management and permission checks.
|
|
"""
|
|
|
|
def __init__(self, currentUser: User, db):
|
|
"""Initialize with user context."""
|
|
self.currentUser = currentUser
|
|
self.mandateId = currentUser.mandateId
|
|
self.userId = currentUser.id
|
|
|
|
if not self.mandateId or not self.userId:
|
|
raise ValueError("Invalid user context: mandateId and userId are required")
|
|
|
|
self.db = db
|
|
|
|
def uam(self, model_class: type, recordset: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
"""
|
|
Unified user access management function that filters data based on user privileges.
|
|
|
|
Args:
|
|
model_class: Pydantic model class for the table
|
|
recordset: Recordset to filter based on access rules
|
|
|
|
Returns:
|
|
Filtered recordset with access control attributes
|
|
"""
|
|
from modules.datamodels.datamodelUam import UserPrivilege
|
|
|
|
userPrivilege = self.currentUser.privilege
|
|
filtered_records = []
|
|
|
|
# System admins see all records
|
|
if userPrivilege == UserPrivilege.SYSADMIN:
|
|
filtered_records = recordset
|
|
# Admins see records in their mandate
|
|
elif userPrivilege == UserPrivilege.ADMIN:
|
|
filtered_records = [r for r in recordset if r.get("mandateId", "-") == self.mandateId]
|
|
# Regular users see only their records
|
|
else:
|
|
filtered_records = [
|
|
r for r in recordset
|
|
if r.get("mandateId", "-") == self.mandateId and r.get("_createdBy") == self.userId
|
|
]
|
|
|
|
# Add access control attributes
|
|
for record in filtered_records:
|
|
record["_hideView"] = False
|
|
record["_hideEdit"] = not self.canModify(model_class, record.get("id"))
|
|
record["_hideDelete"] = not self.canModify(model_class, record.get("id"))
|
|
|
|
return filtered_records
|
|
|
|
def canModify(self, model_class: type, recordId: Optional[str] = None) -> bool:
|
|
"""Checks if the current user can modify records."""
|
|
from modules.datamodels.datamodelUam import UserPrivilege
|
|
|
|
userPrivilege = self.currentUser.privilege
|
|
|
|
if userPrivilege == UserPrivilege.SYSADMIN:
|
|
return True
|
|
|
|
if recordId is not None:
|
|
records = self.db.getRecordset(model_class, recordFilter={"id": recordId})
|
|
if not records:
|
|
return False
|
|
|
|
record = records[0]
|
|
|
|
if userPrivilege == UserPrivilege.ADMIN and record.get("mandateId", "-") == self.mandateId:
|
|
return True
|
|
|
|
if (record.get("mandateId", "-") == self.mandateId and
|
|
record.get("_createdBy") == self.userId):
|
|
return True
|
|
|
|
return False
|
|
else:
|
|
return True # Regular users can create records
|
|
```
|
|
|
|
---
|
|
|
|
### Datei 2: Objects-Implementierung
|
|
|
|
**Datei:** `modules/interfaces/interfaceDbRealEstateObjects.py`
|
|
|
|
```python
|
|
"""
|
|
Interface to Real Estate database objects.
|
|
Uses PostgreSQL connector for data access with user/mandate filtering.
|
|
Handles CRUD operations on Real Estate entities (Projekt, Parzelle, etc.).
|
|
"""
|
|
|
|
import logging
|
|
from typing import Dict, Any, List, Optional
|
|
from modules.datamodels.datamodelRealEstate import (
|
|
Projekt,
|
|
Parzelle,
|
|
Dokument,
|
|
Kanton,
|
|
Gemeinde,
|
|
Land,
|
|
GeoPolylinie,
|
|
GeoPunkt,
|
|
Kontext,
|
|
StatusProzess,
|
|
)
|
|
from modules.datamodels.datamodelUam import User
|
|
from modules.connectors.connectorDbPostgre import DatabaseConnector
|
|
from modules.shared.configuration import APP_CONFIG
|
|
# Import Access-Klasse aus separater Datei
|
|
from modules.interfaces.interfaceDbRealEstateAccess import RealEstateAccess
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Singleton factory for Real Estate interfaces
|
|
_realEstateInterfaces = {}
|
|
|
|
|
|
class RealEstateObjects:
|
|
"""
|
|
Interface to Real Estate database objects.
|
|
Uses PostgreSQL connector for data access with user/mandate filtering.
|
|
Handles CRUD operations on Real Estate entities.
|
|
"""
|
|
|
|
def __init__(self, currentUser: Optional[User] = None):
|
|
"""Initializes the Real Estate Interface."""
|
|
self.currentUser = currentUser
|
|
self.userId = currentUser.id if currentUser else None
|
|
self.mandateId = currentUser.mandateId if currentUser else None
|
|
self.access = None
|
|
|
|
# Initialize database
|
|
self._initializeDatabase()
|
|
|
|
# Set user context if provided
|
|
if currentUser:
|
|
self.setUserContext(currentUser)
|
|
|
|
def _initializeDatabase(self):
|
|
"""Initialize PostgreSQL database connection."""
|
|
try:
|
|
# Get database configuration from environment
|
|
dbHost = APP_CONFIG.get("DB_APP_HOST", "localhost")
|
|
dbDatabase = APP_CONFIG.get("DB_APP_DATABASE", "poweron_app")
|
|
dbUser = APP_CONFIG.get("DB_APP_USER")
|
|
dbPassword = APP_CONFIG.get("DB_APP_PASSWORD_SECRET")
|
|
dbPort = int(APP_CONFIG.get("DB_APP_PORT", 5432))
|
|
|
|
# Initialize database connector
|
|
self.db = DatabaseConnector(
|
|
dbHost=dbHost,
|
|
dbDatabase=dbDatabase,
|
|
dbUser=dbUser,
|
|
dbPassword=dbPassword,
|
|
dbPort=dbPort,
|
|
userId=self.userId if self.userId else None,
|
|
)
|
|
|
|
logger.info(f"Real Estate database connector initialized for database: {dbDatabase}")
|
|
except Exception as e:
|
|
logger.error(f"Error initializing Real Estate database: {e}")
|
|
raise
|
|
|
|
def setUserContext(self, currentUser: User):
|
|
"""Sets the user context for the interface."""
|
|
self.currentUser = currentUser
|
|
self.userId = currentUser.id
|
|
self.mandateId = currentUser.mandateId
|
|
|
|
if not self.userId or not self.mandateId:
|
|
raise ValueError("Invalid user context: id and mandateId are required")
|
|
|
|
# Initialize access control
|
|
self.access = RealEstateAccess(self.currentUser, self.db)
|
|
|
|
# Update database context
|
|
self.db.updateContext(self.userId)
|
|
|
|
# ===== Projekt Methods =====
|
|
|
|
def createProjekt(self, projekt: Projekt) -> Projekt:
|
|
"""Create a new project."""
|
|
# Ensure mandateId is set
|
|
if not projekt.mandateId:
|
|
projekt.mandateId = self.mandateId
|
|
|
|
# Apply access control
|
|
self.access.uam(Projekt, [])
|
|
|
|
# Save to database
|
|
self.db.recordCreate(Projekt, projekt.model_dump())
|
|
|
|
return projekt
|
|
|
|
def getProjekt(self, projektId: str) -> Optional[Projekt]:
|
|
"""Get a project by ID."""
|
|
records = self.db.getRecordset(
|
|
Projekt,
|
|
recordFilter={"id": projektId}
|
|
)
|
|
|
|
if not records:
|
|
return None
|
|
|
|
# Apply access control
|
|
filtered = self.access.uam(Projekt, records)
|
|
|
|
if not filtered:
|
|
return None
|
|
|
|
return Projekt(**filtered[0])
|
|
|
|
def updateProjekt(self, projektId: str, updateData: Dict[str, Any]) -> Optional[Projekt]:
|
|
"""Update a project."""
|
|
projekt = self.getProjekt(projektId)
|
|
if not projekt:
|
|
return None
|
|
|
|
# Check if user can modify
|
|
if not self.access.canModify(Projekt, projektId):
|
|
raise PermissionError(f"User {self.userId} cannot modify project {projektId}")
|
|
|
|
# Update fields
|
|
for key, value in updateData.items():
|
|
if hasattr(projekt, key):
|
|
setattr(projekt, key, value)
|
|
|
|
# Save to database
|
|
self.db.recordModify(Projekt, projektId, projekt.model_dump())
|
|
|
|
return projekt
|
|
|
|
def deleteProjekt(self, projektId: str) -> bool:
|
|
"""Delete a project."""
|
|
projekt = self.getProjekt(projektId)
|
|
if not projekt:
|
|
return False
|
|
|
|
# Check if user can modify
|
|
if not self.access.canModify(Projekt, projektId):
|
|
raise PermissionError(f"User {self.userId} cannot delete project {projektId}")
|
|
|
|
return self.db.recordDelete(Projekt, projektId)
|
|
|
|
# ===== Parzelle Methods =====
|
|
|
|
def createParzelle(self, parzelle: Parzelle) -> Parzelle:
|
|
"""Create a new plot."""
|
|
if not parzelle.mandateId:
|
|
parzelle.mandateId = self.mandateId
|
|
|
|
self.access.uam(Parzelle, [])
|
|
self.db.recordCreate(Parzelle, parzelle.model_dump())
|
|
|
|
return parzelle
|
|
|
|
def getParzelle(self, parzelleId: str) -> Optional[Parzelle]:
|
|
"""Get a plot by ID."""
|
|
records = self.db.getRecordset(
|
|
Parzelle,
|
|
recordFilter={"id": parzelleId}
|
|
)
|
|
|
|
if not records:
|
|
return None
|
|
|
|
filtered = self.access.uam(Parzelle, records)
|
|
|
|
if not filtered:
|
|
return None
|
|
|
|
return Parzelle(**filtered[0])
|
|
|
|
def updateParzelle(self, parzelleId: str, updateData: Dict[str, Any]) -> Optional[Parzelle]:
|
|
"""Update a plot."""
|
|
parzelle = self.getParzelle(parzelleId)
|
|
if not parzelle:
|
|
return None
|
|
|
|
if not self.access.canModify(Parzelle, parzelleId):
|
|
raise PermissionError(f"User {self.userId} cannot modify plot {parzelleId}")
|
|
|
|
for key, value in updateData.items():
|
|
if hasattr(parzelle, key):
|
|
setattr(parzelle, key, value)
|
|
|
|
self.db.recordModify(Parzelle, parzelleId, parzelle.model_dump())
|
|
|
|
return parzelle
|
|
|
|
def deleteParzelle(self, parzelleId: str) -> bool:
|
|
"""Delete a plot."""
|
|
parzelle = self.getParzelle(parzelleId)
|
|
if not parzelle:
|
|
return False
|
|
|
|
if not self.access.canModify(Parzelle, parzelleId):
|
|
raise PermissionError(f"User {self.userId} cannot delete plot {parzelleId}")
|
|
|
|
return self.db.recordDelete(Parzelle, parzelleId)
|
|
|
|
# ===== Dokument Methods =====
|
|
|
|
def createDokument(self, dokument: Dokument) -> Dokument:
|
|
"""Create a new document."""
|
|
if not dokument.mandateId:
|
|
dokument.mandateId = self.mandateId
|
|
|
|
self.access.uam(Dokument, [])
|
|
self.db.recordCreate(Dokument, dokument.model_dump())
|
|
|
|
return dokument
|
|
|
|
def getDokument(self, dokumentId: str) -> Optional[Dokument]:
|
|
"""Get a document by ID."""
|
|
records = self.db.getRecordset(
|
|
Dokument,
|
|
recordFilter={"id": dokumentId}
|
|
)
|
|
|
|
if not records:
|
|
return None
|
|
|
|
filtered = self.access.uam(Dokument, records)
|
|
|
|
if not filtered:
|
|
return None
|
|
|
|
return Dokument(**filtered[0])
|
|
|
|
# ... weitere CRUD-Methoden für andere Entitäten (Kanton, Gemeinde, Land, etc.)
|
|
|
|
|
|
def getInterface(currentUser: User) -> RealEstateObjects:
|
|
"""
|
|
Factory function to get or create a Real Estate interface instance for a user.
|
|
Uses singleton pattern per user.
|
|
"""
|
|
userKey = f"{currentUser.id}_{currentUser.mandateId}"
|
|
|
|
if userKey not in _realEstateInterfaces:
|
|
_realEstateInterfaces[userKey] = RealEstateObjects(currentUser)
|
|
|
|
return _realEstateInterfaces[userKey]
|
|
```
|
|
|
|
## Wichtige Punkte:
|
|
|
|
1. **DatabaseConnector**: Nutzt `connectorDbPostgre.DatabaseConnector` für Datenbankzugriff
|
|
2. **Access Control**: `RealEstateAccess` implementiert Benutzer- und Mandaten-Filterung
|
|
3. **Singleton Pattern**: `getInterface()` erstellt pro User eine Instanz
|
|
4. **CRUD-Operationen**: `recordCreate`, `recordModify`, `recordDelete`, `getRecordset` vom Connector
|
|
5. **MandateId**: Wird automatisch gesetzt, wenn nicht vorhanden
|
|
|
|
---
|
|
|
|
## Schritt 2b: Query-Interface (OPTIONAL - nur für direkte SQL-Queries)
|
|
|
|
### Wann benötigt?
|
|
|
|
**Kurze Antwort:** Nur wenn Sie **direkte SQL-Queries** ausführen möchten (z.B. für komplexe SELECT-Queries). Für CRUD-Operationen verwenden Sie das Real Estate CRUD-Interface.
|
|
|
|
#### Szenario 1: Alles über CRUD-Interface (EMPFOHLEN)
|
|
|
|
**Strukturiert und sicher:**
|
|
|
|
```python
|
|
# User schreibt: "Erstelle Projekt 'Test'"
|
|
# Feature-Logik nutzt CRUD-Interface:
|
|
realEstateInterface = getRealEstateInterface(currentUser)
|
|
projekt = realEstateInterface.createProjekt(Projekt(
|
|
mandateId=currentUser.mandateId, # Automatisch gesetzt
|
|
label="Test" # Validierung durch Pydantic
|
|
))
|
|
```
|
|
|
|
**Vorteile:**
|
|
- ✅ **Validierung**: Pydantic-Modelle prüfen alle Felder automatisch
|
|
- ✅ **Zugriffskontrolle**: `RealEstateAccess` prüft Berechtigungen
|
|
- ✅ **Sicherheit**: Kein SQL-Injection-Risiko
|
|
- ✅ **Geschäftslogik**: Automatisches Setzen von Systemfeldern (`_createdBy`, `_createdAt`)
|
|
- ✅ **Typsicherheit**: Fehler werden zur Entwicklungszeit erkannt
|
|
- ✅ **Wartbarkeit**: Zentrale CRUD-Methoden, einfach zu testen
|
|
|
|
#### Szenario 2: Direkte SQL-Queries (OPTIONAL)
|
|
|
|
**Nur für komplexe SELECT-Queries:**
|
|
|
|
```python
|
|
# Für komplexe Queries, die nicht über CRUD-Methoden abgedeckt sind
|
|
chatInterface = getChatInterface(currentUser)
|
|
results = chatInterface.executeQuery(
|
|
"SELECT p.*, COUNT(parz.id) as parzellen_count FROM Projekt p LEFT JOIN Parzelle parz ON parz.projektId = p.id GROUP BY p.id"
|
|
)
|
|
```
|
|
|
|
**Warnung:**
|
|
- ⚠️ **Nur für SELECT-Queries**: Keine INSERT/UPDATE/DELETE über direkte SQL-Queries
|
|
- ⚠️ **Validierung erforderlich**: Queries sollten validiert werden
|
|
- ⚠️ **SQL-Injection-Risiko**: Immer Parameterisierung verwenden
|
|
|
|
#### Empfehlung
|
|
|
|
**Sie benötigen das Query-Interface, wenn Sie:**
|
|
|
|
- ✅ **Komplexe SELECT-Queries** benötigen (z.B. JOINs, Aggregationen)
|
|
- ✅ **Flexible Query-Ausführung** benötigen (nicht über CRUD-Methoden abgedeckt)
|
|
|
|
**Sie benötigen es NICHT, wenn Sie:**
|
|
|
|
- ✅ **Nur CRUD-Operationen** benötigen (verwenden Sie das CRUD-Interface)
|
|
- ✅ **Einfache Queries** haben (können über CRUD-Methoden abgedeckt werden)
|
|
|
|
**Für Production-Systeme:**
|
|
- **CRUD-Operationen**: Immer über CRUD-Interface
|
|
- **Komplexe Queries**: Optional über Query-Interface (nur SELECT)
|
|
|
|
### Struktur: Query-Interface (stateless)
|
|
|
|
**Eine Datei** für direkte SQL-Query-Ausführung:
|
|
|
|
#### Datei: `modules/interfaces/interfaceDbRealEstateChatObjects.py`
|
|
|
|
```python
|
|
"""
|
|
Interface for direct SQL query execution (stateless).
|
|
Uses PostgreSQL connector for direct query execution without session management.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Dict, Any, Optional
|
|
from modules.datamodels.datamodelUam import User
|
|
from modules.connectors.connectorDbPostgre import DatabaseConnector
|
|
from modules.shared.configuration import APP_CONFIG
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Singleton factory
|
|
_realEstateChatInterfaces = {}
|
|
|
|
|
|
class RealEstateChatObjects:
|
|
"""Interface for direct SQL query execution (stateless)."""
|
|
|
|
def __init__(self, currentUser: Optional[User] = None):
|
|
"""Initialize the Query Interface."""
|
|
self.currentUser = currentUser
|
|
self.userId = currentUser.id if currentUser else None
|
|
self.mandateId = currentUser.mandateId if currentUser else None
|
|
|
|
# Initialize database
|
|
self._initializeDatabase()
|
|
|
|
# Set user context if provided
|
|
if currentUser:
|
|
self.setUserContext(currentUser)
|
|
|
|
def _initializeDatabase(self):
|
|
"""Initialize PostgreSQL database connection."""
|
|
try:
|
|
dbHost = APP_CONFIG.get("DB_APP_HOST", "localhost")
|
|
dbDatabase = APP_CONFIG.get("DB_APP_DATABASE", "poweron_app")
|
|
dbUser = APP_CONFIG.get("DB_APP_USER")
|
|
dbPassword = APP_CONFIG.get("DB_APP_PASSWORD_SECRET")
|
|
dbPort = int(APP_CONFIG.get("DB_APP_PORT", 5432))
|
|
|
|
self.db = DatabaseConnector(
|
|
dbHost=dbHost,
|
|
dbDatabase=dbDatabase,
|
|
dbUser=dbUser,
|
|
dbPassword=dbPassword,
|
|
dbPort=dbPort,
|
|
userId=self.userId if self.userId else None,
|
|
)
|
|
|
|
logger.info(f"Real Estate Query database connector initialized for database: {dbDatabase}")
|
|
except Exception as e:
|
|
logger.error(f"Error initializing Real Estate Query database: {e}")
|
|
raise
|
|
|
|
def setUserContext(self, currentUser: User):
|
|
"""Sets the user context for the interface."""
|
|
self.currentUser = currentUser
|
|
self.userId = currentUser.id
|
|
self.mandateId = currentUser.mandateId
|
|
|
|
if not self.userId or not self.mandateId:
|
|
raise ValueError("Invalid user context: id and mandateId are required")
|
|
|
|
# Update database context
|
|
self.db.updateContext(self.userId)
|
|
|
|
# ===== Database Query Execution =====
|
|
|
|
def executeQuery(self, queryText: str, parameters: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
"""
|
|
Execute a SQL query directly on the database (stateless).
|
|
|
|
WARNING: This method executes raw SQL. Ensure proper validation and sanitization
|
|
before calling this method. Consider implementing query whitelisting or
|
|
only allowing SELECT statements for production use.
|
|
|
|
Args:
|
|
queryText: SQL query string (preferably SELECT only)
|
|
parameters: Optional parameters for parameterized queries
|
|
|
|
Returns:
|
|
Dictionary with 'rows' (list of dicts), 'columns' (list of column names),
|
|
'rowCount' (int), and 'executionTime' (float)
|
|
"""
|
|
import time
|
|
|
|
try:
|
|
start_time = time.time()
|
|
|
|
# Ensure connection is alive
|
|
self.db._ensure_connection()
|
|
|
|
with self.db.connection.cursor() as cursor:
|
|
# Execute query
|
|
if parameters:
|
|
# Use parameterized query for safety
|
|
cursor.execute(queryText, parameters)
|
|
else:
|
|
cursor.execute(queryText)
|
|
|
|
# Fetch results
|
|
rows = cursor.fetchall()
|
|
|
|
# Convert to list of dictionaries
|
|
result_rows = [dict(row) for row in rows]
|
|
|
|
# Get column names
|
|
columns = [desc[0] for desc in cursor.description] if cursor.description else []
|
|
|
|
execution_time = time.time() - start_time
|
|
|
|
return {
|
|
"rows": result_rows,
|
|
"columns": columns,
|
|
"rowCount": len(result_rows),
|
|
"executionTime": execution_time,
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Error executing query: {e}")
|
|
raise
|
|
|
|
|
|
def getInterface(currentUser: User) -> RealEstateChatObjects:
|
|
"""
|
|
Factory function to get or create a Query interface instance for a user.
|
|
Uses singleton pattern per user.
|
|
"""
|
|
userKey = f"{currentUser.id}_{currentUser.mandateId}"
|
|
|
|
if userKey not in _realEstateChatInterfaces:
|
|
_realEstateChatInterfaces[userKey] = RealEstateChatObjects(currentUser)
|
|
|
|
return _realEstateChatInterfaces[userKey]
|
|
```
|
|
|
|
### Hinweise zur Implementierung
|
|
|
|
1. **Stateless**: Keine Session-Management-Funktionen
|
|
2. **Nur für Queries**: Primär für SELECT-Queries gedacht
|
|
3. **Sicherheit**: Immer Parameterisierung verwenden
|
|
4. **Validierung**: Queries sollten validiert werden (z.B. nur SELECT erlauben)
|
|
|
|
### Beispiel: Beide Interfaces zusammen nutzen
|
|
|
|
```python
|
|
from modules.interfaces.interfaceDbRealEstateChatObjects import getInterface as getChatInterface
|
|
from modules.interfaces.interfaceDbRealEstateObjects import getInterface as getRealEstateInterface
|
|
|
|
# CRUD-Interface für strukturierte Operationen
|
|
realEstateInterface = getRealEstateInterface(currentUser)
|
|
projekt = realEstateInterface.createProjekt(Projekt(
|
|
mandateId=currentUser.mandateId,
|
|
label="Neues Projekt"
|
|
))
|
|
|
|
# Query-Interface für komplexe SELECT-Queries (optional)
|
|
chatInterface = getChatInterface(currentUser)
|
|
results = chatInterface.executeQuery(
|
|
"SELECT p.*, COUNT(parz.id) as parzellen_count FROM Projekt p LEFT JOIN Parzelle parz ON parz.projektId = p.id GROUP BY p.id"
|
|
)
|
|
```
|
|
|
|
---
|
|
|
|
## Zusammenfassung: Benötigte Dateien
|
|
|
|
### Erforderlich (für CRUD-Operationen):
|
|
|
|
1. ✅ `modules/datamodels/datamodelRealEstate.py`
|
|
- Real Estate-Datenmodelle (Projekt, Parzelle, Dokument, etc.)
|
|
|
|
2. ✅ `modules/interfaces/interfaceDbRealEstateAccess.py`
|
|
- Zugriffskontrolle für Real Estate-Entitäten (RealEstateAccess)
|
|
|
|
3. ✅ `modules/interfaces/interfaceDbRealEstateObjects.py`
|
|
- Real Estate CRUD-Interface (RealEstateObjects)
|
|
- Nutzt `interfaceDbRealEstateAccess.py`
|
|
- Haupt-Interface für alle CRUD-Operationen
|
|
|
|
### Optional (für direkte SQL-Queries):
|
|
|
|
4. ⚠️ `modules/interfaces/interfaceDbRealEstateChatObjects.py`
|
|
- Query-Interface für direkte SQL-Ausführung (RealEstateChatObjects)
|
|
- Stateless, keine Session-Management
|
|
- Nur wenn Sie komplexe SELECT-Queries benötigen
|
|
|
|
---
|
|
|
|
[← Zurück: Datenmodell erstellen](02-datamodels.md) | [Weiter: Feature-Logik implementieren →](04-feature-logic.md)
|
|
|
|
|
|
|