[Editor] Add support for generating DAOs
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
package com.bartlomiejpluta.base.editor.code.build.generator
|
||||
|
||||
import com.bartlomiejpluta.base.editor.database.model.schema.ColumnType
|
||||
import com.bartlomiejpluta.base.editor.database.model.schema.SchemaColumn
|
||||
import com.bartlomiejpluta.base.editor.database.model.schema.SchemaTable
|
||||
import com.bartlomiejpluta.base.editor.database.service.DatabaseService
|
||||
import com.bartlomiejpluta.base.editor.project.context.ProjectContext
|
||||
import com.bartlomiejpluta.base.editor.project.model.Project
|
||||
import com.squareup.javapoet.*
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.stereotype.Component
|
||||
import java.time.Instant
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.*
|
||||
import javax.annotation.processing.Generated
|
||||
import javax.lang.model.element.Modifier
|
||||
|
||||
@Component
|
||||
class DataAccessObjectCodeGenerator : CodeGenerator {
|
||||
|
||||
@Autowired
|
||||
private lateinit var projectContext: ProjectContext
|
||||
|
||||
@Autowired
|
||||
private lateinit var databaseService: DatabaseService
|
||||
|
||||
override fun generate() {
|
||||
projectContext.project?.let { project ->
|
||||
databaseService.database.tables.forEach {
|
||||
handleTable(project, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleTable(project: Project, table: SchemaTable) {
|
||||
val model = generateModel(project, table)
|
||||
generateDAO(project, table, model)
|
||||
}
|
||||
|
||||
private fun generateModel(project: Project, table: SchemaTable): ClassName {
|
||||
val className = ClassName.get(MODEL_PACKAGE, "${snakeToPascalCase(table.name)}Model")
|
||||
|
||||
val builderAnnotation = AnnotationSpec
|
||||
.builder(ClassName.get("lombok", "Builder"))
|
||||
.build()
|
||||
|
||||
val dataAnnotation = AnnotationSpec
|
||||
.builder(ClassName.get("lombok", "Data"))
|
||||
.build()
|
||||
|
||||
val generatedAnnotation = AnnotationSpec.builder(Generated::class.java).addMember("value", "\$S", GENERATOR_NAME)
|
||||
.addMember("date", "\$S", DateTimeFormatter.ISO_INSTANT.format(Instant.now()))
|
||||
.addMember("comments", "\$S", "Model generated for '${table.name}' database table")
|
||||
.build()
|
||||
|
||||
val generatedClass = TypeSpec.classBuilder(className)
|
||||
.addAnnotation(dataAnnotation)
|
||||
.addAnnotation(builderAnnotation)
|
||||
.addAnnotation(generatedAnnotation)
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
|
||||
|
||||
table.columns.forEach {
|
||||
generatedClass.addField(dbToJavaType(it), snakeToCamelCase(it.name), Modifier.PRIVATE, Modifier.FINAL)
|
||||
}
|
||||
|
||||
JavaFile
|
||||
.builder(MODEL_PACKAGE, generatedClass.build())
|
||||
.build()
|
||||
.writeTo(project.buildGeneratedCodeDirectory)
|
||||
|
||||
return className
|
||||
}
|
||||
|
||||
private fun generateDAO(project: Project, table: SchemaTable, model: ClassName) {
|
||||
val packageName = "com.bartlomiejpluta.base.generated.db.dao"
|
||||
val className = ClassName.get(packageName, "${snakeToPascalCase(table.name)}DAO")
|
||||
|
||||
val generatedAnnotation = AnnotationSpec.builder(Generated::class.java).addMember("value", "\$S", GENERATOR_NAME)
|
||||
.addMember("date", "\$S", DateTimeFormatter.ISO_INSTANT.format(Instant.now()))
|
||||
.addMember("comments", "\$S", "Data Access Object generated for '${table.name}' database table")
|
||||
.build()
|
||||
|
||||
val requiredArgsConstructorAnnotation = AnnotationSpec
|
||||
.builder(ClassName.get("lombok", "RequiredArgsConstructor"))
|
||||
.build()
|
||||
|
||||
val primaryKeys = table.columns.filter { it.primary }
|
||||
|
||||
if (primaryKeys.isEmpty()) {
|
||||
throw IllegalStateException("Table '${table.name}' does not define any primary key")
|
||||
}
|
||||
|
||||
if (primaryKeys.size > 1) {
|
||||
throw IllegalStateException("Table '${table.name}' defines ${primaryKeys.size} primary keys, whereas only 1 is allowed")
|
||||
}
|
||||
|
||||
val primaryKey = primaryKeys[0]
|
||||
|
||||
val generatedClass = TypeSpec.classBuilder(className)
|
||||
.addAnnotation(generatedAnnotation)
|
||||
.addAnnotation(requiredArgsConstructorAnnotation)
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
|
||||
.addField(
|
||||
ClassName.get("com.bartlomiejpluta.base.api.context", "Context"),
|
||||
"context",
|
||||
Modifier.PRIVATE,
|
||||
Modifier.FINAL
|
||||
)
|
||||
.addMethod(
|
||||
MethodSpec.methodBuilder("find")
|
||||
.addParameter(dbToJavaType(primaryKey), "id")
|
||||
.returns(model)
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
|
||||
.beginControlFlow("return context.withDatabase(db ->")
|
||||
.addStatement("var statement = db.prepareStatement(\"SELECT * FROM `${table.name}` WHERE `${primaryKey.name}` = ?\")")
|
||||
.addStatement("statement.${dbToBindMethod(primaryKey)}(1, id)")
|
||||
.addStatement("var result = statement.executeQuery()")
|
||||
.beginControlFlow("if(result.next())")
|
||||
.addStatement("var model = ${model.simpleName()}.builder()")
|
||||
.apply {
|
||||
table.columns.forEach { column ->
|
||||
addStatement("model.${snakeToCamelCase(column.name)}(result.${dbToGetMethod(column)}(\"${column.name}\"))")
|
||||
}
|
||||
}
|
||||
.addStatement("return model.build()")
|
||||
.endControlFlow()
|
||||
.addStatement("throw new RuntimeException(\"No [${model.simpleName()}] found with [${primaryKey.name}] == [\" + id + \"]\")")
|
||||
.endControlFlow()
|
||||
.addStatement(")")
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
|
||||
JavaFile
|
||||
.builder(DAO_PACKAGE, generatedClass)
|
||||
.build()
|
||||
.writeTo(project.buildGeneratedCodeDirectory)
|
||||
|
||||
}
|
||||
|
||||
private fun snakeToPascalCase(snake: String) = snake
|
||||
.lowercase()
|
||||
.split(Regex("_+"))
|
||||
.joinToString("") { w -> w.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } }
|
||||
|
||||
private fun snakeToCamelCase(snake: String) = snakeToPascalCase(snake).replaceFirstChar(Char::lowercase)
|
||||
|
||||
private fun dbToJavaType(column: SchemaColumn) = (when (column.type) {
|
||||
ColumnType.INTEGER -> TypeName.INT
|
||||
ColumnType.BOOLEAN -> TypeName.BOOLEAN
|
||||
ColumnType.TINYINT -> TypeName.SHORT
|
||||
ColumnType.SMALLINT -> TypeName.SHORT
|
||||
ColumnType.BIGINT -> TypeName.LONG
|
||||
ColumnType.DECIMAL -> TypeName.FLOAT
|
||||
ColumnType.DOUBLE -> TypeName.DOUBLE
|
||||
ColumnType.REAL -> TypeName.FLOAT
|
||||
ColumnType.CHAR -> TypeName.CHAR
|
||||
else -> TypeName.get(String::class.java)
|
||||
}).let { if (column.nullable) it.box() else it }
|
||||
|
||||
private fun dbToBindMethod(column: SchemaColumn) = when (column.type) {
|
||||
ColumnType.INTEGER -> "setInt"
|
||||
ColumnType.BOOLEAN -> "setBoolean"
|
||||
ColumnType.TINYINT -> "setShort"
|
||||
ColumnType.SMALLINT -> "setShort"
|
||||
ColumnType.BIGINT -> "setLong"
|
||||
ColumnType.DECIMAL -> "setFloat"
|
||||
ColumnType.DOUBLE -> "setDouble"
|
||||
ColumnType.REAL -> "setFloat"
|
||||
else -> "setString"
|
||||
}
|
||||
|
||||
private fun dbToGetMethod(column: SchemaColumn) = when (column.type) {
|
||||
ColumnType.INTEGER -> "getInt"
|
||||
ColumnType.BOOLEAN -> "getBoolean"
|
||||
ColumnType.TINYINT -> "getShort"
|
||||
ColumnType.SMALLINT -> "getShort"
|
||||
ColumnType.BIGINT -> "getLong"
|
||||
ColumnType.DECIMAL -> "getFloat"
|
||||
ColumnType.DOUBLE -> "getDouble"
|
||||
ColumnType.REAL -> "getFloat"
|
||||
else -> "getString"
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val GENERATOR_NAME = DataAccessObjectCodeGenerator::class.java.canonicalName
|
||||
private const val MODEL_PACKAGE = "com.bartlomiejpluta.base.generated.db.model"
|
||||
private const val DAO_PACKAGE = "com.bartlomiejpluta.base.generated.db.dao"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user