Java Spring Boot - JPA : StackOverflowError with a #ManyToMany relation - java

I am currently developing an application with the API in JavaSpringBoot and I need to add relationships between users (friends). For this I made a many-to-many relation which contains two fields:
CREATE TABLE friend_relation
(
fk_id_friend integer REFERENCES users (id) NOT NULL,
fk_id_user integer REFERENCES users (id) NOT NULL,
PRIMARY KEY (fk_id_friend, fk_id_user)
);
When I connect with a user and add relationships : everything is fine, but if I connect to one of the accounts added as a friend by the other user, there is a StackOverflowError.
I know it's because of the almost identical entries in the database, but I have no idea how to get my entity to be correct.
Currently each user must add the other individually, I imagine that I have to make a friend request system but again I am blocked.
Do I have to make an "effective" field in my friend_relation table. If so, how do I use it? Should I create a specific entity for this table or leave it in the User entity?
Currently, this is what my user entity looks like:
#Entity
#Table(name = "users")
#Data
#Accessors(chain = true)
public class UserEntity {
#Id
#SequenceGenerator(name = "users_generator", sequenceName = "users_sequence", allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "users_generator")
#Column(name = "id")
private Integer id;
[...]
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(
name = "friend_relation",
joinColumns = #JoinColumn(name = "fk_id_user", referencedColumnName = "id", nullable = false),
inverseJoinColumns = #JoinColumn(name = "fk_id_friend", referencedColumnName = "id", nullable = false)
)
private List<UserEntity> friends = new ArrayList<>();
}
When trying to modify my entity to avoid the error:
#ManyToMany
#JoinTable(name="friend_relation",
joinColumns=#JoinColumn(name="fk_id_user"),
inverseJoinColumns=#JoinColumn(name="fk_id_friend")
)
private List<UserEntity> friends;
#ManyToMany
#JoinTable(name="friend_relation",
joinColumns=#JoinColumn(name="fk_id_friend"),
inverseJoinColumns=#JoinColumn(name="fk_id_user")
)
private List<UserEntity> friendOf;
I looked for resources on the internet but I did not find it, I may have searched poorly and I apologize in advance if the answer has already been given.
If you can help me it is with great pleasure, thank you in advance!
(And sorry for the Google Translate I preferred not to use my rough English)✌
Ok sorry update, i don't post the stake trace : https://pastebin.com/Ls2qRpU4
It happens when I get my user on the front side. I am trying to connect but I cannot because this error occurs.

First of, I've noticed an #Data on your entity, which I suppose is from Project Lombok? Just be careful, Lombok generated methods can trigger lazy loading and there are problems with equals/hashCode for auto-generated IDs. (see here and here)
Now to your problem:
It's indeed a JSON serialization issue. You have circular references and Jackson runs in circles bc. it doesn't know where to stop.
You can either use a DTO projection where you resolve this circle yourself, like:
public class UserDto {
public Integer id;
public List<Integer> friends; // List of IDs
}
You can also use Jackson annotations to achieve almost the same thing, like #JsonIdentityInfo. Here's an article about it.
#JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
#Entity
#Table(name = "users")
#Data
#Accessors(chain = true)
public class UserEntity {
#Id
#SequenceGenerator(name = "users_generator", sequenceName = "users_sequence", allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "users_generator")
#Column(name = "id")
private Integer id;
[...]
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(
name = "friend_relation",
joinColumns = #JoinColumn(name = "fk_id_user", referencedColumnName = "id", nullable = false),
inverseJoinColumns = #JoinColumn(name = "fk_id_friend", referencedColumnName = "id", nullable = false)
)
private List<UserEntity> friends = new ArrayList<>();
}
When you have complex Entities with circular references or very large object trees, you often have to think hard about how to serialize them - especially when taking LazyLoading into account. I have spent a lot of time on this in complex professional projects. Simple automatically generated DTOs and serialization sometimes don't cut it.

