One-To-Many relation with JPA & Hibernate - spring

I am new to Stack Overflow, so sorry in advance if I make a mistake regarding asking a question.
I am trying to create a one-to-many relation between my User entity and my Team entity. Which is pretty straight forward as you can auto-generate a join table's using annotions. But I also want an extra column in this join table. I want to keep track of whether someone is a coach yes or no. I hope this ERD gives a clearer idea of what I mean:
As you can see, I also added the join table user_roles. I think this should be taken into consideration when considering how to solve this problem. Because the User Entity has a set of these roles. These (should) be retrieved when you retrieve a Team from the database.
This is what I have now in terms of code:
User class:
package com.goldwasser.app.resourceserver.entity
import java.time.LocalDateTime
import java.util.*
import javax.persistence.*
import javax.validation.constraints.Email
import javax.validation.constraints.NotBlank
import javax.validation.constraints.Size
#Entity
#Table(name = "users")
data class User(
#NotBlank
#Size(max = 50)
#Email
private val username: String,
#NotBlank
#Size(max = 50)
#Email
private val email: String,
#NotBlank
#Size(max = 120)
private val password: String,
#NotBlank
#Size(max = 50)
private val firstname: String,
#NotBlank
#Size(max = 50)
private val lastname: String,
#NotBlank
private val createTime: LocalDateTime,
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "user_roles",
joinColumns = [JoinColumn(name = "user_id")],
inverseJoinColumns = [JoinColumn(name = "role_id")])
private val roles: MutableSet<Role>,
#OneToOne(mappedBy = "player", cascade = [CascadeType.ALL], orphanRemoval = true)
var playerTeam: PlayerTeam? = null
) : AbstractJpaPersistable<UUID>() {
companion object {
operator fun invoke(
username: String? = null,
email: String? = null,
password: String? = null,
firstName: String? = null,
lastName: String? = null,
createTime: LocalDateTime? = null,
roles: MutableSet<Role>? = null
) = User(
username ?: "",
email ?: "",
password ?: "",
firstName ?: "",
lastName ?: "",
createTime ?: LocalDateTime.now(),
roles ?: mutableSetOf()
)
}
fun getRoles(): Set<Role> = roles
fun getPassword(): String = password
fun getUsername(): String = username
fun getEmail(): String = email
fun getFirstname(): String = firstname
fun getLastname(): String = lastname
fun getCreateTime(): LocalDateTime = createTime
fun setRoles(roles: Set<Role>) {
this.roles.addAll(roles)
}
}
PlayerTeam class:
package com.goldwasser.app.resourceserver.entity
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import java.util.UUID
import javax.persistence.*
#Entity
#Table(name = "player_teams")
data class PlayerTeam(
#EmbeddedId
var playerTeamId: PlayerTeamId? = null,
#OneToOne(fetch = FetchType.LAZY)
#MapsId("userId")
val player: User? = null,
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("teamId")
val team: Team? = null,
val isCoach: Boolean = false
) {
override fun toString(): String {
val mapper = ObjectMapper()
mapper.enable(SerializationFeature.INDENT_OUTPUT)
return mapper.writeValueAsString(this)
}
fun getIsCoach(): Boolean = isCoach
}
PlayerTeamId (Composite-key) class:
package com.goldwasser.app.resourceserver.entity
import org.hibernate.annotations.GenericGenerator
import org.hibernate.annotations.Type
import java.io.Serializable
import java.util.UUID
import javax.persistence.Column
import javax.persistence.Embeddable
import javax.persistence.GeneratedValue
#Embeddable
data class PlayerTeamId (
#GeneratedValue(generator = "uuid2")
#GenericGenerator(
name = "uuid2",
strategy = "uuid2"
)
#Column(name = "user_id", updatable = false, nullable = false, columnDefinition = "VARCHAR(36)")
#Type(type = "uuid-char")
var userId: UUID = UUID.randomUUID(),
#GeneratedValue(generator = "uuid2")
#GenericGenerator(
name = "uuid2",
strategy = "uuid2"
)
#Column(name = "team_id", updatable = false, nullable = false, columnDefinition = "VARCHAR(36)")
#Type(type = "uuid-char")
var teamId: UUID = UUID.randomUUID()
) : Serializable
My Team class:
package com.goldwasser.app.resourceserver.entity
import java.util.UUID
import javax.persistence.*
#Entity
#Table(name="teams")
data class Team(
val name: String,
#OneToMany(mappedBy = "team", cascade = [CascadeType.ALL], orphanRemoval = true)
var playerTeams: MutableSet<PlayerTeam>? = null
): AbstractJpaPersistable<UUID>() {
}
And at last, my main Entity class (all entities should inherit this one because it provides them with an Id as UUID):
package com.goldwasser.app.resourceserver.entity
import org.hibernate.annotations.GenericGenerator
import org.hibernate.annotations.Type
import org.springframework.data.util.ProxyUtils
import java.io.Serializable
import javax.persistence.*
#MappedSuperclass
abstract class AbstractJpaPersistable<T : Serializable> {
#Id
#GeneratedValue(generator = "uuid2")
#GenericGenerator(
name = "uuid2",
strategy = "uuid2"
)
#Column(name = "id", updatable = false, nullable = false, columnDefinition = "VARCHAR(36)")
#Type(type = "uuid-char")
private var id: T? = null
fun getId(): T? {
return this.id
}
fun setId(type: T?) {
this.id = type
}
override fun equals(other: Any?): Boolean {
other ?: return false
if (this === other) return true
if (javaClass != ProxyUtils.getUserClass(other)) return false
other as AbstractJpaPersistable<*>
return if (null == this.id) false else this.id == other.id
}
override fun hashCode(): Int {
return 31
}
override fun toString() = "Entity of type ${this.javaClass.name} with id: $id"
}
Now that you've seen all my entity classes, I will show/tell you where I have been getting stuck at.
I have a repository called "TeamRepository". It's responsible for handling all database action related stuff for the Team entity:
package com.goldwasser.app.resourceserver.repository
import com.goldwasser.app.resourceserver.entity.Team
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
import java.util.UUID
#Repository
interface TeamRepository : JpaRepository<Team, UUID> {
}
And I have a service called "TeamServiceImpl":
package com.goldwasser.app.resourceserver.service
import com.goldwasser.app.resourceserver.entity.PlayerTeam
import com.goldwasser.app.resourceserver.entity.PlayerTeamId
import com.goldwasser.app.resourceserver.entity.Team
import com.goldwasser.app.resourceserver.entity.User
import com.goldwasser.app.resourceserver.repository.PlayerTeamRepository
import com.goldwasser.app.resourceserver.repository.TeamRepository
import com.goldwasser.app.resourceserver.repository.UserRepository
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.data.annotation.Persistent
import org.springframework.stereotype.Service
import javax.transaction.Transactional
#Service
class TeamServiceImpl : TeamService {
#Autowired
private lateinit var playerTeamRepository: PlayerTeamRepository
#Autowired
private lateinit var userRepository: UserRepository
#Autowired
private lateinit var teamRepository: TeamRepository
#Transactional
override fun addPlayerToTeam(player: User, team: Team) {
val playerTeamId = PlayerTeamId(
player.getId()!!,
team.getId()!!
)
val playerTeam = PlayerTeam(
playerTeamId,
user = player,
team = team,
isCoach = true
)
playerTeamRepository.save(playerTeam)
}
override fun getPlayerTeam(team: Team) : Team {
val teamInDb = teamRepository.findById(team.getId()!!).get()
return teamInDb;
}
}
The problem is that when I call getPlayerTeam(team), my teamInDb does contain a Team-object, but the playerTeams of the Team-object results in a LazyInitializationException. The same applies when I try to fetch a user object. I know this has to do something with JPA not being able to access the requested entity because of the session being closed. But I do not know how to fix this.
EDIT:
I have added the all-open plugin:
plugins {
id("org.springframework.boot") version "2.6.6"
id("org.jetbrains.kotlin.plugin.allopen") version "1.6.21"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
kotlin("jvm") version "1.6.10"
kotlin("plugin.spring") version "1.6.10"
kotlin("plugin.jpa") version "1.6.10"
}
Now I do get a PlayerTeam object containing a Player and Team. But those Player and Team have also have a PlayerTeam set which cannot be initialised, see picture below:

