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:
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:
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:
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:
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:
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:
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:
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:
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
- betotto's blog
- Inicie sesión o regístrese para enviar comentarios
Comentarios
Sugerencia
Como sugerencia, tus URL no necesitan indicar la acción que van a ejecutar, el propio verbo HTTP lo indica
GET /services/questions
Aqui el propio verbo GET indica que se trata de una consulta
POST /services/questions
Aqui el propio verbo POST indica que se trata de agregar o guardar
igual para los verbos PUT (actualización) y DELETE
Saludos!