de.svws_nrw.db.utils.schema.DBSchemaManager Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of svws-db-utils Show documentation
Show all versions of svws-db-utils Show documentation
Diese Bibliothek unterstützt bei dem Zugriff auf Datenbanken für die Schulverwaltungssoftware in NRW
package de.svws_nrw.db.utils.schema;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import de.svws_nrw.config.SVWSKonfiguration;
import de.svws_nrw.config.SVWSKonfigurationException;
import de.svws_nrw.core.logger.Logger;
import de.svws_nrw.db.Benutzer;
import de.svws_nrw.db.DBConfig;
import de.svws_nrw.db.DBDriver;
import de.svws_nrw.db.DBEntityManager;
import de.svws_nrw.db.DBException;
import de.svws_nrw.db.dto.current.schema.DTOSchemaStatus;
import de.svws_nrw.db.schema.DBSchemaViews;
import de.svws_nrw.db.schema.Schema;
import de.svws_nrw.db.schema.SchemaRevisionen;
import de.svws_nrw.db.schema.SchemaTabelle;
import de.svws_nrw.db.schema.SchemaTabelleIndex;
import de.svws_nrw.db.schema.SchemaTabelleTrigger;
import de.svws_nrw.db.schema.View;
/**
* Diese Klasse stellt Hilfs-Funktionen zur Verfügung, um auf ein SVWS-Datenbank-Schema zuzugreifen und dieses zu bearbeiten.
*/
public final class DBSchemaManager {
/// Der Datenbank-Benutzer
private final Benutzer user;
/// Der Status des Datenbank-Schema
private final DBSchemaStatus status;
/// Ein Logger, um die Abläufe in dem Schema-Manager zu loggen
private final Logger logger;
/// Gibt an, ob die Ausführung von Operationen bei einzelnen Fehlern abgebrochen werden sollen.
private final boolean returnOnError;
/// Enthält ggf. einen Fehler-String für einen zuletzt aufgetretenen Fehler
private String lastError;
/** Der Updater, um Datenbank-Updates durchzuführen */
public final DBUpdater updater;
/** Der Backup-Manager, für den Import von und den Export nach SQLite */
public final DBBackupManager backup;
/** String-Konstanten, die wiederholt verwendet werden */
private static final String strOK = "[OK]";
private static final String strError = "[Fehler]";
/**
* Erstellt einen neuen DBSchema-Manager, der Schema-Operationen mithilfe des angegebenen Datenbank-Benutzer ermöglicht.
*
* @param user der Datenbak-Benutzer
* @param returnOnError gibt an, ob Operatioen bei Einzelfehlern abgebrochen werden sollen
* @param logger ein Logger, um die Abläufe in dem Schema-Manager zu loggen
*/
private DBSchemaManager(final Benutzer user, final boolean returnOnError, final Logger logger) {
this.user = user;
this.status = DBSchemaStatus.read(user);
this.returnOnError = returnOnError;
this.logger = logger;
this.updater = new DBUpdater(this, returnOnError);
this.backup = new DBBackupManager(this);
}
/**
* Versucht einen neuen DB-Schema-Manager zu erstellen.
*
* @param user der Datenbank-Benutzer
* @param returnOnError gibt an, ob Operatioen bei Einzelfehlern abgebrochen werden sollen
* @param logger ein Logger, um die Abläufe in dem Schema-Manager zu loggen
*
* @return der DB-Schema-Manager bei Erfolg
*/
public static DBSchemaManager create(final Benutzer user, final boolean returnOnError, final Logger logger) {
return new DBSchemaManager(user, returnOnError, (logger == null) ? new Logger() : logger);
}
/**
* Gibt den Datenbank-Benutzer des Managers zurück.
*
* @return der Datenbank-Benutzer
*/
public Benutzer getUser() {
return this.user;
}
/**
* Gibt den Namen des Schemas zurück.
*
* @return der Name des Schemas;
*/
public String getSchemaname() {
return this.status.getSchemaname();
}
/**
* Gibt den Schema-Status (see {@link DBSchemaStatus} zurück.
*
* @return der Schema-Status von diesem Schema-Manager
*/
public DBSchemaStatus getSchemaStatus() {
return this.status;
}
/**
* Gibt den vom Schema-Manager verwendeten Logger zurück.
*
* @return der Logger
*/
public Logger getLogger() {
return this.logger;
}
/**
* Gibt den zuletzt gesetzten Fehler einer Operation des Schema-Managers zurück.
*
* @return der zuletzt gesetzte Fehler
*/
public String getLastError() {
return lastError;
}
/**
* Führt die SQL-Skripte zum Erstellen aller Datenbank-Tabellen der angegebenen Schema-Revision
* aus.
*
* @param conn die Datenbank-Verbindung mit aktiver Transaktion
* @param revision die Revision des Datenbank-Schemas
*
* @return true, falls alle Tabellen erfolgreich erstellt wurden
*/
private boolean createAllTables(final DBEntityManager conn, final long revision) {
boolean result = true;
final var dbms = conn.getDBDriver();
for (final SchemaTabelle tab : Schema.getTabellen(revision)) {
logger.logLn(tab.name());
final String script = tab.getSQL(dbms, revision);
final boolean success = conn.transactionNativeUpdate(script) != Integer.MIN_VALUE;
conn.transactionFlush();
if (!success) {
result = false;
if (returnOnError)
break;
} else {
final List pkTrigger = tab.getPrimaerschluesselTriggerSQLList(dbms, revision, true);
if (!pkTrigger.isEmpty()) {
logger.logLn(" -> Erstelle Trigger für Auto-Inkremente");
for (final String scriptTrigger : pkTrigger) {
if (conn.transactionNativeUpdate(scriptTrigger) == Integer.MIN_VALUE) {
result = false;
if (returnOnError)
break;
}
conn.transactionFlush();
}
}
}
}
return result;
}
/**
* Führt die SQL-Skripte zum Erstellen aller Datenbank-Indizes der angegebenen Schema-Revision
* aus.
*
* @param conn die Datenbank-Verbindung mit aktiver Transaktion
* @param revision die Revision des Datenbank-Schemas
*
* @return true, falls alle Indizes erfolgreich erstellt wurden
*/
private boolean createAllIndizes(final DBEntityManager conn, final long revision) {
boolean result = true;
for (final SchemaTabelle tab : Schema.getTabellen(revision)) {
for (final SchemaTabelleIndex idx : tab.indizes(revision)) {
logger.logLn(idx.name());
final String script = idx.getSQL();
if (conn.transactionNativeUpdate(script) == Integer.MIN_VALUE) {
result = false;
if (returnOnError)
break;
}
conn.transactionFlush();
}
}
return result;
}
/**
* Erstellt die Views für das Schemas der angegebenen Schema-Revision.
*
* @param conn die Datenbank-Verbindung mit aktiver Transaktion
* @param revision die Revision des Datenbank-Schemas
*
* @return true, falls alle Skripte erfolgreich ausgeführt wurden
*/
private boolean executeSQLCreateViews(final DBEntityManager conn, final long revision) {
boolean result = true;
final List views = DBSchemaViews.getInstance().getViewsActive(revision);
for (final View view : views) {
logger.logLn(view.name);
if (conn.transactionNativeUpdate(view.getSQLCreate(conn.getDBDriver())) == Integer.MIN_VALUE) {
result = false;
if (returnOnError)
break;
}
conn.transactionFlush();
}
return result;
}
/**
* Führt die SQL-Finalisierungs-Skripte beim Erstellen eines Schemas der angegebenen Schema-Revision
* aus.
*
* @param conn die Datenbank-Verbindung mit aktiver Transaktion
* @param revision die Revision des Datenbank-Schemas
*
* @return true, falls alle Skripte erfolgreich ausgeführt wurden
*/
private boolean createDefaultSVWSBenutzer(final DBEntityManager conn, final long revision) {
boolean result = true;
final List sqlList = Schema.getCreateBenutzerSQL(revision);
for (final String sql : sqlList) {
logger.logLn(sql);
if (conn.transactionNativeUpdate(sql) == Integer.MIN_VALUE) {
result = false;
if (returnOnError)
break;
}
conn.transactionFlush();
}
return result;
}
/**
* Setzt die Datenbank-Revision auf die angegebene Revision. Dabei wird die Transaktion aus der übergebenen
* Datenbankverbindung genutzt.
*
* @param conn die Datenbankverbindung
* @param revision die zu setzende Revision, bei -1 wird die neueste Revision gesetzt
*
* @return true, falls die Revision erfolgreich gesetzt wurde, sonst false
*/
public static boolean setDBRevision(final DBEntityManager conn, final long revision) {
final long rev = (revision == -1) ? SchemaRevisionen.maxRevision.revision : revision;
if (rev == -1)
return false;
final DTOSchemaStatus oldObj = conn.querySingle(DTOSchemaStatus.class);
final DTOSchemaStatus newObj = new DTOSchemaStatus(rev, (rev > SchemaRevisionen.maxRevision.revision) || ((oldObj != null) && (oldObj.IsTainted)));
if (oldObj == null) {
if (!conn.transactionPersist(newObj))
return false;
} else {
if (!conn.transactionReplace(oldObj, newObj))
return false;
}
conn.transactionFlush();
return true;
}
/**
* Führt die SQL-Skripte zum Erstellen aller Datenbank-Trigger der angegebenen Schema-Revision
* aus, welche nicht zu den Auto-Inkrementen bei Primärschlüsseln gehören.
*
* Die Datenbank-Transaktion muss außerhalb dieser Methoden gehandhabt werden.
*
* @param conn die Datenbank-Verbindung
* @param logger der Logger
* @param revision die Revision des Datenbank-Schemas
* @param returnOnError gibt an, ob die Funktion bei einem Fehler sofort zurückkehrt oder nicht
*
* @return true, falls alle Trigger erfolgreich erstellt wurden
*/
public static boolean createAllTrigger(final DBEntityManager conn, final Logger logger, final long revision, final boolean returnOnError) {
logger.logLn("- Erstelle Trigger für die aktuelle DB-Revision... ");
logger.modifyIndent(2);
boolean result = true;
final var dbms = conn.getDBDriver();
for (final SchemaTabelle tab : Schema.getTabellen(revision)) {
for (final SchemaTabelleTrigger trig : tab.trigger()) {
if (!dbms.equals(trig.dbms()))
continue;
if (revision < trig.revision().revision)
continue;
if ((trig.veraltet().revision >= 0) && (revision >= trig.veraltet().revision))
continue;
logger.logLn(trig.name());
final String script = trig.getSQL(conn.getDBDriver(), true);
if (conn.transactionNativeUpdate(script) == Integer.MIN_VALUE) {
result = false;
if (returnOnError)
break;
}
}
}
conn.transactionFlush();
logger.modifyIndent(-2);
if (!result) {
logger.logLn(strError);
if (returnOnError)
return false;
}
logger.logLn(strOK);
return result;
}
/**
* Erstellt ein SVWS-Datenbank-Schema der angegebenen Revision
*
* @param conn die Datenbank-Verbindung zum Erstellen des Schemas
* @param revision die Revision für das SVWS-DB-Schema
* @param createUser gibt an, ob Default-SVWS-Benutzer angelegt werden sollen
* @param createTrigger gibt an, ob auch die Trigger für die Datenbank-Revision erstellt werden sollen
* (oder ob dies später seperat aufgerufen wird)
*
* @return true, wenn das Schema erfolgreich erstellt wurde, sonst false
*/
private boolean createSVWSSchema(final DBEntityManager conn, final long revision, final boolean createUser, final boolean createTrigger) {
logger.logLn("- Erstelle Tabellen für die aktuelle DB-Revision... ");
logger.modifyIndent(2);
boolean success = createAllTables(conn, revision);
logger.modifyIndent(-2);
if (!success) {
logger.logLn(strError);
if (returnOnError)
return false;
}
logger.logLn(strOK);
logger.logLn("- Erstelle Indizes für die aktuelle DB-Revision... ");
logger.modifyIndent(2);
success = createAllIndizes(conn, revision);
logger.modifyIndent(-2);
if (!success) {
logger.logLn(strError);
if (returnOnError)
return false;
}
logger.logLn(strOK);
if (createTrigger) {
logger.logLn("- Erstelle Trigger für die aktuelle DB-Revision... ");
logger.modifyIndent(2);
success = createAllTrigger(conn, logger, revision, returnOnError);
logger.modifyIndent(-2);
if (!success) {
logger.logLn(strError);
if (returnOnError)
return false;
}
logger.logLn(strOK);
}
logger.logLn("- Schreibe die Daten der Core-Types");
logger.modifyIndent(2);
success = updater.coreTypes.update(conn, false, revision);
conn.transactionFlush();
logger.modifyIndent(-2);
if (!success) {
logger.logLn(strError);
if (returnOnError)
return false;
}
logger.logLn(strOK);
logger.logLn("- Erstelle Views: ");
logger.modifyIndent(2);
success = executeSQLCreateViews(conn, revision);
logger.modifyIndent(-2);
if (!success) {
logger.logLn(strError);
if (returnOnError)
return false;
}
logger.logLn(strOK);
if (createUser) {
logger.logLn("- Lege Default-Benutzer an: ");
logger.modifyIndent(2);
success = createDefaultSVWSBenutzer(conn, revision);
logger.modifyIndent(-2);
if (!success) {
logger.logLn(strError);
if (returnOnError)
return false;
}
logger.logLn(strOK);
}
logger.logLn("- Setze die DB-Revision in der neu erzeugten Datenbank: ");
logger.modifyIndent(2);
success = setDBRevision(conn, revision);
logger.modifyIndent(-2);
if (!success) {
logger.logLn(strError);
if (returnOnError)
return false;
}
logger.logLn(strOK);
return true;
}
/**
* Erstellt ein SVWS-Datenbank-Schema der angegebenen Revision
*
* @param user der Datenbank-Benutzer über welchen eine Datenbank-Verbindung zum Erstellen des Schemas aufgebaut wird
* @param revision die Revision für das SVWS-DB-Schema
* @param createUser gibt an, ob Default-SVWS-Benutzer angelegt werden sollen
* @param createTrigger gibt an, ob auch die Trigger für die Datenbank-Revision erstellt werden sollen
* (oder ob dies später seperat aufgerufen wird)
*
* @throws DBException falls das Erstellen des Schemas fehlschlägt
*/
public void createSVWSSchema(final Benutzer user, final long revision, final boolean createUser, final boolean createTrigger) throws DBException {
logger.logLn("-> Erstelle in der Ziel-DB ein SVWS-Schema der Revision " + revision);
logger.modifyIndent(2);
boolean result = true;
try (DBEntityManager conn = user.getEntityManager()) {
try {
conn.transactionBegin();
result = createSVWSSchema(conn, revision, createUser, createTrigger);
if (!result)
throw new DBException("Fehler beim Erstellen des Schemas");
if (!conn.transactionCommit())
throw new DBException("Fehler beim Erstellen des Schemas - Datenbank-Transaktion konnte nicht abgeschlossen werden.");
} catch (final Exception e) {
logger.logLn(" " + strError);
if (e instanceof DBException)
throw e;
throw new DBException("Unerwarteter Fehler beim Erstellen des Schemas: " + e.getMessage());
} finally {
logger.modifyIndent(-2);
conn.transactionRollback();
}
}
logger.logLn(strOK);
}
/**
* Verwirft das SVWS-Datenbank-Schema, wenn das DBMS keine Unterstützung zum Verwerfen von
* mehreren Tabellen hat. Diese Implementierung wird für SQLite- und MDB-Access-Datenbanken verwendet.
*
* @param conn die Datenbankverbindung
* @param driver die Informationen zum verwendeten Datenbank-Treiber (s.o.)
*
* @return true, falls die Operationen erfolgreich waren und ansonsten false
*/
private boolean dropSVWSSchemaMultipleStatements(final DBEntityManager conn, final DBDriver driver) {
// no support to drop multiple tables in one drop statement - drop table order is important due to foreign key constraints
boolean success = true;
// Bestimme die aktuelle Revision der Datenbank
final DBSchemaVersion version = status.getVersion();
final long revision = version.getRevisionOrDefault(0);
final List tabellen = Schema.getTabellen(revision);
Collections.reverse(tabellen);
for (final SchemaTabelle tab : tabellen) {
// Prüfe bei einer Lecagy-Schild-DB, ob eine Tabelle für die Migration vorliegt - nur diese sollen verworfen werden
if (!version.isValid() && !tab.migrate())
continue;
if (!status.hasTable(tab.name()))
continue;
logger.log(tab.name() + "... ");
final String sql = "DROP TABLE " + ((driver == DBDriver.SQLITE) ? "IF EXISTS " : "") + tab.name() + ";";
final int result = conn.executeWithJDBCConnection(sql);
if (result == Integer.MIN_VALUE) {
logger.logLn(0, " " + strError);
success = false;
} else {
logger.logLn(0, " " + strOK);
}
}
// berücksichtige auch Tabellen, die bei der Revision eigentlich nicht definiert oder nicht für die Migration vorgemerkt sind
status.update();
for (final String tabname : status.getTabellen()) {
logger.log(tabname + "... ");
final String sql = "DROP TABLE " + ((driver == DBDriver.SQLITE) ? "IF EXISTS " : "") + tabname + ";";
final int result = conn.executeWithJDBCConnection(sql);
if (result == Integer.MIN_VALUE) {
logger.logLn(0, " " + strError);
success = false;
} else {
logger.logLn(0, " " + strOK);
}
}
return success;
}
/**
* Verwirft das SVWS-Datenbank-Schema.
*
* @return true, wenn das Schema erfolgreich verworfen wurde,
* false wenn dabei Fehler aufgetreten sind.
*/
public boolean dropSVWSSchema() {
try (DBEntityManager conn = user.getEntityManager()) {
boolean success = true;
logger.logLn("- Verwerfe Tabellen...");
logger.modifyIndent(2);
final DBDriver driver = conn.getDBDriver();
if ((driver == DBDriver.MDB) || (driver == DBDriver.SQLITE)) {
success = dropSVWSSchemaMultipleStatements(conn, driver);
} else if ((driver == DBDriver.MARIA_DB) || (driver == DBDriver.MYSQL)) {
// drop all existing in one statement - we do not need to cope with foreign key constraints
final List tableNames = status.getTabellen();
if (!tableNames.isEmpty()) {
logger.log("alle auf einmal... ");
final List sql = new ArrayList<>();
sql.add("SET foreign_key_checks = 0");
sql.add(status.getTabellen().stream().collect(Collectors.joining(",", "DROP TABLE IF EXISTS ", ";")));
sql.add("SET foreign_key_checks = 1");
try {
conn.executeBatchWithJDBCConnection(sql);
logger.logLn(0, " " + strOK);
} catch (final DBException e) {
e.printStackTrace();
logger.logLn(0, " " + strError);
success = false;
}
}
} else if (driver == DBDriver.MSSQL) {
// drop all existing in one statement - we do not need to cope with foreign key constraints
final List tableNames = status.getTabellen();
if (!tableNames.isEmpty()) {
logger.log("alle auf einmal... ");
final String sql = status.getTabellen().stream().collect(Collectors.joining(",", "DROP TABLE IF EXISTS ", ";"));
final int result = conn.executeWithJDBCConnection(sql);
if (result == Integer.MIN_VALUE) {
logger.logLn(0, " " + strError);
success = false;
} else {
logger.logLn(0, " " + strOK);
}
}
}
logger.modifyIndent(-2);
status.update();
return success;
}
}
/**
* Diese Methode erstellt in dem durch tgtConfig beschriebenen Schema ein SVWS-Schema der angebenen Revision.
*
* @param tgtConfig die Datenbank-Konfiguration für den Zugriff auf die SVWS-Server-Datenbank
* @param rev die Revision, bis zu welcher die Zieldatenbank aktualisiert wird
* @param logger ein Logger, welcher das Erstellen loggt.
* @param clearSchema gibt an, ob das Schema zuvor bereinigt werden sol
*
* @return true, falls das Erstellen erfolgreich durchgeführt wurde.
*/
private static boolean createAndUpdateSchemaInto(final DBConfig tgtConfig, final long rev, final Logger logger, final boolean clearSchema) {
final Benutzer schemaUser;
try {
schemaUser = Benutzer.create(tgtConfig);
} catch (@SuppressWarnings("unused") final DBException db) {
logger.logLn("Fehler beim Erstellen der Datenbankverbindung zum Ziel-Schema. Sind die Anmeldedaten korrekt?");
return false;
}
final DBSchemaManager manager = DBSchemaManager.create(schemaUser, true, logger);
// Schema leeren...
if ((clearSchema) && (!manager.dropSVWSSchema())) {
logger.logLn("Fehler beim Leeren des Schemas in der Ziel-Datenbank.");
return false;
}
logger.logLn("Erstelle das Schema zunächst in der Revision 0.");
logger.modifyIndent(2);
try (DBEntityManager conn = schemaUser.getEntityManager()) {
try {
conn.transactionBegin();
if (!manager.createSVWSSchema(conn, 0, true, true))
throw new DBException("Fehler beim Erstellen des Schemas");
if (!conn.transactionCommit())
throw new DBException("Fehler beim Erstellen des Schemas - Transaktion konnte nicht abgeschlossen werden");
} catch (final DBException e) {
logger.logLn(e.getMessage());
return false;
} catch (final Exception e) {
logger.logLn("Unerwarteter Fehler beim Erstellen des Schemas: " + e.getMessage());
return false;
} finally {
logger.modifyIndent(-2);
conn.transactionRollback();
}
}
logger.logLn("Aktualisiere das Schema schrittweise auf Revision " + rev + ".");
logger.modifyIndent(2);
if (!manager.updater.update(schemaUser, rev, false, true))
return false;
logger.modifyIndent(-2);
return true;
}
/**
* Diese Methode erstellt in der durch tgtConfig beschriebene SVWS-Server-Datenbank ein neues Schema.
*
* @param tgtConfig die Datenbank-Konfiguration für den Zugriff auf die SVWS-Server-Datenbank
* @param tgtRootUser der Benutzername eines Benutzers, der mit den Rechten zum Verwalten der Datenbankschemata ausgestattet ist.
* @param tgtRootPW das root-Kennwort für den Zugriff auf die Zieldatenbank
* @param maxUpdateRevision die Revision, bis zu welcher die Zieldatenbank aktualisiert wird
* @param logger ein Logger, welcher das Erstellen loggt.
*
* @return true, falls das Erstellen erfolgreich durchgeführt wurde.
*/
public static boolean createNewSchema(final DBConfig tgtConfig, final String tgtRootUser, final String tgtRootPW, final long maxUpdateRevision,
final Logger logger) {
final long max_revision = SchemaRevisionen.maxRevision.revision;
long rev = maxUpdateRevision;
if (rev < 0)
rev = max_revision;
if (rev > max_revision)
return false;
if (!DBMigrationManager.createNewTargetSchema(tgtConfig, tgtRootUser, tgtRootPW, logger))
return false;
return createAndUpdateSchemaInto(tgtConfig, rev, logger, false);
}
/**
* Diese Methode verwendet das durch tgtConfig beschriebene Datenbank-Schema, um ein leeres SVWS-Schema
* der angegebenen Revision anzulegen. Dabei werden evtl. zuvor bestehende Tabellen entfernt.
*
* @param tgtConfig die Datenbank-Konfiguration für den Zugriff auf das Datenbank-Schema
* @param maxUpdateRevision die Revision, bis zu welcher das SVWS-Schema aktualisiert wird
* @param logger ein Logger, welcher das Erstellen loggt.
*
* @return true, falls das SVWS-Schema erfolgreich erzeugt wurde.
*
* @throws SVWSKonfigurationException falls ein Fehler beim Zugriff auf die Konfiguration auftritt
*/
public static boolean recycleSchema(final DBConfig tgtConfig, final long maxUpdateRevision, final Logger logger) throws SVWSKonfigurationException {
final long max_revision = SchemaRevisionen.maxRevision.revision;
long rev = maxUpdateRevision;
if (rev < 0)
rev = max_revision;
if (rev > max_revision)
return false;
final boolean success = createAndUpdateSchemaInto(tgtConfig, rev, logger, true);
// Prüfe, ob das Datenbank-Schema bereits in der Konfiguration vorhanden ist oder nicht. Wenn nicht, dann füge es hinzu
final DBConfig tmp = SVWSKonfiguration.get().getDBConfig(tgtConfig.getDBSchema());
if (tmp == null)
SVWSKonfiguration.get().createOrUpdateSchema(tgtConfig.getDBSchema(), tgtConfig.getUsername(), tgtConfig.getPassword(), false);
return success;
}
}