Related

Repository not executing methods in test

I am trying to simply test a repository. Here is my test code:
#ContextConfiguration(classes = [
DatabaseTestConfiguration::class
])
#TestPropertySource(
properties = [
"spring.jpa.hibernate.ddl-auto=create-drop",
"spring.flyway.enabled=false"
]
)
#AutoConfigureTestDatabase
#AutoConfigureDataJpa
#AutoConfigureTestEntityManager
internal open class UserTest {
#Autowired
private lateinit var entityManager: TestEntityManager
#Autowired
private lateinit var userRepository: UserRepository
#Test
#Transactional
open fun whenSuccessfullyFoundAnyDuplicatedUserThenReturnTrue() {
val user = User()
user.name = NAME
user.surname = SURNAME
user.gender = GENDER
entityManager.persistAndFlush(user)
assertEquals(true, userRepository.anyDuplicate(
name = user.name,
surname = user.surname,
gender = user.gender,
))
}
companion object {
const val NAME = "John"
const val SURNAME = "Snow"
const val GENDER = "male"
}
Here is my DatabaseTestConfiguration:
#TestConfiguration
#EntityScan(value = [
"com.username.db.entities"
])
#EnableJpaRepositories(
"com.username.db.repositories"
)
class DatabaseTestConfiguration {
}
This is my repository:
#Repository
interface UserRepository: JpaRepository<User,Long> {
#Query(
"""
SELECT CASE
WHEN COUNT(user.id) > 0 THEN TRUE
ELSE FALSE END
FROM User user
WHERE user.name = :name AND user.surname = :surname AND user.gender = :gender
)
"""
)
fun anyDuplicate(
#Param("name") name: String?,
#Param("surname") surname: String?,
#Param("gender") gender: String?
): Boolean
}
This is my entity:
#Entity
#Table(name = "user")
class User {
#Id
#GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "cdr_generator"
)
#SequenceGenerator(
name = "cdr_generator",
sequenceName = "cdr_seq",
allocationSize = 1
)
#Column(name = "id", nullable = false)
var id: Long? = null
#Column(name = "name", nullable = false)
var name: String? = null
#Column(name = "surname", nullable = false)
var surname: String? = null
#Column(name = "gender", nullable = false)
var gender: String? = null
}
Now when i am testing. this will always fail my test. When i debug it, i get that in my test it never enters userRepository.anyDuplicate() function. I assume that the problem is with configuring test. Can anybody help?

