I have two classes which have bidirectional ManyToMany relations in a Spring Boot application. When I would like to fetch my entities, they start recursively looping, and I get a stackoverflow exception. These is my implementation.
#Entity
#Table(name = "route")
data class Route(
#Column(name = "uid")
#Type(type = "pg-uuid")
#Id
var uid: UUID,
var image: String,
#Column(name = "rate_id")
var rate_id: UUID,
#ManyToMany(cascade = [CascadeType.ALL], fetch = FetchType.LAZY)
#JoinTable(name = "ach",
joinColumns = [JoinColumn(name = "route_id", referencedColumnName = "uid")],
inverseJoinColumns = [JoinColumn(name = "athlete_id", referencedColumnName = "uid")])
var athletes: List<Athlete> = mutableListOf())
#Entity
#Table(name = "athlete")
data class Athlete(
#Column(name = "uid")
#Type(type = "pg-uuid")
#Id
var uid: UUID,
var email: String,
var image: String,
#ManyToMany(mappedBy = "athletes")
var routes: List<Route> = mutableListOf())
I understand that the problem is that both of my list attribute is in the constructor. However I would like to have the list attributes in the response. I have seen solutions where the toString method was overwritten to create a json string. I would prefer to return an object instead of a jsonString. Is there a way to implement the above problem with or without dataclasses? Please give some example if there is a way.
Please notice that this answer is solution for Kotlin data classes with ManyToMany bidirectional relation.
#ManyToMany(cascade = [CascadeType.ALL], fetch = FetchType.LAZY)
#JoinTable(name = "ach",
joinColumns = [JoinColumn(name = "route_id", referencedColumnName = "uid")],
inverseJoinColumns = [JoinColumn(name = "athlete_id", referencedColumnName = "uid")])
#JsonIgnoreProperties("routes")
var athletes: List<Athlete> = mutableListOf())
#ManyToMany(mappedBy = "athletes")
#JsonIgnoreProperties("athletes")
var routes: List<Route> = mutableListOf())
With adding the #JsonIgnoreProperties, you can avoid the recursive loop.
For me the above solution didn't work. Instead I had to override the equals and hashCode methods to avoid the recursion like so:
#Entity
#Table(name = "route")
data class Route(
#Column(name = "uid")
#Type(type = "pg-uuid")
#Id
var uid: UUID,
var image: String,
#Column(name = "rate_id")
var rate_id: UUID,
#ManyToMany(cascade = [CascadeType.ALL], fetch = FetchType.LAZY)
#JoinTable(name = "ach",
joinColumns = [JoinColumn(name = "route_id", referencedColumnName = "uid")],
inverseJoinColumns = [JoinColumn(name = "athlete_id", referencedColumnName = "uid")])
var athletes: List<Athlete> = mutableListOf()) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Route
if (uid != other.uid) return false
if (image != other.image) return false
if (rate_id != other.rate_id) return false
return true
}
override fun hashCode(): Int {
var result = uid
result = 31 * result + image.hashCode()
result = 31 * result + rate_id.hashCode()
return result
}
}
By default when you generate the equals and hashCode (by right-clicking > "Generate..." > "equals() and hashCode()" it will look like this:
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Route
if (uid != other.uid) return false
if (image != other.image) return false
if (rate_id != other.rate_id) return false
if (route != other.route) return false
return true
}
override fun hashCode(): Int {
var result = uid
result = 31 * result + image.hashCode()
result = 31 * result + rate_id.hashCode()
result = 31 * result + route.hashCode()
return result
}
You have to remove the route object from both methods to stop the recursion.
IMPORTANT: You can do this on either side (Athlete or Route) to get the same result.
Related
I have a question regarding Spring Data JPA.
To make it as simple as possible I made up a very simple example.
We have the TestUser, that can have a FavouriteColor, but his favouriteColor can also be null.
TestUser.kt
#Entity
class TestUser(
#Id
#Column(name = "TestUserId")
var userId: Long,
#Column(name = "Name")
var name: String,
#Column(name = "FavouriteColorId")
var favouriteColorId: Long? = null,
#OneToOne
#JoinColumn(
name = "FavouriteColorId",
referencedColumnName = "FavouriteColorId",
insertable = false,
updatable = false,
nullable = true
)
var favouriteColor: FavouriteColor? = null
)
FavouriteColor.kt
#Entity
class FavouriteColor(
#Id
#Column(name = "FavouriteColorId")
var favouriteColorId: Long,
#Column(name = "ColorCode")
var colorCode: String
)
When I search for the users that have a favourite Color by findTestUsersByFavouriteColorNotNull(), the size of the result is 0. Even if there is an User that has a favourite color. And when I use findAll() and then apply the filter, the result is correct.
StackOverflowTest.kt
#SpringBootTest
#Transactional
class StackOverflowTest {
#Autowired
lateinit var testUserRepository: TestUserRepository
#Autowired
lateinit var favouriteColorRepository: FavouriteColorRepository
#Test
fun testFilter() {
val favouriteColor = FavouriteColor(favouriteColorId = 0L, colorCode = "#000000")
favouriteColorRepository.save(favouriteColor)
val user = testUserRepository.save(TestUser(userId = 0L, name = "Testuser"))
user.favouriteColor = favouriteColor
testUserRepository.save(user)
val usersWithColor1 = testUserRepository.findAll().filter { it.favouriteColor != null }
assert(usersWithColor1.size == 1) // This assertion is correct
val usersWithColor2 = testUserRepository.findTestUsersByFavouriteColorIdIsNotNull()
assert(usersWithColor2.size == 1) // This assertion fails
val usersWithColor3 = testUserRepository.findTestUsersByFavouriteColorIsNotNull()
assert(usersWithColor3.size == 1) // This assertion fails
}
}
Update:
I added the Repository function findTestUsersByFavouriteColorIdNotNull() but it also does not work
Update2:
I updated the functions to findTestUsersByFavouriteColorIdIsNotNull and findTestUsersByFavouriteColorIsNotNull, but the assertions are still failing
Can somebody explain me, why the findTestUsersByFavouriteColorNotNull() does not work ? And is there some way to get this function working in the tests?
Thanks :)
I'm suspecting that happen because you have 2 variables of the same column name
#Column(name = "FavouriteColorId")
var favouriteColorId: Long? = null,
#OneToOne
#JoinColumn(
name = "FavouriteColorId",
referencedColumnName = "FavouriteColorId",
insertable = false,
updatable = false,
nullable = true
)
var favouriteColor: FavouriteColor? = null
Try removing one of the variable, and try again.
I trying to get two fields and a #ElementCollection from entity using projection with interface, but the JPA are selecting all fields from my entity and when i remove the method that get the list of my #ElementCollection the JPA select the only two fields.
My entity class:
#Entity(name = "users")
data class User(
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null,
#Column(nullable = false)
var name: String? = null,
#Column(unique = true, nullable = false)
var cpf: String? = null,
#Column(name = "phone_number", nullable = false)
var phoneNumber: String? = null,
#Column(unique = true, nullable = false)
var email: String? = null,
#Column(name = "password_hash")
var passwordHash: String? = null,
#Column(name = "password_hash_recovery")
var passwordHashRecovery: String? = null,
#Column(name = "password_hash_recovery_date")
var passwordHashRecoveryDate: String? = null,
#Column(name = "self_employed", nullable = false)
var selfEmployed: Boolean? = null,
#JoinColumn(name = "user_photo", referencedColumnName = "id")
#OneToOne(fetch = FetchType.LAZY)
var userPhoto: File? = null,
#JoinColumn(name = "id_location", referencedColumnName = "id")
#OneToOne(fetch = FetchType.LAZY)
var location: Location? = null,
#Column(name = "rating_star", nullable = false)
#Enumerated(EnumType.STRING)
var ratingStar: RatingStar = RatingStar.ONE,
#JoinColumn(name = "id_area", referencedColumnName = "id")
#OneToOne(fetch = FetchType.LAZY)
var area: Area? = null,
#OneToMany(mappedBy = "user", cascade = [CascadeType.ALL], fetch = FetchType.LAZY)
var workTimes: List<WorkTime> = arrayListOf(),
#OneToMany(mappedBy = "contractor", cascade = [CascadeType.ALL], fetch = FetchType.LAZY)
var contractorOrders: List<Order>? = arrayListOf(),
#OneToMany(mappedBy = "selfEmployed", cascade = [CascadeType.ALL], fetch = FetchType.LAZY)
var selfEmployedOrders: List<Order>? = arrayListOf(),
) {
#ElementCollection(fetch = FetchType.EAGER)
#Enumerated(EnumType.STRING)
#CollectionTable(
name = "profiles_authorties",
joinColumns = [JoinColumn(name = "user_id", referencedColumnName = "id")],
)
#Column(name = "authority")
private val _authorities: MutableSet<ProfileAuthorities> = HashSet()
init {
_authorities.add(ProfileAuthorities.CLIENT)
}
fun setAsAdmin() =
_authorities.add(ProfileAuthorities.ADMIN)
fun getAuthorities(): Set<ProfileAuthorities> = _authorities
}
My interface for projection:
interface LoginUserProjection {
fun getId(): Long
fun getPasswordHash(): String
fun getAuthorities(): Set<ProfileAuthorities>
}
The result query is:
Hibernate: select user0_.id as id1_12_, user0_.id_area as id_area11_12_, user0_.cpf as cpf2_12_, user0_.email as email3_12_, user0_.id_location as id_loca12_12_, user0_.name as name4_12_, user0_.password_hash as password5_12_, user0_.password_hash_recovery as password6_12_, user0_.password_hash_recovery_date as password7_12_, user0_.phone_number as phone_nu8_12_, user0_.rating_star as rating_s9_12_, user0_.self_employed as self_em10_12_, user0_.user_photo as user_ph13_12_ from users user0_ where user0_.id=?
Hibernate: select authoriti0_.user_id as user_id1_8_0_, authoriti0_.authority as authorit2_8_0_ from profiles_authorties authoriti0_ where authoriti0_.user_id=?
when i remove fun getAuthorities(): Set<ProfileAuthorities> from LoginUserProjection the result is:
Hibernate: select user0_.id as col_0_0_, user0_.password_hash as col_1_0_ from users user0_ where user0_.id=?
My repository method:
#Repository
interface UserRepository : JpaRepository<User, Long> {
fun <T> getUserProjectionById(id: Long, projection: Class<T>): T?
}
I use Spring Boot, Hibernate, Kotlin
In build.gradle.kts:
apply(plugin="org.hibernate.orm")
tasks.withType<org.hibernate.orm.tooling.gradle.EnhanceTask>().configureEach {
options.enableLazyInitialization = true
options.enableDirtyTracking = true
options.enableAssociationManagement = true
}
User entity:
#Entity
#Table(name = "user")
class User(
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "id")
var id: Long = -1,
#Column(name = "user_name", unique = true, nullable = false, length = 20)
var userName: String = "",
#Column(unique = true, nullable = false)
var email: String = "",
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "user_roles", joinColumns = [JoinColumn(name = "user_id")],
inverseJoinColumns = [JoinColumn(name = "role_id")])
var roles: MutableSet<Role> = mutableSetOf()
) {
#OneToOne(fetch = FetchType.LAZY, cascade = [CascadeType.ALL], mappedBy = "user", optional = false)
#LazyToOne(LazyToOneOption.PROXY)
#LazyGroup( "person" )
lateinit var person: Person
....
}
I get it by:
#Transactional
#Component
class UserServiceImpl (private val userRepository: UserRepository){
override fun getUserData(id: Long): Optional<UserView> {
return userRepository.findById(id).map { UserView.build(it, it.person) }
}
...
}
And it.person throws lateinit property has not been initialized exception, but Person was loaded( I see it by hibernate log ). Getting roles works fine.
I have same result without #LazyToOne(LazyToOneOption.PROXY) and #LazyGroup( "person" ).
SOLVED:
#OneToOne(fetch = FetchType.LAZY, cascade = [CascadeType.ALL])
#JoinColumn(name = "person_id", referencedColumnName = "id")
#LazyToOne(LazyToOneOption.PROXY)
#LazyGroup( "person" )
lateinit var person: Person
We have following schema as described on diagram below. There's an Entity with bidirectional many-to-many relations with two other entities(Relation1, Relation2). There's also many-to-many relation between Entity-Relation2 relationship itself and Relation1 entity. When we create an instance of Entity and persist into database, it's working fine, except that operation takes too much time. I'd like to ask, if there are any possible optimizations on Hibernate level, which could boost performance of save operation.
Here's diagram:
Model definitions:
class Entity : AbstractJpaPersistable() {
var name: String? = null
var description: String? = null
#OneToMany(mappedBy = "entity", fetch = FetchType.LAZY, cascade = [CascadeType.PERSIST], orphanRemoval = true)
var entityRelations1: List<EntityToRelation1> = emptyList()
#OneToMany(mappedBy = "entity", fetch = FetchType.LAZY, cascade = [CascadeType.PERSIST], orphanRemoval = true)
var entityRelations2: List<EntityToRelation2> = emptyList()
}
#Entity
class Relation2: AbstractJpaPersistable(){
#OneToMany(mappedBy = "relation2", fetch = FetchType.LAZY, cascade = [CascadeType.PERSIST], orphanRemoval = true)
var entityRelations2: List<EntityToRelation2> = emptyList()
}
#Entity
class Relation1: AbstractJpaPersistable(){
#OneToMany(mappedBy = "relation1", fetch = FetchType.LAZY, cascade = [CascadeType.PERSIST], orphanRemoval = true)
var entityRelations1: List<EntityToRelation1> = emptyList()
#OneToMany(mappedBy = "relation1", fetch = FetchType.LAZY, cascade = [CascadeType.PERSIST], orphanRemoval = true)
var entityRelations2Relations1: List<EntityToRelation2ToRelation1> = emptyList()
}
#Entity
class EntityToRelation2 {
#EmbeddedId
var entityToRelation2Id: EntityToRelation2Id = EntityToRelation2Id()
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("entityId")
#JoinColumn(name = "entity_id", insertable = false, updatable = false)
var entity: Entity? = null
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("relation2Id")
#JoinColumn(name = "relation2_id", insertable = false, updatable = false)
var relation2: Relation2? = null
#OneToMany(mappedBy = "entityToRelation2", fetch = FetchType.LAZY, cascade = [CascadeType.PERSIST], orphanRemoval = true)
var entityRelations2Relations1: List<EntityToRelation2ToRelation1> = emptyList()
}
#Embeddable
class EntityToRelation2Id : Serializable {
#Column(name = "entity_id")
var entityId: Int? = null
#Column(name = "relation2_id")
var relationId: Int? = null
}
#Entity
class EntityToRelation1 {
#EmbeddedId
var entityToRelation1Id = EntityToRelation1Id()
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("entityId")
#JoinColumn(name = "entity_id", insertable = false, updatable = false)
var entity: Entity? = null
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("relation1Id")
#JoinColumn(name = "relation1_id", insertable = false, updatable = false)
var relation1: Relation1? = null
}
#Embeddable
class EntityToRelation1Id : Serializable {
#Column(name = "entity_id")
var entityId: Int? = null
#Column(name = "relation1_id")
var relation1Id: Int? = null
}
#Entity
class EntityToRelation2ToRelation1 {
#EmbeddedId
var entityToRelation2ToRelation1Id = EntityToRelation2ToRelation1Id()
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("entityToRelation2Id")
#JoinColumns(
JoinColumn(name = "entity_id"),
JoinColumn(name = "relation2_id")
)
var entityToRelation2: EntityToRelation2? = null
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("relation1Id")
#JoinColumn(name = "relation1_id", insertable = false, updatable = false)
var relation1: Relation1? = null
}
#Embeddable
class EntityToRelation2ToRelation1Id : Serializable {
var entityToRelation2Id: EntityToRelation2Id? = null
#Column(name = "relation1_id")
var relation1Id: Int? = null
}
i have a many to many relationship between 2 tables.
below are the two tables with mappings.
StaffSearchCriteria is used to search staffs having skills selected.
this search criteria is persisted in DB so that we can again lookup it later.
the issue i am facing is that i am not able to properly save this data.
i am not understanding the "cascade" part of the mapping.
due to which, if i do " Cascade.ALL ", the data is saved properly, but when i delete the search criteria, then it also deletes the Skill entries associated with it, which is wrong.
i just want that if i delete Skill, StaffSearchCriteria entry should not get deleted and similarly for the Skill;
Only the selected data should be deleted and its entry in the mapping table.
the other table should not be affected by that action.
StaffSearchCriteria
#Entity
#Table(name = "staff_search_criteria")
#NamedQueries({
#NamedQuery(name = "StaffSearchCriteria.findAll", query = "SELECT s FROM StaffSearchCriteria s")})
public class StaffSearchCriteria implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Basic(optional = false)
#Column(name = "id")
private Long id;
#Basic(optional = false)
#NotNull
#Column(name = "version")
private long version;
#Lob
#Size(max = 2147483647)
#Column(name = "description")
private String description;
#Basic(optional = false)
#NotNull
#Size(min = 1, max = 200)
#Column(name = "name")
private String name;
#ManyToMany(mappedBy = "staffSearchCriteriaCollection", cascade = {CascadeType.MERGE, CascadeType.PERSIST}, fetch = FetchType.LAZY)
private Collection<Skill> skillCollection;
==================================================
Skill
#Entity
#Table(name = "skill")
#NamedQueries({
#NamedQuery(name = "Skill.findAll", query = "SELECT s FROM Skill s")})
public class Skill implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Basic(optional = false)
#Column(name = "id")
private Long id;
#Basic(optional = false)
#NotNull
#Column(name = "version")
private long version;
#Lob
#Size(max = 2147483647)
#Column(name = "description")
private String description;
#Basic(optional = false)
#NotNull
#Size(min = 1, max = 100)
#Column(name = "name")
private String name;
#JoinTable(name = "mission_skill", joinColumns = {
#JoinColumn(name = "skill_id", referencedColumnName = "id")}, inverseJoinColumns = {
#JoinColumn(name = "mission_skills_id", referencedColumnName = "id")})
#ManyToMany(fetch = FetchType.LAZY)
private Collection<Mission> missionCollection;
#JoinTable(name = "staff_search_criteria_skill", joinColumns = {
#JoinColumn(name = "skill_id", referencedColumnName = "id")}, inverseJoinColumns = {
#JoinColumn(name = "staff_search_criteria_skills_id", referencedColumnName = "id")})
#ManyToMany(cascade = {CascadeType.MERGE, CascadeType.PERSIST}, fetch = FetchType.LAZY)
private Collection<StaffSearchCriteria> staffSearchCriteriaCollection;
Save method
public StaffSearchCriteria saveStaffSearchCriteria(StaffSearchCriteria staffSearchCriteria) {
logger.info(" [StaffSearchCriteriaDAOImpl] saveStaffSearchCriteria method called. - staffSearchCriteria = " + staffSearchCriteria);
Session session = sessionFactory.getCurrentSession();
session.saveOrUpdate(staffSearchCriteria);
return staffSearchCriteria;
}
delete method
public void deleteStaffSearchCriteria(Long id) {
logger.info(" [StaffSearchCriteriaDAOImpl] deleteStaffSearchCriteria method called. - id = " + id);
Session session = sessionFactory.getCurrentSession();
Query query = session.createQuery("FROM StaffSearchCriteria ssc where ssc.id = " + id);
if(null != query.uniqueResult()){
StaffSearchCriteria staffSearchCriteria = (StaffSearchCriteria)query.uniqueResult();
session.delete(staffSearchCriteria);
}
}
Please help me here.What am i doing wrong?
Finally i solved it. what i did was as follows.
1. In controller, i found out which skills were removed from previous saved data.
2. passed that list of Skill as well as the StaffSearchCriteria to the service save method.
3. in Service, i iterated over each skill to be removed and removed the staffSearchCriteria object from it and saved it.
4. then passed the staff search criteria to dao and used saveOrUpdate method.
Below are the code snippets.
1.Controller
List<Skill> skillList2 = new ArrayList<Skill>();
if(null != request.getParameterValues("skillCollection")){
for(String skillId : request.getParameterValues("skillCollection")){
if((!skillId.equals(null)) && skillId.length() > 0){
Skill skill = skillService.findSkillById(Long.parseLong(skillId));
// skill will be lazily initialized :(
// initialize it
skill.setStaffSearchCriteriaCollection(staffSearchCriteriaService.getAllStaffSearchCriteriaBySkillId(skill.getId()));
// set staff search criteria in each skill. because it is the owner
if(null != skill.getStaffSearchCriteriaCollection()){
skill.getStaffSearchCriteriaCollection().add(staffSearchCriteria);
}else{
List<StaffSearchCriteria> staffSearchCriteriaList = new ArrayList<StaffSearchCriteria>();
staffSearchCriteriaList.add(staffSearchCriteria);
skill.setStaffSearchCriteriaCollection(staffSearchCriteriaList);
}
skillList2.add(skill);
}
}
}
staffSearchCriteria.setSkillCollection(skillList2);
// Remove OLD skills also. plz. :)
List<Skill> skillList3 = null;
if(null != staffSearchCriteria && staffSearchCriteria.getId() != null && staffSearchCriteria.getId() > 0){
// this skillList3 will contain only those which are removed.
skillList3 = skillService.getAllSkillByStaffSearchCriteriaId(staffSearchCriteria.getId());
skillList3.removeAll(skillList2);
}
// now set staffSearchCriteriacollection and then pass it.
List<Skill> removedskillList = new ArrayList<Skill>();
if(null != skillList3){
for(Skill skill : skillList3){
skill.setStaffSearchCriteriaCollection(staffSearchCriteriaService.getAllStaffSearchCriteriaBySkillId(skill.getId()));
removedskillList.add(skill);
}
}
// now pass to service and save these skills after removing this staff search criteria from them.
staffSearchCriteria = staffSearchCriteriaService.saveStaffSearchCriteria(staffSearchCriteria, removedskillList);
2.Service
if(null != removedskillList && removedskillList.size() > 0){
for(Skill skill : removedskillList){
skill.getStaffSearchCriteriaCollection().remove(staffSearchCriteria);
skillDAO.saveSkill(skill);
}
}
return staffSearchCriteriaDAO.saveStaffSearchCriteria(staffSearchCriteria);
3.DAO
Session session = sessionFactory.getCurrentSession();
session.saveOrUpdate(staffSearchCriteria);
4.Entity Class - Skill
#JoinTable(name = "staff_search_criteria_skill", joinColumns = {
#JoinColumn(name = "skill_id", referencedColumnName = "id")}, inverseJoinColumns = {
#JoinColumn(name = "staff_search_criteria_skills_id", referencedColumnName = "id")})
#ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.LAZY)
private Collection<StaffSearchCriteria> staffSearchCriteriaCollection = new ArrayList<StaffSearchCriteria>();
5.Entity Class - StaffSearchCriteria
#ManyToMany(mappedBy = "staffSearchCriteriaCollection", fetch = FetchType.LAZY, cascade = {CascadeType.ALL})
private Collection<Skill> skillCollection = new ArrayList<Skill>();
Hope this helps.