I'm not an expert on this, so I'm sure others will give more accurate answers. However, the problem seems to occur when mapping the entity to JSON. You can't map to JSON a user with a collection of friend users that also have their friends in a fully mapped collection since that causes infinite recursion. I would try to use Jackson annotations to make the serialization of the friends list produce a list of ids instead of a list of complete users.

Related

Hibernate Multiple #OneToMany bound to same entity type

I have yet another #OneToMany question. In this case, I'm trying to model a person having a list of excluded people they shouldn't be able to send items to. This is a Spring Boot app using JPA.
In the code below, the exclusions list populates properly but the excludedBy List does not. Because of this, I believe that is causing the deletion of a Person that is excluded by another person to fail because the Exclusion in excludedBy is not mapped on the object properly.
#Entity
#Table(name = "person")
public class Person {
#Id
#GeneratedValue
#Column(nullable = false)
Long id;
...
#OneToMany(mappedBy = "sender", cascade = { CascadeType.ALL })
List<Exclusion> exclusions = new ArrayList<>();
//This is not getting populated
#JsonIgnore
#OneToMany(mappedBy = "receiver", cascade = { CascadeType.ALL })
List<Exclusion> excludedBy = new ArrayList<>();
...
}
#Entity
#Table(name = "exclusions")
public class Exclusion {
#Id
#GeneratedValue
#Column(nullable = false)
Long id;
#ManyToOne
#JsonIgnore
Person sender;
#ManyToOne
Person receiver;
...
}
I would expect that this would have mapped the bidirectional relationship properly and as such the excludedBy List would be populated as well.
Any wisdom on this matter would be great!
1 - An #Id is by default not nullable, not required:
#Column(nullable = false)
2 - There is no need for an #Id in this class. Both sides of the exclusion are together unique. Not needed:
#Id
#GeneratedValue
Long id;
3 - An "Exclusion" requires both an excludedBy and an excluded, give them names that match and they are your #Id. It is a 2 way ManyToMany relationship.
#Entity
#Table(name = "exclusions")
public class Exclusion {
#Id
#ManyToMany // An ID so not optional, so no need for (optional = false)
Person excludedBy;
#Id
#ManyToMany // An ID so not optional, so no need for (optional = false)
Person excluded;
}
Entity Exclusion always knows both sides of the story.
#ManyToMany(mappedBy = "excludedBy", cascade = { CascadeType.ALL })
List<Exclusion> excluded = new ArrayList<>();
#ManyToMany(mappedBy = "excluded", cascade = { CascadeType.ALL })
List<Exclusion> excludedBy = new ArrayList<>();
Tip: JSON DTOs shouldn't be defined in your JPA DTOs, otherwise you can't change your internal data model independently of your external API model.
I had this problem in the past. Your key problem ist that your ORM Mapper hibernate does not know which of your database entries need to be assinged to exclusions and which are assiged to excludedBy. You need a discriminator and add the constraint in your select. I would propose a solution that looks something like this:
#OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name = "PRIMARY_KEX_IN_EXCLUSION_TABLE", referencedColumnName = "id")
#Where(clause = "is_excluded_by = 0")
private Set<Exclusion> exclusions;
#OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name = "PRIMARY_KEX_IN_EXCLUSION_TABLE", referencedColumnName = "id")
#Where(clause = "is_excluded_by = 1")
private Set<Exclusion> excludedBy;
the value isExcludedBy needs to be a database column, part of your Entity and set in your code manually.
I think you also need to use Set instead of List when having multiple collections in one Entity. https://vladmihalcea.com/spring-data-jpa-multiplebagfetchexception/

Why MERGE operation works when saving parent with no primary key which has children with primary key?

