eu.miltema.slimdbsync.SchemaGenerator Maven / Gradle / Ivy
package eu.miltema.slimdbsync;
import eu.miltema.slimdbsync.def.*;
import eu.miltema.slimdbsync.pg.PgAdapter;
import eu.miltema.slimorm.*;
import java.util.*;
import java.util.function.Consumer;
import static java.util.stream.Collectors.*;
import javax.persistence.*;
import java.sql.Statement;
public class SchemaGenerator {
private Database db;
private DatabaseAdapter dbAdapter;
private SyncContext ctx;
private Consumer logger = message -> {};
private List messageElements = new ArrayList();//elements for debug messages
private boolean dropUnused = true;
public SchemaGenerator(Database db) {
this.db = db;
this.dbAdapter = new PgAdapter(db.getSchema());
this.ctx = new SyncContext();
}
public SchemaGenerator setLogger(Consumer logger) {
this.logger = logger;
return this;
}
/**
* Synchronize database tables according to entity classes
* @param entityClasses entity classes
* @throws SchemaUpdateException when schema updating fails
*/
public void sync(Class> ... entityClasses) throws SchemaUpdateException {
try {
initModelTables(entityClasses);
loadCurrentSchema();
StringBuilder sb = new StringBuilder();
detectChanges(sb);
applyChanges(sb.toString());
}
catch(SchemaUpdateException sue) {
throw sue;
}
catch(Exception e) {
throw new SchemaUpdateException(e);
}
}
private void initModelTables(Class>[] entityClasses) throws SchemaUpdateException {
ctx.modelSequenceNames = new HashSet<>();
ctx.modelTables = new HashMap<>();
ctx.modelPrimaryKeys = new HashMap<>();
for(Class> clazz : entityClasses) {
EntityProperties eprop = db.getDialect().getProperties(clazz);
TableDef table = new TableDef();
table.name = eprop.tableName;
table.columns = eprop.fields.stream().map(fprop -> {
ModelColumnDef c = new ModelColumnDef(eprop, fprop, dbAdapter);
if (c.sourceSequence != null)
ctx.modelSequenceNames.add(c.sourceSequence);
return c;
}).collect(toMap(c -> c.name, c -> c));
ctx.modelTables.put(table.name, table);
if (eprop.idField != null)
ctx.modelPrimaryKeys.put(table.name, new PrimaryKeyDef(table.name, eprop.idField.columnName, null));
}
initModelForeignKeys(entityClasses);
initModelUniques(entityClasses);
initModelChecks(entityClasses);
initModelIndexes(entityClasses);
}
private void initModelForeignKeys(Class>[] entityClasses) {
ctx.modelForeignKeys = new HashMap<>();
for(Class> clazz : entityClasses) {
EntityProperties eprops = db.getDialect().getProperties(clazz);
eprops.fields.stream().forEach(f -> {
ModelColumnDef coldef = (ModelColumnDef) ctx.modelTables.get(eprops.tableName).columns.get(f.columnName);
if (coldef.isForeignKey) {
Class> targetClass = f.fieldType;
EntityProperties target = db.getDialect().getProperties(targetClass);
EntityProperties targetProps = db.getDialect().getProperties(targetClass);
if (targetProps == null)
throw new SchemaUpdateException(f.field, ": @ManyToOne target class " + targetClass.getName() + " not registered with SchemaGenerator");
if (target.idField == null)
throw new SchemaUpdateException(f.field, ": @ManyToOne target class " + targetClass.getName() + " does not declare id-field");
coldef.type = ctx.modelTables.get(targetProps.tableName).columns.values().stream().filter(fcoldef -> fcoldef.isPrimaryKey()).map(fcoldef -> fcoldef.type).findAny().orElse(null);
ForeignKeyDef fdef = new ForeignKeyDef(eprops.tableName, f.columnName, target.tableName, target.idField.columnName, null);
ctx.modelForeignKeys.put(fdef.localTable + "/" + fdef.localColumn, fdef);
}
});
}
}
private void initModelUniques(Class>[] entityClasses) {
ctx.modelUniques = new HashMap<>();
for(Class> clazz : entityClasses) {
EntityProperties eprops = db.getDialect().getProperties(clazz);
// Add fields with @Column(unique=true)
ctx.modelTables.get(eprops.tableName).columns.values().stream().map(coldef -> (ModelColumnDef) coldef).filter(coldef -> coldef.isUnique).forEach(coldef -> {
UniqueDef udef = new UniqueDef();
udef.tableName = eprops.tableName;
udef.columns = new String[] {coldef.name};
ctx.modelUniques.put(udef.toString(), udef);
});
// Add fields with @Table(uniqueConstraints = @UniqueConstraint(columnNames= {"abc", "xyz"}))
if (clazz.isAnnotationPresent(Table.class)) {
UniqueConstraint[] uca = clazz.getAnnotation(Table.class).uniqueConstraints();
if (uca != null)
for(UniqueConstraint uc : uca)
if (uc.columnNames() != null && uc.columnNames().length > 0) {
UniqueDef udef = new UniqueDef();
udef.tableName = eprops.tableName;
udef.columns = uc.columnNames();
ctx.modelUniques.put(udef.toString(), udef);
}
}
}
}
private void initModelChecks(Class>[] entityClasses) {
ctx.modelChecks = new HashMap<>();
for(Class> clazz : entityClasses) {
EntityProperties eprop = db.getDialect().getProperties(clazz);
eprop.fields.stream().filter(fprop -> fprop.fieldType.isEnum()).forEach(fprop -> {
CheckDef cdef = new CheckDef(null, eprop.tableName, fprop.columnName);
cdef.validValues = Arrays.stream(fprop.fieldType.getEnumConstants()).map(c -> c.toString()).toArray(String[]::new);
ctx.modelChecks.put(cdef.toString(), cdef);
});
}
}
private void initModelIndexes(Class>[] entityClasses) {
ctx.modelIndexes = new HashMap<>();
for(Class> clazz : entityClasses) {
EntityProperties eprops = db.getDialect().getProperties(clazz);
if (clazz.isAnnotationPresent(Indexes.class)) {
Index[] indexes = clazz.getAnnotation(Indexes.class).value();
if (indexes != null && indexes.length > 0)
for(Index index : indexes)
if (index.value() != null && index.value().length > 0) {
IndexDef idef = new IndexDef();
idef.tableName = eprops.tableName;
idef.columns = index.value();
ctx.modelIndexes.put(idef.toString(), idef);
}
}
}
}
private void loadCurrentSchema() throws Exception {
ctx.dbSequenceNames = dbAdapter.loadCurrentSequenceNames(db);
ctx.dbTables = dbAdapter.loadCurrentTables(db).stream().collect(toMap(def -> def.name, def -> def));
ctx.dbPrimaryKeys = dbAdapter.loadCurrentPrimaryKeys(db).stream().collect(toMap(pk -> pk.table, pk -> pk));
ctx.dbForeignKeys = dbAdapter.loadCurrentForeignKeys(db).stream().collect(toMap(fk -> fk.localTable + "/" + fk.localColumn, fk -> fk));
ctx.dbUniques = dbAdapter.loadCurrentUniques(db).stream().collect(toMap(u -> u.toString(), u -> u));
ctx.dbChecks = dbAdapter.loadCurrentChecks(db).stream().collect(toMap(u -> u.toString(), u -> u));
ctx.dbIndexes = dbAdapter.loadCurrentIndexes(db).stream().collect(toMap(u -> u.toString(), u -> u));
}
private void detectChanges(StringBuilder sb) {
detectNewSequences(sb);
detectNewTables(sb);
ctx.modelTables.values().stream().filter(table -> ctx.dbTables.containsKey(table.name)).forEach(table -> {
detectNewColumns(table, sb);
detectChangedColumns(table, sb);
if (dropUnused) detectRemovedColumns(table, sb);
});
detectNewPrimaryKeys(sb);
detectRemovedPrimaryKeys(sb);
detectNewForeignKeys(sb);
detectRemovedForeignKeys(sb);
detectNewUniques(sb);
detectRemovedUniques(sb);
detectNewChecks(sb);
detectRemovedChecks(sb);
detectNewIndexes(sb);
detectRemovedIndexes(sb);
if (dropUnused) detectRemovedTables(sb);
if (dropUnused) detectRemovedSequences(sb);
}
private void detectNewForeignKeys(StringBuilder sb) {
ctx.modelForeignKeys.keySet().stream().
filter(mfname -> !ctx.dbForeignKeys.containsKey(mfname)).
map(mfname -> ctx.modelForeignKeys.get(mfname)).
forEach(mfk -> sb.append(dbAdapter.createForeignKey(mfk)));
}
private void detectRemovedForeignKeys(StringBuilder sb) {
ctx.dbForeignKeys.keySet().stream().
filter(dbfname -> !ctx.modelForeignKeys.containsKey(dbfname)).
map(dbfname -> ctx.dbForeignKeys.get(dbfname)).
forEach(dbf -> sb.append(dbAdapter.dropForeignKey(dbf.localTable, dbf.localColumn, dbf.constraintName)));
}
private void detectNewUniques(StringBuilder sb) {
ctx.modelUniques.keySet().stream().
filter(uname -> !ctx.dbUniques.containsKey(uname)).
map(uname -> ctx.modelUniques.get(uname)).
forEach(udef -> sb.append(dbAdapter.createUnique(udef)));
}
private void detectRemovedUniques(StringBuilder sb) {
ctx.dbUniques.keySet().stream().
filter(uname -> !ctx.modelUniques.containsKey(uname)).
map(uname -> ctx.dbUniques.get(uname)).
forEach(uudef -> sb.append(dbAdapter.dropUnique(uudef)));
}
private void detectNewChecks(StringBuilder sb) {
ctx.modelChecks.keySet().stream().
filter(cname -> !ctx.dbChecks.containsKey(cname)).
map(cname -> ctx.modelChecks.get(cname)).
forEach(cdef -> sb.append(dbAdapter.createCheck(cdef)));
}
private void detectRemovedChecks(StringBuilder sb) {
ctx.dbChecks.keySet().stream().
filter(cname -> !ctx.modelChecks.containsKey(cname)).
map(cname -> ctx.dbChecks.get(cname)).
forEach(cdef -> sb.append(dbAdapter.dropCheck(cdef)));
}
private void detectNewIndexes(StringBuilder sb) {
ctx.modelIndexes.keySet().stream().
filter(iname -> !ctx.dbIndexes.containsKey(iname)).
map(iname -> ctx.modelIndexes.get(iname)).
forEach(idef -> sb.append(dbAdapter.createIndex(idef)));
}
private void detectRemovedIndexes(StringBuilder sb) {
ctx.dbIndexes.keySet().stream().
filter(iname -> !ctx.modelIndexes.containsKey(iname)).
map(iname -> ctx.dbIndexes.get(iname)).
forEach(idef -> sb.append(dbAdapter.dropIndex(idef)));
}
private void detectRemovedPrimaryKeys(StringBuilder sb) {
for(PrimaryKeyDef pk : ctx.dbPrimaryKeys.values()) {
TableDef table = ctx.modelTables.get(pk.table);
if (table == null)
continue;//a table was dropped: pk will be implicitly cascade-dropped
if (!table.columns.containsKey(pk.column))
continue;//pk column was removed; pk will be implicitly cascade-dropped
if (table.columns.get(pk.column).isPrimaryKey())
continue;//column is still primary key; don' drop the constraint
sb.append(dbAdapter.dropPrimaryKey(pk.table, pk.column, pk.constraintName));
}
}
private String getPrimaryKeyColumn(TableDef table) {
return table.columns.values().stream().filter(c -> c.isPrimaryKey()).map(c -> c.name).findAny().orElse(null);
}
private void detectNewSequences(StringBuilder sb) {
ctx.modelSequenceNames.stream().
filter(s -> !ctx.dbSequenceNames.contains(s)).
peek(s -> messageElements.add(s)).
forEach(s -> sb.append(dbAdapter.createSequence(s)));
logElementsMessage("Added sequences ");
}
private void detectNewTables(StringBuilder sb) {
ctx.modelTables.values().stream().
filter(table -> !ctx.dbTables.containsKey(table.name)).
peek(table -> messageElements.add(table.name)).
forEach(table -> sb.append(dbAdapter.createTableWithColumns(table)));
logElementsMessage("Added tables ");
}
private void detectNewColumns(TableDef newTable, StringBuilder sb) {
Map existingCols = ctx.dbTables.get(newTable.name).columns;
newTable.columns.values().stream().
filter(col -> !existingCols.containsKey(col.name)).
peek(col -> messageElements.add(col.name)).
forEach(col -> sb.append(dbAdapter.addColumn(newTable.name, col)));
logElementsMessage("Added " + newTable.name + " columns ");
}
private void detectChangedColumns(TableDef newTable, StringBuilder sb) {
Map existingCols = ctx.dbTables.get(newTable.name).columns;
newTable.columns.values().stream().
filter(col -> existingCols.containsKey(col.name)).
filter(col -> col.columnDefinitionOverride == null). // if manual column definition is present, then we won't manage changes, because new/existing comparison is inaccurate
forEach(col -> {
ColumnDef col2 = existingCols.get(col.name);
if (!Objects.equals(col.type, col2.type))
sb.append(dbAdapter.alterColumnType(newTable.name, col.name, col.type));
if (col.isNullable != col2.isNullable)
sb.append(dbAdapter.alterColumnNullability(newTable.name, col.name, col.isNullable));
if (!Objects.equals(col.sourceSequence, col2.sourceSequence))
sb.append(dbAdapter.alterColumnDefaultValue(newTable.name, col.name, col.sourceSequence));
});
}
private void detectRemovedColumns(TableDef newTable, StringBuilder sb) {
ctx.dbTables.get(newTable.name).columns.keySet().stream().
filter(col -> !newTable.columns.containsKey(col)).
peek(col -> messageElements.add(col)).
forEach(col -> sb.append(dbAdapter.dropColumn(newTable.name, col)));
logElementsMessage("Removed " + newTable.name + " columns ");
}
private void detectNewPrimaryKeys(StringBuilder sb) {
ctx.modelTables.values().stream().forEach(table -> {
String pkColumn = getPrimaryKeyColumn(table);
if (pkColumn != null) {
PrimaryKeyDef existingKey = ctx.dbPrimaryKeys.get(table.name);
if (existingKey == null || !Objects.equals(existingKey.column, pkColumn))
sb.append(dbAdapter.addPrimaryKey(table.name, pkColumn));
}
});
}
private void detectRemovedTables(StringBuilder sb) {
ctx.dbTables.keySet().stream().
filter(table -> !ctx.modelTables.containsKey(table)).
peek(table -> messageElements.add(table)).
forEach(table -> sb.append(dbAdapter.dropTable(table)));
logElementsMessage("Removed tables ");
}
private void detectRemovedSequences(StringBuilder sb) {
ctx.dbSequenceNames.stream().
filter(seq -> !ctx.modelSequenceNames.contains(seq)).
peek(seq -> messageElements.add(seq)).
forEach(seq -> sb.append(dbAdapter.dropSequence(seq)));
logElementsMessage("Removed sequences ");
}
private void logElementsMessage(String messagePrefix) {
if (messageElements.isEmpty())
return;
logger.accept(messagePrefix + messageElements.stream().collect(joining(", ")));
messageElements.clear();
}
protected void applyChanges(String ddlChanges) throws Exception {
db.transaction((db, connection) -> {
try(Statement stmt = connection.createStatement()) {
stmt.executeUpdate(ddlChanges);
}
return null;
});
}
public SchemaGenerator dropUnused(boolean b) {
dropUnused = b;
return this;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy