style="display:inline-block;width:728px;height:90px"
data-ad-client="ca-pub-5164839828746352"
data-ad-slot="7563230308">

Payara, Kotlin y Mysql

Bueno comenzando la parte de los servicios vamos a tener básicamente dos endpoints uno para consultar preguntas y otro para insertar preguntas.

GET /services/questions/getAll
POST /services/questions/add

Por tanto creamos el archivo kotlin QuestionService en la carpeta question (primero crear la carpeta question), si no encontramos preguntas regresaremos 404, esto es a modo de ejemplo si quieres puedes regresar 200 y un array vacio es tu gusto y/o disenio, usaremos Gson como librería para parsear JSON a Kotlin y viceversa:

package question

import com.google.gson.Gson
import javax.ws.rs.*
import javax.ws.rs.core.MediaType
import javax.ws.rs.core.Response

@Path("/question")
class QuestionService {

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/getAll")
    fun getAllQuestionsApi(): Response {
        val questions = getAllQuestions()
        return if(questions.isEmpty()) {
            Response.status(404).build()
        } else {
            Response.status(200).entity(Gson().toJson(questions)).build()
        }
    }

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/add")
    fun addQuestionApi(question: String): Response {
        val gson = Gson()
        val newQuestion = addQuestion(gson.fromJson(question, Question::class.java))

        return Response.status(201).entity(Gson().toJson(newQuestion)).build()

    }
}

Como se puede observar tenemos dos funciones que requerimos getAllQuestions y addQuestion, a mi me gusta la estructura ruta -> controlador -> dao, por lo que a continuación vamos a definir el dao, para eso primero necesitamos una base de datos igual en MySQL y con las mismas tablas de la vez pasada

Question

Campo
Tipo

idQuestion
Integer

texto
text

Answer

Campo
Tipo

idAnswer
Integer

texto
text

idQuestion
Integer

Con eso tenemos una relación 1 a N de Question a Answer. Vamos a usar el super ultra complicado JDBC para la parte de persistencia y un mapeador sencillo que se llama Simple Flat Mapper; creamos un archivo kotlin llamado QuestionDao en la carpeta question con el siguiente contenido:

package question

import answer.Answer
import answer.insertAnswerWithQuestion
import config.getDataSource
import org.simpleflatmapper.jdbc.JdbcMapperFactory
import java.sql.Statement
import kotlin.streams.toList

private const val queryAllQuestions = "SELECT q.idQuestion, q.text, a.idAnswer AS answers_idAnswer, a.text AS answers_text " +
        "FROM question q LEFT JOIN answer a ON q.idQuestion = a.idQuestion"
private const val insertQuestionQuery = "INSERT INTO question (text) VALUES (?)"

data class Question (
        val idQuestion: Int = -1,
        var text: String = "",
        var answers: List<Answer> = emptyList())

var mapper = JdbcMapperFactory.newInstance()
        .fieldMapperErrorHandler {
            _, _, target, _ -> log.info("Question with no answers $target")
        }
        .addKeys("idQuestion")
        .newMapper<Question>(Question::class.java)

fun findAllQuestions(): List<Question> {
    log.debug("Getting all questions")
    log.debug("Query: $queryAllQuestions")

    return getDataSource().connection.use { con ->
        con.prepareStatement(queryAllQuestions).use { ps ->
            ps.executeQuery().use { rs -> mapper.stream(rs).toList() }
        }
    }
}

fun insertQuestion(question: Question): Question {
    log.debug("Add question with text ${question.text}")
    return getDataSource().connection.use { con ->
        val newQuestion = con.prepareStatement(insertQuestionQuery, Statement.RETURN_GENERATED_KEYS).use { ps ->
            ps.setString(1, question.text)
            ps.executeUpdate()
            ps.generatedKeys.use { rs ->
                var idQuestion = -1
                if(rs.next()) {
                    idQuestion = rs.getInt(1)
                }
                Question(idQuestion = idQuestion, text = question.text)
            }
        }

        if(!question.answers.isEmpty()) {
            newQuestion.answers = question.answers.map { a -> insertAnswerWithQuestion(a, newQuestion.idQuestion, con) }
        }
        con.commit()
        return newQuestion
    }
}

Como tenemos dos entidades, necesitamos otro dao para las respuestas, creamos una carpeta answer y agregamos el archivo kotlin AnswerDao con el siguiente contenido:

package answer

import java.sql.Connection
import java.sql.Statement

data class Answer (
        val idAnswer: Int = -1,
        var text: String = "")

private const val insertAnswerQuery = "INSERT INTO answer (text, idQuestion) VALUES (?, ?)"

fun insertAnswerWithQuestion(answer: Answer, idQuestion: Int, con: Connection): Answer {
    log.debug("Add answer with text ${answer.text}")

    return con.prepareStatement(insertAnswerQuery, Statement.RETURN_GENERATED_KEYS).use { ps ->
        ps.setString(1, answer.text)
        ps.setInt(2, idQuestion)
        ps.execute()
        ps.generatedKeys.use { generatedKeys ->
            generatedKeys.next()
            Answer(idAnswer = generatedKeys.getInt(1), text = answer.text)
        }
    }
}

Como se puede leer para consultar las preguntas usamos el mapper que nos apoya en la creación de la lista de preguntas con respuesta, mientras que para insertar la pregunta primero insertamos la pregunta y después en la misma transacción insertamos la lista de respuestas. Con esto tenemos listo la función que encuentra las preguntas en la base de datos y una función que las agrega, nos falta agregar los controladores que conecten la base con los servicios y eso es lo que vamos a hacer por tanto creamos un archivo kotlin que se llame QuestionController en la carpeta question:

package question

import org.slf4j.Logger
import org.slf4j.LoggerFactory

val log: Logger = LoggerFactory.getLogger(Question::class.java)

fun getAllQuestions(): HashMap<String, List<Question>> = hashMapOf("questions" to findAllQuestions())

fun addQuestion(question: Question): Question {
    return insertQuestion(question)
}

fun main(args: Array<String>) {
    println(getAllQuestions())
}

Tambien necesitamos el controller de las respuestas aunque por el momento solo sirve para inicializar el logger, creamos un kotlin file en la carpeta answer:

package answer

import org.slf4j.Logger
import org.slf4j.LoggerFactory

val log: Logger = LoggerFactory.getLogger(Answer::class.java)

Necesitamos configurar el Datasource, para ello usaremos HIkariCP que es rapido y ligero, creamos una carpeta llamada config y dentro agregamos el archivo kotlin DBConfig con la información de nuestra base de datos:

package config

import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import javax.sql.DataSource

val ds = createDataSource()

private fun createDataSource(): HikariDataSource {
    val config = HikariConfig()

    config.jdbcUrl = "jdbc:mysql://localhost:3306/questions?serverTimezone=UTC&useSSL=false"
    config.username = "betotto"
    config.password = "123456"
    config.driverClassName = "com.mysql.cj.jdbc.Driver"
    config.isAutoCommit = false
    config.maximumPoolSize = 5
    config.poolName = "questions"
    return HikariDataSource(config)
}

fun getDataSource(): DataSource = ds

Finalmente actualizamos las dependencias del gradle.build file:

dependencies {
    compile group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk8', version: '1.2.51'
    compile group: 'com.zaxxer', name: 'HikariCP', version: '3.1.0'
    compile group: 'org.simpleflatmapper', name:'sfm-jdbc', version: '3.19.0'
    compile group: 'mysql', name: 'mysql-connector-java', version: '8.0.11'
    compile group: 'org.slf4j', name: 'slf4j-api', version:'1.7.25'
    compile group: 'org.slf4j', name: 'slf4j-simple', version:'1.7.25'
    compile group: 'com.google.code.gson', name: 'gson', version: '2.8.5'
    compileOnly group: 'org.eclipse.microprofile', name: 'microprofile', version: '1.3'
    testCompile group: 'junit', name: 'junit', version: '4.12'
}

En el QuestionController esta un main que nos ayudara a probar que las llamadas a la base de datos funcionen de manera adecuada sin tener que desplegar a cada rato el war, corriendo el main y con algunos datos en la base esta es la salida:

{questions=[Question(idQuestion=42, text=pregunta uno, answers=[Answer(idAnswer=64, text=Answer1), Answer(idAnswer=65, text=Anxwer2)]), Question(idQuestion=43, text=pregunta dos, answers=[Answer(idAnswer=66, text=Answer1), Answer(idAnswer=67, text=Anxwer2), Answer(idAnswer=68, text=Anxwer3)]), Question(idQuestion=44, text=pregunta tres, answers=[])]}

Listo, hemos terminado nuestros servicios para consultar y para guardar preguntas, solo nos falta ejecutar “gradle war” en la terminal y copiar nuestro War en la misma ubicación que PayaraMicro jar y ejecutar en la terminal “java -jar payara-micro-5.183.jar --deploy PayaraMicro-0.0.1.war”

Usando postman

GET URL: http://10.237.97.147:8080/PayaraMicro-0.0.1/services/question/getAll

{
"questions": [
{
"idQuestion": 42,
"text": "pregunta uno",
"answers": [
{
"idAnswer": 64,
"text": "Answer1"
},
{
"idAnswer": 65,
"text": "Anxwer2"
}
]
},
{
"idQuestion": 43,
"text": "pregunta dos",
"answers": [
{
"idAnswer": 66,
"text": "Answer1"
},
{
"idAnswer": 67,
"text": "Anxwer2"
},
{
"idAnswer": 68,
"text": "Anxwer3"
}
]
},
{
"idQuestion": 44,
"text": "pregunta tres",
"answers": []
}
]
}

POST URL: http://10.237.97.147:8080/PayaraMicro-0.0.1/services/question/add
Body:

{
"text": "Nueva pregunta",
"answers": [
{
"text": "Respuesta 1"
}
]
}

Response:

{
"idQuestion": 45,
"text": "Nueva pregunta",
"answers": [
{
"idAnswer": 69,
"text": "Respuesta 1"
}
]
}

Y listo hemos terminado nuestros servicios HTTP rest básicos, el siguiente post sera hacer la parte web de esta miniaplicacion

style="display:inline-block;width:728px;height:90px"
data-ad-client="ca-pub-5164839828746352"
data-ad-slot="7563230308">