gateway/gwserver/connector_db_mysql.py
2025-03-21 14:29:48 +01:00

644 lines
No EOL
23 KiB
Python

import os
import logging
from typing import List, Dict, Any, Optional, Union
from datetime import datetime
import mysql.connector
from mysql.connector import Error
logger = logging.getLogger(__name__)
class MySQLDatabaseConnector:
"""
Ein Konnektor für MySQL-basierte Datenspeicherung.
Stellt generische Datenbankoperationen bereit.
"""
def __init__(self, db_host: str, db_name: str, db_user: str, db_password: str, mandate_id: int = None, user_id: int = None):
"""
Initialisiert den MySQL-Datenbankkonnektor.
Args:
db_host: MySQL-Server Host
db_name: Name der Datenbank
db_user: Benutzername für die Authentifizierung
db_password: Passwort für die Authentifizierung
mandate_id: Kontext-Parameter für den Mandanten
user_id: Kontext-Parameter für den Benutzer
"""
# Speichere die Eingabeparameter
self.db_host = db_host
self.db_name = db_name
self.db_user = db_user
self.db_password = db_password
# Prüfe, ob Kontext-Parameter gesetzt sind
if mandate_id is None or user_id is None:
raise ValueError("mandate_id und user_id müssen gesetzt sein")
self.mandate_id = mandate_id
self.user_id = user_id
# Stelle Verbindung zur Datenbank her
self.connection = self._create_connection()
# System-Tabelle initialisieren
self._system_table_name = "_system"
self._initialize_system_table()
logger.info(f"MySQLDatabaseConnector initialisiert für Datenbank: {db_name}")
logger.info(f"Kontext: mandate_id={mandate_id}, user_id={user_id}")
def _create_connection(self):
"""Erstellt eine Verbindung zur MySQL-Datenbank"""
try:
connection = mysql.connector.connect(
host=self.db_host,
database=self.db_name,
user=self.db_user,
password=self.db_password
)
if connection.is_connected():
logger.info(f"Verbunden mit MySQL-Server Version {connection.get_server_info()}")
return connection
except Error as e:
logger.error(f"Fehler bei der Verbindung zu MySQL: {e}")
raise
def _initialize_system_table(self):
"""Initialisiert die System-Tabelle, falls sie noch nicht existiert."""
cursor = None
try:
cursor = self.connection.cursor()
# Prüfe, ob die System-Tabelle existiert
cursor.execute(f"""
SELECT COUNT(*)
FROM information_schema.tables
WHERE table_schema = '{self.db_name}'
AND table_name = '{self._system_table_name}'
""")
if cursor.fetchone()[0] == 0:
# Erstelle die System-Tabelle
cursor.execute(f"""
CREATE TABLE {self._system_table_name} (
table_name VARCHAR(255) PRIMARY KEY,
initial_id INT NOT NULL
)
""")
self.connection.commit()
logger.info(f"System-Tabelle '{self._system_table_name}' erstellt")
except Error as e:
logger.error(f"Fehler beim Initialisieren der System-Tabelle: {e}")
if self.connection.is_connected():
self.connection.rollback()
raise
finally:
if cursor and cursor.is_connected():
cursor.close()
def _execute_query(self, query: str, params: tuple = None):
"""Führt eine SQL-Abfrage aus"""
cursor = None
try:
cursor = self.connection.cursor(dictionary=True)
cursor.execute(query, params)
return cursor
except Error as e:
logger.error(f"Fehler bei der Ausführung der Abfrage: {e}")
raise
finally:
if cursor:
cursor.close()
def _execute_select(self, query: str, params: tuple = None) -> List[Dict[str, Any]]:
"""Führt eine SELECT-Abfrage aus und gibt die Ergebnisse zurück"""
cursor = None
try:
cursor = self.connection.cursor(dictionary=True)
cursor.execute(query, params)
result = cursor.fetchall()
return result
except Error as e:
logger.error(f"Fehler bei der Ausführung der SELECT-Abfrage: {e}")
raise
finally:
if cursor:
cursor.close()
def _execute_insert(self, query: str, params: tuple = None) -> int:
"""Führt eine INSERT-Abfrage aus und gibt die ID des eingefügten Datensatzes zurück"""
cursor = None
try:
cursor = self.connection.cursor()
cursor.execute(query, params)
self.connection.commit()
return cursor.lastrowid
except Error as e:
logger.error(f"Fehler bei der Ausführung der INSERT-Abfrage: {e}")
self.connection.rollback()
raise
finally:
if cursor:
cursor.close()
def _execute_update(self, query: str, params: tuple = None) -> int:
"""Führt eine UPDATE-Abfrage aus und gibt die Anzahl der betroffenen Zeilen zurück"""
cursor = None
try:
cursor = self.connection.cursor()
cursor.execute(query, params)
self.connection.commit()
return cursor.rowcount
except Error as e:
logger.error(f"Fehler bei der Ausführung der UPDATE-Abfrage: {e}")
self.connection.rollback()
raise
finally:
if cursor:
cursor.close()
def _execute_delete(self, query: str, params: tuple = None) -> int:
"""Führt eine DELETE-Abfrage aus und gibt die Anzahl der gelöschten Zeilen zurück"""
cursor = None
try:
cursor = self.connection.cursor()
cursor.execute(query, params)
self.connection.commit()
return cursor.rowcount
except Error as e:
logger.error(f"Fehler bei der Ausführung der DELETE-Abfrage: {e}")
self.connection.rollback()
raise
finally:
if cursor:
cursor.close()
def _apply_record_filter(self, record_filter: Dict[str, Any] = None) -> str:
"""Erstellt eine WHERE-Klausel basierend auf dem Datensatzfilter"""
if not record_filter:
return "WHERE 1=1"
conditions = []
params = []
for field, value in record_filter.items():
conditions.append(f"{field} = %s")
params.append(value)
where_clause = "WHERE " + " AND ".join(conditions)
return where_clause, tuple(params)
def _get_context_filter(self) -> tuple:
"""Erstellt eine WHERE-Klausel für den Mandanten- und Benutzerkontext"""
return "WHERE mandate_id = %s", (self.mandate_id,)
# Public API
def get_tables(self, filter_criteria: Dict[str, Any] = None) -> List[str]:
"""
Gibt eine Liste aller verfügbaren Tabellen zurück.
Args:
filter_criteria: Optionale Filterkriterien (nicht implementiert)
Returns:
Liste der Tabellennamen
"""
query = """
SELECT table_name
FROM information_schema.tables
WHERE table_schema = %s
AND table_name NOT LIKE '\_%'
"""
try:
result = self._execute_select(query, (self.db_name,))
return [row["table_name"] for row in result]
except Exception as e:
logger.error(f"Fehler beim Abrufen der Tabellen: {e}")
return []
def get_fields(self, table: str, filter_criteria: Dict[str, Any] = None) -> List[str]:
"""
Gibt eine Liste aller Felder einer Tabelle zurück.
Args:
table: Name der Tabelle
filter_criteria: Optionale Filterkriterien (nicht implementiert)
Returns:
Liste der Feldnamen
"""
query = """
SELECT column_name
FROM information_schema.columns
WHERE table_schema = %s AND table_name = %s
"""
try:
result = self._execute_select(query, (self.db_name, table))
return [row["column_name"] for row in result]
except Exception as e:
logger.error(f"Fehler beim Abrufen der Felder für Tabelle {table}: {e}")
return []
def get_schema(self, table: str, language: str = None, filter_criteria: Dict[str, Any] = None) -> Dict[str, Dict[str, Any]]:
"""
Gibt ein Schema-Objekt für eine Tabelle zurück mit Datentypen und Labels.
Args:
table: Name der Tabelle
language: Sprache für die Labels (optional)
filter_criteria: Optionale Filterkriterien (nicht implementiert)
Returns:
Schema-Objekt mit Feldern, Datentypen und Labels
"""
query = """
SELECT
column_name,
data_type,
column_comment
FROM
information_schema.columns
WHERE
table_schema = %s AND table_name = %s
"""
schema = {}
try:
result = self._execute_select(query, (self.db_name, table))
for row in result:
field = row["column_name"]
data_type = row["data_type"]
comment = row["column_comment"]
# Label erstellen (Standardwert ist der Feldname)
label = field
# Wenn ein Kommentar existiert, verwende diesen als Label
if comment:
label = comment
schema[field] = {
"type": data_type,
"label": label
}
return schema
except Exception as e:
logger.error(f"Fehler beim Abrufen des Schemas für Tabelle {table}: {e}")
return {}
def get_recordset(self, table: str, field_filter: List[str] = None, record_filter: Dict[str, Any] = None) -> List[Dict[str, Any]]:
"""
Gibt eine Liste von Datensätzen aus einer Tabelle zurück, gefiltert nach Kriterien.
Args:
table: Name der Tabelle
field_filter: Filter für Felder (welche Felder zurückgegeben werden sollen)
record_filter: Filter für Datensätze (welche Datensätze zurückgegeben werden sollen)
Returns:
Liste der gefilterten Datensätze
"""
# Bestimme die Felder für die Abfrage
fields = "*"
if field_filter and isinstance(field_filter, list):
fields = ", ".join(field_filter)
# Basisbedingung ist der Mandantenkontext
base_where, base_params = self._get_context_filter()
# Wende zusätzliche Filterbedingungen an, wenn vorhanden
additional_where = ""
additional_params = ()
if record_filter:
additional_where, additional_params = self._apply_record_filter(record_filter)
# Entferne das "WHERE" am Anfang und ersetze es durch "AND"
additional_where = " AND " + additional_where[6:]
# Kombiniere die Bedingungen und Parameter
where_clause = base_where + additional_where
params = base_params + additional_params
# Erstelle die vollständige Abfrage
query = f"""
SELECT {fields} FROM {table} {where_clause}
"""
try:
return self._execute_select(query, params)
except Exception as e:
logger.error(f"Fehler beim Abrufen der Datensätze aus Tabelle {table}: {e}")
return []
def record_create(self, table: str, record_data: Dict[str, Any]) -> Dict[str, Any]:
"""
Erstellt einen neuen Datensatz in der Tabelle.
Args:
table: Name der Tabelle
record_data: Daten für den neuen Datensatz
Returns:
Der erstellte Datensatz
"""
# Füge mandate_id und user_id hinzu, falls nicht vorhanden oder 0
if "mandate_id" not in record_data or record_data["mandate_id"] == 0:
# Versuche, die initiale mandate_id aus der System-Tabelle zu holen
initial_mandate_id = self.get_initial_id("mandates")
if initial_mandate_id is not None:
record_data["mandate_id"] = initial_mandate_id
else:
record_data["mandate_id"] = 0
if "user_id" not in record_data or record_data["user_id"] == 0:
# Versuche, die initiale user_id aus der System-Tabelle zu holen
initial_user_id = self.get_initial_id("users")
if initial_user_id is not None:
record_data["user_id"] = initial_user_id
else:
record_data["user_id"] = 0
# Erstelle die Abfrage
fields = ", ".join(record_data.keys())
placeholders = ", ".join(["%s"] * len(record_data))
values = tuple(record_data.values())
query = f"""
INSERT INTO {table} ({fields})
VALUES ({placeholders})
"""
try:
# Prüfe zuerst, ob die Tabelle leer ist
check_query = f"""
SELECT COUNT(*) as count FROM {table}
"""
count_result = self._execute_select(check_query)
is_empty = count_result[0]["count"] == 0
# Führe die Abfrage aus und erhalte die ID des neuen Datensatzes
new_id = self._execute_insert(query, values)
# Wenn die Tabelle vorher leer war, registriere die neue ID als initiale ID
if is_empty and new_id:
self.register_initial_id(table, new_id)
logger.info(f"Initiale ID {new_id} für Tabelle {table} registriert")
# Füge die ID zum Datensatz hinzu, falls eine zurückgegeben wurde
if new_id:
record_data["id"] = new_id
return record_data
except Exception as e:
logger.error(f"Fehler beim Erstellen des Datensatzes in Tabelle {table}: {e}")
raise ValueError(f"Fehler beim Erstellen des Datensatzes in Tabelle {table}")
def record_delete(self, table: str, record_id: Union[str, int]) -> bool:
"""
Löscht einen Datensatz aus der Tabelle.
Args:
table: Name der Tabelle
record_id: ID des zu löschenden Datensatzes
Returns:
True bei Erfolg, False bei Fehler
"""
# Prüfe, ob es sich um die initiale ID handelt
initial_id = self.get_initial_id(table)
if initial_id is not None and initial_id == record_id:
logger.warning(f"Versuch, den initialen Datensatz mit ID {record_id} aus Tabelle {table} zu löschen, wurde verhindert")
return False
# Prüfe zuerst, ob der Datensatz zum aktuellen Mandanten gehört
check_query = f"""
SELECT mandate_id FROM {table} WHERE id = %s
"""
try:
result = self._execute_select(check_query, (record_id,))
if not result:
# Datensatz nicht gefunden
return False
if result[0]["mandate_id"] != self.mandate_id:
raise ValueError("Not your mandate")
# Lösche den Datensatz
delete_query = f"""
DELETE FROM {table} WHERE id = %s AND mandate_id = %s
"""
rows_affected = self._execute_delete(delete_query, (record_id, self.mandate_id))
return rows_affected > 0
except Exception as e:
logger.error(f"Fehler beim Löschen des Datensatzes aus Tabelle {table}: {e}")
return False
def record_modify(self, table: str, record_id: Union[str, int], record_data: Dict[str, Any]) -> Dict[str, Any]:
"""
Ändert einen Datensatz in der Tabelle.
Args:
table: Name der Tabelle
record_id: ID des zu ändernden Datensatzes
record_data: Neue Daten für den Datensatz
Returns:
Der aktualisierte Datensatz
"""
# Prüfe, ob es sich um die initiale ID handelt und die ID geändert werden soll
initial_id = self.get_initial_id(table)
if initial_id is not None and initial_id == record_id and "id" in record_data and record_data["id"] != record_id:
raise ValueError(f"Die ID des initialen Datensatzes in Tabelle {table} kann nicht geändert werden")
# Prüfe zuerst, ob der Datensatz zum aktuellen Mandanten gehört
check_query = f"""
SELECT mandate_id FROM {table} WHERE id = %s
"""
try:
result = self._execute_select(check_query, (record_id,))
if not result:
# Datensatz nicht gefunden
raise ValueError(f"Datensatz mit ID {record_id} nicht gefunden in Tabelle {table}")
if result[0]["mandate_id"] != self.mandate_id:
raise ValueError("Not your mandate")
# Erstelle die SET-Klausel und Parameter für das Update
set_clauses = []
values = []
for key, value in record_data.items():
set_clauses.append(f"{key} = %s")
values.append(value)
set_clause = ", ".join(set_clauses)
values.append(record_id) # Für die WHERE-Bedingung
values.append(self.mandate_id) # Für die mandate_id-Bedingung
# Aktualisiere den Datensatz
update_query = f"""
UPDATE {table}
SET {set_clause}
WHERE id = %s AND mandate_id = %s
"""
rows_affected = self._execute_update(update_query, tuple(values))
if rows_affected > 0:
# Lade den aktualisierten Datensatz
get_query = f"""
SELECT * FROM {table} WHERE id = %s
"""
updated_record = self._execute_select(get_query, (record_id,))
if updated_record:
return updated_record[0]
else:
raise ValueError(f"Fehler beim Abrufen des aktualisierten Datensatzes aus Tabelle {table}")
else:
raise ValueError(f"Fehler beim Aktualisieren des Datensatzes in Tabelle {table}")
except Exception as e:
logger.error(f"Fehler beim Aktualisieren des Datensatzes in Tabelle {table}: {e}")
raise
# System-Tabellen-Funktionen
def register_initial_id(self, table: str, initial_id: int) -> bool:
"""
Registriert die initiale ID für eine Tabelle.
Args:
table: Name der Tabelle
initial_id: Die initiale ID
Returns:
True bei Erfolg, False bei Fehler
"""
try:
# Prüfe zuerst, ob bereits eine initiale ID für diese Tabelle registriert ist
check_query = f"""
SELECT COUNT(*) as count
FROM {self._system_table_name}
WHERE table_name = %s
"""
result = self._execute_select(check_query, (table,))
if result and result[0]["count"] > 0:
# Bereits registriert
return True
# Registriere die initiale ID
insert_query = f"""
INSERT INTO {self._system_table_name} (table_name, initial_id)
VALUES (%s, %s)
"""
self._execute_insert(insert_query, (table, initial_id))
logger.info(f"Initiale ID {initial_id} für Tabelle {table} registriert")
return True
except Exception as e:
logger.error(f"Fehler beim Registrieren der initialen ID für Tabelle {table}: {e}")
return False
def get_initial_id(self, table: str) -> Optional[int]:
"""
Gibt die initiale ID für eine Tabelle zurück.
Args:
table: Name der Tabelle
Returns:
Die initiale ID oder None, wenn nicht vorhanden
"""
try:
query = f"""
SELECT initial_id
FROM {self._system_table_name}
WHERE table_name = %s
"""
result = self._execute_select(query, (table,))
if result and len(result) > 0:
return result[0]["initial_id"]
return None
except Exception as e:
logger.error(f"Fehler beim Abrufen der initialen ID für Tabelle {table}: {e}")
return None
def has_initial_id(self, table: str) -> bool:
"""
Prüft, ob eine initiale ID für eine Tabelle registriert ist.
Args:
table: Name der Tabelle
Returns:
True, wenn eine initiale ID registriert ist, sonst False
"""
try:
query = f"""
SELECT COUNT(*) as count
FROM {self._system_table_name}
WHERE table_name = %s
"""
result = self._execute_select(query, (table,))
if result and len(result) > 0:
return result[0]["count"] > 0
return False
except Exception as e:
logger.error(f"Fehler beim Prüfen der initialen ID für Tabelle {table}: {e}")
return False
def get_all_initial_ids(self) -> Dict[str, int]:
"""
Gibt alle registrierten initialen IDs zurück.
Returns:
Dictionary mit Tabellennamen als Schlüssel und initialen IDs als Werte
"""
try:
query = f"""
SELECT table_name, initial_id
FROM {self._system_table_name}
"""
result = self._execute_select(query)
initial_ids = {}
for row in result:
initial_ids[row["table_name"]] = row["initial_id"]
return initial_ids
except Exception as e:
logger.error(f"Fehler beim Abrufen aller initialen IDs: {e}")
return {}
def close(self):
"""Schließt die Datenbankverbindung"""
if hasattr(self, 'connection') and self.connection.is_connected():
self.connection.close()
logger.info("Datenbankverbindung geschlossen")