Skip to content

Initial Data Model for Schema SQL Generation #1481

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 13, 2023
1 change: 0 additions & 1 deletion spring-data-jdbc/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

<artifactId>spring-data-jdbc</artifactId>
<version>3.1.0-756-SNAPSHOT</version>

<name>Spring Data JDBC</name>
<description>Spring Data module for JDBC repositories.</description>
<url>https://projects.spring.io/spring-data-jdbc</url>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert;

import java.util.Iterator;

/**
* {@link MappingContext} implementation.
*
Expand Down Expand Up @@ -101,5 +103,4 @@ protected RelationalPersistentProperty createPersistentProperty(Property propert
public NamingStrategy getNamingStrategy() {
return this.namingStrategy;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.relational.core.mapping.schemasqlgeneration;

import java.util.HashMap;

public class BaseTypeMapper {

final HashMap<Class<?>,String> mapClassToDatabaseType = new HashMap<Class<?>,String>();

public BaseTypeMapper() {
mapClassToDatabaseType.put(String.class, "VARCHAR(255)");
mapClassToDatabaseType.put(Boolean.class, "TINYINT");
mapClassToDatabaseType.put(Double.class, "DOUBLE");
mapClassToDatabaseType.put(Float.class, "FLOAT");
mapClassToDatabaseType.put(Integer.class, "INT");
}
public String databaseTypeFromClass(Class<?> type) {
return mapClassToDatabaseType.get(type);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.relational.core.mapping.schemasqlgeneration;

import org.springframework.data.relational.core.sql.SqlIdentifier;

import java.io.Serial;
import java.io.Serializable;

/**
* Class that models a Column for generating SQL for Schema generation.
*
* @author Kurt Niemi
*/
public class ColumnModel implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private final SqlIdentifier name;
private final String type;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decided to simplify the model to be a String value. Whatever is in here is what gets in the generated SQL.

private final boolean nullable;

public ColumnModel(SqlIdentifier name, String type, boolean nullable) {
this.name = name;
this.type = type;
this.nullable = nullable;
}

public ColumnModel(SqlIdentifier name, String type) {
this.name = name;
this.type = type;
this.nullable = false;
}

public SqlIdentifier getName() {
return name;
}

public String getType() {
return type;
}

public boolean isNullable() {
return nullable;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.relational.core.mapping.schemasqlgeneration;

import java.io.Serial;
import java.io.Serializable;

/**
* Class that models a Foreign Key relationship for generating SQL for Schema generation.
*
* @author Kurt Niemi
*/
public class ForeignKeyColumnModel implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private final TableModel foreignTable;
private final ColumnModel foreignColumn;
private final ColumnModel column;

public ForeignKeyColumnModel(TableModel foreignTable, ColumnModel foreignColumn, ColumnModel column) {
this.foreignTable = foreignTable;
this.foreignColumn = foreignColumn;
this.column = column;
}

public TableModel getForeignTable() {
return foreignTable;
}

public ColumnModel getForeignColumn() {
return foreignColumn;
}

public ColumnModel getColumn() {
return column;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.relational.core.mapping.schemasqlgeneration;

import org.springframework.data.relational.core.mapping.BasicRelationalPersistentProperty;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;

import java.io.Serial;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

/**
* Model class that contains Table/Column information that can be used
* to generate SQL for Schema generation.
*
* @author Kurt Niemi
*/
public class SchemaSQLGenerationDataModel implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private final List<TableModel> tableData = new ArrayList<TableModel>();
BaseTypeMapper typeMapper;

/**
* Default constructor so that we can deserialize a model
*/
public SchemaSQLGenerationDataModel() {
}

/**
* Create model from a RelationalMappingContext
*/
public SchemaSQLGenerationDataModel(RelationalMappingContext context) {

if (typeMapper == null) {
typeMapper = new BaseTypeMapper();
}

for (RelationalPersistentEntity entity : context.getPersistentEntities()) {
TableModel tableModel = new TableModel(entity.getTableName());

Iterator<BasicRelationalPersistentProperty> iter =
entity.getPersistentProperties(Column.class).iterator();

while (iter.hasNext()) {
BasicRelationalPersistentProperty p = iter.next();
ColumnModel columnModel = new ColumnModel(p.getColumnName(),
typeMapper.databaseTypeFromClass(p.getActualType()),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@schauder - I know initially I had wanted to modify the @column annotation to have a gazillion modifiers :-) to match what was in JPA.

After implementing the default behavior (and the change to the Column model) - I think adding just one annotation (databaseType) would be sufficient. We would let developers specify whatever they want there, and that is exactly what would be in the generated SQL.

If the user wishes to change our defaults, they can optionally subclass BaseTypeMapper. I stayed with what I think are datatypes that are supported by all Databases.

Will add more comments to the code - to document/describe to users how to extend/change behavior.

true);
tableModel.getColumns().add(columnModel);
}
tableData.add(tableModel);
}
}

public List<TableModel> getTableData() {
return tableData;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.relational.core.mapping.schemasqlgeneration;

import org.springframework.data.relational.core.sql.IdentifierProcessing;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

public class SchemaSQLGenerator {

private final IdentifierProcessing identifierProcssing;
public SchemaSQLGenerator(IdentifierProcessing identifierProcessing) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@schauder -

Is there a way to get the IdentifierProcessing from the RelationalMappingContext? (or when user's use this class to generate SQL - that they can easily get an IdentifierProcessing object - that matches the one they are using for executing SQL)?

For the Unit Test(s) I think passing it in gives flexibility.

this.identifierProcssing = identifierProcessing;
}

List<List<TableModel>> reorderTablesInHierarchy(SchemaSQLGenerationDataModel dataModel) {

// ::TODO:: Take Parent/Child relationships into account (i.e if a child table has
// a Foreign Key to a table, that parent table needs to be created first.

// For now this method will simple put the tables in the same level
List<List<TableModel>> orderedTables = new ArrayList<List<TableModel>>();
List<TableModel> tables = new ArrayList<TableModel>();

for (TableModel table : dataModel.getTableData()) {
tables.add(table);
}
orderedTables.add(tables);

return orderedTables;
}

HashMap<Class<?>,String> mapClassToSQLType = null;

public String generateSQL(ColumnModel column) {

StringBuilder sql = new StringBuilder();
sql.append(column.getName().toSql(identifierProcssing));
sql.append(" ");

sql.append(column.getType());

if (!column.isNullable()) {
sql.append(" NOT NULL");
}

return sql.toString();
}

public String generatePrimaryKeySQL(TableModel table) {
// ::TODO:: Implement
return "";
}

public String generateForeignKeySQL(TableModel table) {
// ::TODO:: Implement
return "";
}

public String generateSQL(TableModel table) {

StringBuilder sql = new StringBuilder();

sql.append("CREATE TABLE ");
sql.append(table.getName().toSql(identifierProcssing));
sql.append(" (");

int numColumns = table.getColumns().size();
for (int i=0; i < numColumns; i++) {
sql.append(generateSQL(table.getColumns().get(i)));
if (i != numColumns-1) {
sql.append(",");
}
}

sql.append(generatePrimaryKeySQL(table));
sql.append(generateForeignKeySQL(table));

sql.append(" );");

return sql.toString();
}

public String generateSQL(SchemaSQLGenerationDataModel dataModel) {

StringBuilder sql = new StringBuilder();
List<List<TableModel>> orderedTables = reorderTablesInHierarchy(dataModel);

for (List<TableModel> tables : orderedTables) {
for (TableModel table : tables) {
String tableSQL = generateSQL(table);
sql.append(tableSQL + "\n");
}
}

return sql.toString();
}
}
Loading