# 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)