I have following classes in bidirectional many to many relationship.
#Table(name = "message")
#Entity
public class Message {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "message_id", unique = true, nullable = false)
private int id;
#ManyToMany(fetch = FetchType.EAGER,cascade = CascadeType.MERGE)
#JoinTable(name = "tags_messages",
joinColumns = #JoinColumn(name = "message_id", referencedColumnName = "message_id"),
inverseJoinColumns = #JoinColumn(name = "tag_id", referencedColumnName = "tag_id"))
private Set<Tag> tags=new HashSet<>();
and
#Table
#Entity(name = "tag")
public class Tag {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "tag_id", unique = true, nullable = false)
private int id;
#Column(name = "name", unique = false, nullable = false)
private String name;
#JsonIgnore
#ManyToMany(fetch = FetchType.EAGER,cascade = CascadeType.MERGE)
private Set<Message> messages;
When trying to save new Message, I got exception saying: "detached entity to persist...Tag". I got it to work by setting CascadeType.MERGE, but I don't understand why it is working. It would be great if someone can explain me why :)
Steps I did which lead to exception:
In db I already had two Tags objects and no Messages. I also had connecting empty table messages_tags
On frontend (Android) I create new Message object (without id), add one Tag (entire object, pulled from db, with id) to Message.
Send new Message to backend (using Retrofit). Hit my controller function, then service function in which I tried to save new Message with accompanying child Tags. At first, my cascading type annotation on both side, was like this:
#ManyToMany(fetch = FetchType.EAGER,cascade = CascadeType.ALL)
I thought, since I have one part of relationship covered, that I need to cover other one as well. So I did this:
newMessage.getTags().forEach(t -> t.getMessages().add(newMessage));
messageRepository.save(newMessage) //Bum! exception
I commented out that line for setting other part of relationship, set MERGE as cascading type and save simply worked. WHY? Are there any other consequences I may experience while doing other CRUD operations on any of these entities?
When you add a new Tag to the Message on the frontend, you have a different persistent context from the one used on backend. That's why the tag entity is seen as detached (it has a PK but it is not in the backend's persistent context). Since you did not specify a cascade type for JPA to know that to do with Tag instance, the persist of the Message instance fails.
Once you specify the cascade type MERGE, the Tag instance is merged into the backend's persistent context and the save succeeds.
You can avoid the using of MERGE cascade by saving first the Tag instance.

Hibernate querying the entity after fetching its collection