Spring boot "Operation is not supported for read-only collection" when updating a record in database

I am fairly new to Spring Boot and unable to find out why my CRUD repository seems to keep throwing an exception saying "Operation is not supported for read-only collection" when sending a request to a Put endpoint. All other repositories seem to work just fine. Here's my code:
User.kt
package com.karbal.tutortek.entities
import com.karbal.tutortek.dto.userDTO.UserPostDTO
import java.sql.Date
import javax.persistence.*
#Entity
#Table(name = "users")
data class User(
#Id
#Column(name = "id", nullable = false)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_generator")
#SequenceGenerator(name = "user_generator", sequenceName = "user_seq", allocationSize = 1)
var id: Long? = null,
#Column(name = "firstName", nullable = false)
var firstName: String = "",
#Column(name = "lastName", nullable = false)
var lastName: String = "",
#Column(name = "birthDate", nullable = false)
var birthDate: Date = Date(System.currentTimeMillis()),
#Column(name = "rating", nullable = false)
var rating: Float = 0.0F,
#OneToMany(mappedBy = "user")
var payments: List<Payment> = listOf(),
#OneToMany(mappedBy = "user")
var topics: List<Topic> = listOf()
){
constructor(userPostDTO: UserPostDTO) : this(
null,
userPostDTO.firstName,
userPostDTO.lastName,
userPostDTO.birthDate,
userPostDTO.rating
)
fun copy(user: User){
firstName = user.firstName
lastName = user.lastName
birthDate = user.birthDate
rating = user.rating
payments = user.payments
topics = user.topics
}
}
UserController.kt
package com.karbal.tutortek.controllers
import com.karbal.tutortek.dto.userDTO.UserGetDTO
import com.karbal.tutortek.dto.userDTO.UserPostDTO
import com.karbal.tutortek.entities.User
import com.karbal.tutortek.services.UserService
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.*
import org.springframework.web.server.ResponseStatusException
import java.util.*
#RestController
class UserController(val userService: UserService) {
#PostMapping("/users/add")
fun addUser(#RequestBody userDTO: UserPostDTO): UserGetDTO {
val user = User(userDTO)
return UserGetDTO(userService.saveUser(user))
}
#DeleteMapping("/users/{id}")
fun deleteUser(#PathVariable id: Long){
val user = userService.getUser(id)
if(user.isEmpty) throw ResponseStatusException(HttpStatus.NOT_FOUND, "User not found")
userService.deleteUser(id)
}
#GetMapping("/users/all")
fun getAllUsers() = userService.getAllUsers().map { u -> UserGetDTO(u) }
#GetMapping("/users/{id}")
fun getUser(#PathVariable id: Long): UserGetDTO {
val user = userService.getUser(id)
if(user.isEmpty) throw ResponseStatusException(HttpStatus.NOT_FOUND, "User not found")
return UserGetDTO(user.get())
}
#PutMapping("/users/{id}")
fun updateUser(#PathVariable id: Long, #RequestBody userDTO: UserPostDTO){
val user = User(userDTO)
val userInDatabase = userService.getUser(id)
if(userInDatabase.isEmpty) throw ResponseStatusException(HttpStatus.NOT_FOUND, "User not found")
val extractedUser = userInDatabase.get()
extractedUser.copy(user)
userService.saveUser(extractedUser)
}
}
UserService.kt
package com.karbal.tutortek.services
import com.karbal.tutortek.entities.User
import org.springframework.stereotype.Service
import com.karbal.tutortek.repositories.UserRepository
#Service
class UserService(val database: UserRepository) {
fun getAllUsers(): List<User> = database.getAllUsers()
fun saveUser(user: User) = database.save(user)
fun deleteUser(id: Long) = database.deleteById(id)
fun getUser(id: Long) = database.findById(id)
}
UserRepository.kt
package com.karbal.tutortek.repositories
import com.karbal.tutortek.entities.User
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.CrudRepository
import org.springframework.stereotype.Repository
#Repository
interface UserRepository : CrudRepository<User, Long> {
#Query("SELECT * FROM users", nativeQuery = true)
fun getAllUsers(): List<User>
}
UserPostDTO.kt
package com.karbal.tutortek.dto.userDTO
import java.sql.Date
data class UserPostDTO(
var firstName: String,
var lastName: String,
var rating: Float,
var birthDate: Date
)
The JSON that I send:
{
"firstName": "Thomas",
"lastName": "Thompson",
"rating": 4.7,
"birthDate": "2000-02-03"
}
Post works fine. Put works fine on other entities in my code. But here it always responds with 500 and a message "Operation is not supported for read-only collection". Any ideas why this could be happening?
Just solved this after a couple of hours. Changed lists in User class to mutable lists and now it works fine.

