jooby-project / jooby

The modular web framework for Java and Kotlin
https://jooby.io
Apache License 2.0
1.7k stars 199 forks source link

Jooby Hibernate: "Session/EntityManager is closed" in the second call #3478

Closed jonaskahn closed 1 month ago

jonaskahn commented 1 month ago

I'm building an template api with jooby using Kotlin. When I try to integrate with Hibernate . The error is thrown everytime I re-call (after the second times) java.lang.IllegalStateException: Session/EntityManager is closed

Here is my configuration

App.kt

    install(HibernateModule().scan("io.github.jonaskahn.entities"))
    use(TransactionalRequest().enabledByDefault(false)) 

Controller

package io.github.jonaskahn.controller.users

import io.github.jonaskahn.services.authen.AuthenticationService
import io.github.jonaskahn.services.user.UserService
import io.jooby.annotation.POST
import io.jooby.annotation.Path
import io.jooby.annotation.Transactional
import jakarta.inject.Inject

@Path("/users")
class UserController @Inject constructor(
    private val userService: UserService,
    private val authenticationService: AuthenticationService
) {

    @POST("/generate-token")
    fun generateToken(request: UserTokenRequest): String {
        return authenticationService.generateToken(request.username!!, request.password!!)
    }

    @POST("/register")
    @Transactional
    fun register(request: UserRegisterRequest) {
        userService.createUser(request)
    }
}

UserService

package io.github.jonaskahn.services.user

import com.google.inject.ImplementedBy
import io.github.jonaskahn.controller.users.UserRegisterRequest

@ImplementedBy(UserServiceImpl::class)
interface UserService {
    fun createUser(request: UserRegisterRequest)
}

UserServiceImpl

package io.github.jonaskahn.services.user

import io.github.jonaskahn.controller.users.UserRegisterRequest
import io.github.jonaskahn.entities.User
import io.github.jonaskahn.entities.enums.Status
import io.github.jonaskahn.repositories.UserRepository
import io.github.jonaskahn.services.authen.PasswordEncoder
import jakarta.inject.Inject
import jakarta.inject.Singleton

@Singleton
internal class UserServiceImpl @Inject constructor(
    private val userRepository: UserRepository,
    private val passwordEncoder: PasswordEncoder,
) : UserService {

    override fun createUser(request: UserRegisterRequest) {
        if (userRepository.existsByUsernameOrEmail(request.username, request.email)) {
            throw UserExistException()
        }
        val newUser = User()
        newUser.email = request.email
        newUser.username = request.username ?: request.email
        newUser.fullName = request.name
        newUser.password = passwordEncoder.encode(request.password!!)
        newUser.status = Status.LOCK
        userRepository.save(newUser)
    }
}

UserRepository

package io.github.jonaskahn.repositories

import com.google.inject.ImplementedBy
import io.github.jonaskahn.entities.User

@ImplementedBy(UserRepositoryImpl::class)
interface UserRepository {
    fun findByUsernameOrEmail(username: String): User?

    fun existsByUsernameOrEmail(username: String?, email: String?): Boolean

    fun save(user: User)
}

UserRepositoryImpl

package io.github.jonaskahn.repositories

import io.github.jonaskahn.entities.User
import jakarta.inject.Inject
import jakarta.inject.Singleton
import jakarta.persistence.EntityManager

@Singleton
class UserRepositoryImpl @Inject constructor(
    private val em: EntityManager
) : UserRepository {

    override fun findByUsernameOrEmail(username: String): User? {
        val query =
            em.createQuery("select u from users u where username = :username or email = :username", User::class.java)
        query.setParameter("username", username)
        return query.singleResult
    }

    override fun existsByUsernameOrEmail(username: String?, email: String?): Boolean {
        if (username == null && email == null) return false
        val sql = StringBuilder("SELECT 1 FROM users u WHERE 1 = 1 ")
        val params = mutableMapOf<String, String>()
        if (username != null) {
            sql.append(" AND u.username = :username")
            params["username"] = username
        }
        if (email != null) {
            sql.append(" AND u.email = :email")
            params["email"] = email
        }
        val query = em.createQuery("select exists ($sql)", Boolean::class.java)
        params.forEach { (k, v) -> query.setParameter(k, v) }
        return query.singleResult
    }

    override fun save(user: User) {
        em.persist(user)
    }
}

Create user request

curl --location 'http://localhost:8080/users/register' \
--header 'Content-Type: application/json' \
--data-raw '{
    "name": "Jonas",
    "email": "test@test.com",
    "password": "11111111"
}'

I only success call API once, for the next times. It will throw an error like "java.lang.IllegalStateException: Session/EntityManager is closed"

For further information: I took some tests, even with transaction enabled by default, the error still appear. Could you point me out which is wrong? Thanks P/s: Here is my project: https://github.com/jonaskahn/kooby-api-template

jknack commented 1 month ago

Hi @jonaskahn

The transactional request middleware/filter creates an EntityManager per request, once request is done the EM is closed. The issue is on your @Singleton repository, just remove that annotation and everything should work

jonaskahn commented 1 month ago

Oh, I see. I read the document and I thought the mechanism like spring boot but it seem does not. So I remove @Singleton and now it worked. Thank you

edgar-espina-wpp commented 1 month ago

Correct. We don't generate a proxy (or anything like that), that is why the scope is important

jonaskahn commented 1 month ago

Yup. I see. I will close the issue