We have the following 2 classes
public class StagingConcept implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO, generator = "sequence")
#SequenceGenerator(name = "sequence", sequenceName = "stg_concept_seq")
#Column(name = "id")
private long id;
#Column(name = "concept_id", nullable = false, length = 18)
private String conceptId;
#OneToMany(mappedBy = "concept", fetch = FetchType.LAZY,
cascade = { CascadeType.ALL })
private Set<StagingConceptDescription> descriptions;
// rest of the class
}
public class StagingConceptDescription {
#Id
#GeneratedValue(strategy = GenerationType.AUTO, generator = "sequence")
#SequenceGenerator(name = "sequence", sequenceName = "stg_concept_desc_seq")
#Column(name = "id")
private long id;
#ManyToOne
#JoinColumn(name = "concept_id", referencedColumnName = "concept_id")
#ForeignKey(name = "stg_concept_desc_fk1")
private StagingConcept concept;
// rest of the class
}
Some of the details, such as other class properties and entity annotations, have been omitted to keep the example precise. Please let me know if you need more details. Yes, the FK from StagingConceptDescription to StagingConcept is a non-PK Foreign Key.
When I create a Criteria:
"from " + StagingConcept.class.getCanonicalName()
I get all the StagingConcept entities from the DB in one single query. But I need to get the descriptions for each StagingConcept. For that, I write a query:
"from " + StagingConcept.class.getCanonicalName() + " join fetch descriptions"
The resulting SQL looks like:
select stagingcon0_.id as col_0_0_,
descriptio1_.id as id178_1_,
stagingcon0_.is_active as is2_149_0_,
stagingcon0_.brand_restriction_status as brand3_149_0_,
stagingcon0_.concept_id as concept4_149_0_,
stagingcon0_.container_type as container5_149_0_,
stagingcon0_.controlled_drug_status as controlled6_149_0_,
stagingcon0_.effective_date as effective7_149_0_,
stagingcon0_.form as form149_0_,
stagingcon0_.is_multi_component as is9_149_0_,
stagingcon0_.namespace as namespace149_0_,
stagingcon0_.preferred_term as preferred11_149_0_,
stagingcon0_.source as source149_0_,
stagingcon0_.source_version as source13_149_0_,
stagingcon0_.subsidy_status as subsidy14_149_0_,
stagingcon0_.type as type149_0_,
stagingcon0_.unit_of_use_size as unit16_149_0_,
stagingcon0_.unit_of_use_size_unit as unit17_149_0_,
descriptio1_.is_active as is2_178_1_,
descriptio1_.concept_id as concept6_178_1_,
descriptio1_.is_preferred as is3_178_1_,
descriptio1_.term as term178_1_,
descriptio1_.type as type178_1_,
descriptio1_.concept_id as concept6_149_0__,
descriptio1_.id as id0__
from stg_concept stagingcon0_
inner join stg_concept_description descriptio1_ on stagingcon0_.concept_id=descriptio1_.concept_id
It does fetch all the StagingConcepts and their descriptions, albeit in a slightly larger result set in that SQL.
All looks fine up until here. But then it goes and tries to find a staging concept for each and every description again. So if I have 30000 Staging Concepts and 60000 descriptions, it will send another 60000 queries to fetch the staging concept for every description. This looks a little nasty and takes up huge amount of time, enough to run past the transaction timeout.
To attempt to resolve this issue, I change the StagingConceptDescription to
public class StagingConceptDescription {
#Id
#GeneratedValue(strategy = GenerationType.AUTO, generator = "sequence")
#SequenceGenerator(name = "sequence", sequenceName = "stg_concept_desc_seq")
#Column(name = "id")
private long id;
#ManyToOne(fetch = FetchType.LAZY, optional=false)
#JoinColumn(name = "concept_id", referencedColumnName = "concept_id")
#ForeignKey(name = "stg_concept_desc_fk1")
private StagingConcept concept;
// rest of the class
}
So, the ManyToOne relationship is now set to LAZY, explicitly. And also, the relationship states that the concept is not optional, in an attempt to indicate that the relationship is non-optional. By setting that, I meant to tell hibernate that it should be OK to create a proxy object, if needed, because the other end of the relationship is always going to be present. But none of this had any effect. Not too sure if this would work.
I have also tried the #Fetch annotations, etc. None of them work. Even setting it to #LazyToOne(LazyToOneOption.PROXY) didn't have any effect.
Based off https://stackoverflow.com/a/29863982/5464931, you can do another join fetch.
e.g.
"from " + StagingConcept.class.getCanonicalName() + " join fetch descriptions join fetch descriptions.concept"
But this isn't the best solution because it still queries the entity/parent/concept again which is unnecessary.

Overcoming lazy loading issues in Hibernate, many-to-many, relationship