how to include an Embedded attribute to json return object spring boot

I have a Kotlin Spring Boot application with these following entities:
import com.fasterxml.jackson.annotation.JsonIgnore
import javax.persistence.*
#Entity
#Table(name = "users")
class Users(
#Id
var username: String = "",
#JsonIgnore
var password: String = "",
#JsonIgnore
var enabled: Boolean = false,
#ElementCollection
#CollectionTable(name = "authorities", joinColumns = [JoinColumn(name = "username")])
#MapKeyColumn(name = "users")
private var authorities : MutableSet<Authorities> = mutableSetOf()
)
and
#Embeddable
class Authorities(
var authority: String
)
When I call this endpoint:
#GetMapping("/users")
fun getUserById(#RequestParam(value = "username") username : String) : ResponseEntity<Users>{
val optionalUser = usersService.getByUsername(username)
return if(optionalUser.isPresent) ResponseEntity.ok(optionalUser.get())
else ResponseEntity(HttpStatus.BAD_REQUEST)
}
it returns correctly, as you can see in this json:
{
"username": "test#gmail.com"
}
But it doesn't return the list of authorities that belongs to this user, and I wanted to have it returned. My question is: How could I do it?
Thank you so much for helping

Spring boot UUID primary key entity doesn't show right id after created

So I have entity with UUID as primary key. After I create the entity in my service, the response return wrong id.
My Role entity
package denny.study.stock.entity
import denny.study.stock.util.converter.JsonToMapConverter
import org.hibernate.annotations.CreationTimestamp
import org.hibernate.annotations.GenericGenerator
import org.hibernate.annotations.Type
import org.hibernate.annotations.UpdateTimestamp
import java.util.*
import javax.persistence.*
#Entity
#Table(name = "roles")
class Role {
#Id
#GeneratedValue(generator = "uuid2")
#GenericGenerator(name = "uuid2", strategy = "uuid2")
#Column(name = "id", columnDefinition = "CHAR(36)")
#Type(type = "uuid-char")
var id: UUID = UUID.randomUUID()
#Column
var name: String = ""
#Column
var slug: String = ""
#Column
#Convert(converter = JsonToMapConverter::class)
var permissions: MutableMap<String, Boolean>? = null
#Column(name = "created_at")
#CreationTimestamp
var createdAt: Date = Date()
#Column(name = "updated_at")
#UpdateTimestamp
var updatedAt: Date? = null
}
My store method in role service
override fun store(roleRequest: RoleRequest): RoleResponse {
val role = Role().apply {
name = roleRequest.name
slug = roleRequest.slug
permissions = roleRequest.permissions
}
roleRepository.save(role)
return RoleResponse(role)
}
My Role response
package denny.study.stock.model.response.role
import denny.study.stock.entity.Role
class RoleResponse(role: Role) {
var id = role.id
var name = role.name
var slug = role.slug
var permissions = role.permissions
}
the json response return id "f95bddf6-eb22-49bb-b8e6-5eb819603fa9"
{
"code": 200,
"status": "OK",
"data": {
"id": "f95bddf6-eb22-49bb-b8e6-5eb819603fa9",
"name": "Role 1",
"slug": "role-1",
"permissions": {
"asd": true,
"dsa": false
}
}
}
while in DB it stored as "87596692-7ee9-4ecb-a425-3b1372d901f4". Do you know why It return the wrong id? Thank you!
You are using #GeneratedValue annotation and also assigning UUID.randomUUID() to the id attribute. Either use one or the other, not both.
If you want the ID to be generated by the persistence provider then keep #GeneratedValue and remove #GenericGenerator (which is a Hibernate annotation) and UUID.randomUUID(). If you want to do that on your own then remove #GeneratedValue and #GenericGenerator.