Teaching myself Hibernate, I have the following table structure/relationship
Which is represented by the following classes...
Users
#Entity
#Table(name = "users")
public class User implements IUser<Role>, Serializable {
#Id
#GeneratedValue(strategy = javax.persistence.GenerationType.IDENTITY)
#SequenceGenerator(name = "user_key_seq")
#Column(name = "key", insertable = false, updatable = false)
private Long key;
#Column(name = "name")
private String name;
#ManyToMany(cascade = {CascadeType.ALL})
#JoinTable(name = "userroles",
joinColumns = {
#JoinColumn(name = "userkey")},
inverseJoinColumns = {
#JoinColumn(name = "rolekey")})
private Set<Role> roles = new HashSet<>(5);
#Override
public Set<Role> getRoles() {
return roles;
}
// Other setters and getters
}
Roles
#Entity
#Table(name = "roles")
public class Role implements IRole, Serializable {
#Id
#GeneratedValue(strategy = javax.persistence.GenerationType.IDENTITY)
#SequenceGenerator(name = "roles_key_seq")
#Column(name = "key", insertable = false, updatable = false)
private Long key;
#Column(name="name")
private String name;
#ManyToMany(cascade = {CascadeType.ALL})
#JoinTable(name="parentroles",
joinColumns = {#JoinColumn(name="childkey")},
inverseJoinColumns = {#JoinColumn(name="parentkey")})
private Set<Role> roles = new HashSet<>(5);
// Other setters and getters
}
The problem I'm getting is this...
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: org.kaizen.chishiki.core.data.User.roles, could not initialize proxy - no Session
at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:575)
at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:214)
at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:554)
at org.hibernate.collection.internal.AbstractPersistentCollection.read(AbstractPersistentCollection.java:142)
at org.hibernate.collection.internal.PersistentSet.iterator(PersistentSet.java:180)
at og.kaizen.chishiki.core.testdatacore.Main.<init>(Main.java:37)
at og.kaizen.chishiki.core.testdatacore.Main.main(Main.java:25)
Now, I know I could set the fetch type to FetchType.EAGER, but I'd prefer not to do that, apart from performance concerns, I'm trying to learn my way around these problems ;)
I was wondering if there was a way to write a Hibernate query to satisfy this relationship and load the rows manually (it would also allow me to return the interface of Role instead of the implementation as I'm having to do now (would allow me to maintain that contract)).
From what I've read, it would seem that I would need to construct an Entity for UserRoles. Not that I'm against this solution, but I was curious if there was a way to get around it.
roles appears in a number of other relationships, so there is no way it can form a direct relationship with the parent table (that is, I can't put the key of the parent table in the role table directly, besides, users can have multiple roles)
The other solution I had in mind was to write a stored procedure to do it for me. While this is viable solution that I'm not above doing, it does make migrating the application to another database more difficult, not a show stopper, but I'm trying to keep these ideas in mind.
If you want to load users with their roles, but still keep the association lazy, use a HQL query containing a left join fetch:
select u from User u left join fetch u.roles where ...
If you have a User and want to make sure its roles collection is initialized before closing the session, call Hibernate.initialize(user.getRoles()).
If you are using Hibernate 4.1.6+ then one can handle those lazy association problems by using hibernate.enable_lazy_load_no_trans property in hibernate.properties.
For More refer : https://stackoverflow.com/a/11913404/286588
I solved this problem by annotating the method in the controller as transactional.As lazy-load basically returns a list of future objects ,the method needs to be transactional.

Unwanted behavior – Hibernate deletes child elements annotated with #ElementCollection when parent element is updated

everyone.
I am have Customer and Service tables in one to many relation - one customer can have no or many services. The Service table has a customer_id column which is a foreign key referencing the primary key of the Customer table.
When retrieving customers from the database I need to get only the IDs of the related services and that is why I decided to use the #ElementCollection annotation.
My mapping for the Customer is as follows:
#Entity
#Table(name = "customer")
public class CustomerEntity {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
#Column(nullable = false)
private String name;
#ElementCollection(fetch = FetchType.EAGER)
#CollectionTable(name = "service", joinColumns = #JoinColumn(name = "customer_id", updatable = false, insertable = true))
#Column(name = "id", updatable = false, insertable = true)
private Set<Integer> serviceIds;
}
Everything works perfect when I get data from the database. The problem is when I try to update a customer in the database. Upon update Hibernate deletes all the Service table rows which reference the updated customer if the serviceIds member of the customer entity was set to null or an empty Set. I would like to avoid this behavior. I would like this serviceIds member to be read only for Hibernate and to be ignored when the customer is updated in the database - i.e. I want to update only the customer table and nothing else. Is it possible to achieve this using ElementCollection?
By the way I tried the following mapping and the update of the Customer does not lead to any deletions in the Service table even if I set the serviceIds to null or an empty Set.
#OneToMany
#JoinColumn(name = "customer_id", referencedColumnName = "id", updatable = false, insertable = false)
private Set<ServiceEntity> serviceIds;
Thank You for Your help.
When modifying - even - a single element in an #ElementCollection, Hibernate will delete the old collection (the one persisted in DB) and then insert the new image of the collection. The question was already asked here:
Hibernate - #ElementCollection - Strange delete/insert behavior
In order to avoid this problem you can:
Use a List of Integer with the #OrderColumn annotation as described in the above link.
Create an Entity class wrapping your Integer (+ #Generated #Id).
Best regards

Categories