Parameter specified as non-null is null:Can't Update individual Entity field in Spring Boot jpa. Caused Parameter specified as non-null is null

Recently, I am learning Spring boot with Postgrasql as Database. All of my POST, GET, DELETE methods work perfectly without PUT method. What I want is to update individual field of entity. But if I don't update all field, it says 'Parameter specified as non-null is null'
My code is
User.kt
import java.time.LocalDate
import javax.persistence.*
import javax.persistence.GenerationType.SEQUENCE
#Entity
#Table(name = "user_info")
data class User(
#Id
#SequenceGenerator(
name = "user_seq",
sequenceName = "user_seq",
allocationSize = 1
)
#GeneratedValue(
strategy = SEQUENCE,
generator = "user_seq"
)
#Column(
name = "id",
updatable = false
)
val id: Long = -1,
#Column(
name = "first_name",
nullable = false,
length = 50,
updatable = true
)
val firstName: String,
#Column(
name = "last_name",
nullable = false,
length = 50,
updatable = true
)
val lastName: String,
#Column(
name = "email",
nullable = true,
length = 150,
updatable = true
)
val email: String,
#Column(
name = "gender",
nullable = false,
length = 2,
updatable = true
)
val gender: String,
#Column(
name = "date_of_birth",
nullable = false,
updatable = true
)
val dateOfBirth: LocalDate,
#Column(
name = "country",
nullable = false,
length = 50,
updatable = true
)
val country: String
)
UserController.kt
package com.example.demo.user
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.format.annotation.DateTimeFormat
import org.springframework.format.annotation.DateTimeFormat.ISO.DATE
import org.springframework.web.bind.annotation.*
import java.time.LocalDate
#RestController
#RequestMapping(
path = [
"/api/v1/"
]
)
class UserController(
#Autowired private val userService: UserService
) {
#PutMapping("update/{id}")
fun updateUser(
#PathVariable("id") id: Long,
#RequestParam(required = false) firstName: String,
#RequestParam(required = false) lastName: String,
#RequestParam(required = false) email: String,
#RequestParam(required = false) gender: String,
#RequestParam(required = false) country: String,
#RequestParam(required = false) #DateTimeFormat(iso = DATE) dateOfBirth: LocalDate
) {
return userService.updateUser(id, firstName, lastName, email, gender, country, dateOfBirth)
}
}
UserService.kt
package com.example.demo.user
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDate
#Service
class UserService(
#Autowired private val userRepository: UserRepository
) {
fun registerUser(user: User) {
if (userRepository.findUserByEmail(user.email).isPresent) throw IllegalStateException("Email Already taken")
userRepository.save(user)
}
fun getUsers(offset: Int): List<User> {
return userRepository.getUsers(offset)
}
fun getUserInfo(userId: Long): User {
if (!userRepository.existsById(userId)) throw IllegalArgumentException("User not found")
return userRepository.findById(userId).get()
}
fun deleteUser(userId: Long) {
if (!userRepository.existsById(userId)) throw IllegalStateException("User with id $userId does not exist")
userRepository.deleteById(userId)
}
#Transactional
fun updateUser(id: Long, firstName: String, lastName: String, email: String, gender: String, country: String, dateOfBirth: LocalDate) {
val user = userRepository.findById(id).orElseThrow { throw IllegalStateException("User with id $id does not exist") }
// if (userRepository.findUserByEmail(user.email).isPresent) throw IllegalStateException("Email Already taken")
if (firstName.isNotEmpty()) user.firstName = firstName
if (lastName.isNotEmpty()) user.lastName = lastName
if (email.isNotEmpty()) user.email = email
if (gender.isNotEmpty() && (gender == "M" || gender == "F")) user.gender = gender
if (country.isNotEmpty()) user.country = country
if (dateOfBirth.toString().isNotEmpty()) user.dateOfBirth = dateOfBirth
}
}
Response payload is
{
"timestamp": "2021-03-02T11:45:27.025+00:00",
"status": 500,
"error": "Internal Server Error",
"message": "Parameter specified as non-null is null: method com.example.demo.user.UserController.updateUser, parameter dateOfBirth",
"path": "/api/v1/update/1000"
}
The problem comes from your REST controller. The request params are annotated with required=false but the type is String. So when an HTTP request comes with null values, Spring can't deserialize data in those fields. (That explains the NullpointerException.
You have to provide nullable types (like "String?") In your controller and this nullability should not necessariry proliferate in all layers of your code.
For example "firstname" is optional for update but mandatory for creation.
I have not worked much on kotlin but what I think the issue is Kotlin is null-safe programming language. You need to explicitly tell if variable can be null or not. For more detail please refer this blog. Updating your entity class with field accepting null should work.
for example To allow nulls, you can declare a variable as nullable string, written String?:

Resources