Issue with JPA. When fetchin eager parent all of its lazy children are also fetched [duplicate] - spring-boot

This question already has answers here:
How can I make a JPA OneToOne relation lazy
(12 answers)
Closed 4 years ago.
I am making a spring-boot project and currently I am facing a problem with lazy fetching. I have three classes one that is fetching its children with eager(Incident) and two that are with lazy(FileContent, FileStorage). The structure is:
Incident(1) -> (m)FileContent(1) -> (m)FileStorage.
Whenever I fetch from Incidet all of the fileContents and all of the fileStorages are also fetched. This should not be happening, fileStorages should not be fetched. It would be great if someone could tell me why the code behaves like this and help me fix It.
These are the classes:
#Getter
#Setter
#Entity
public class Incident extends BaseEntity {
#JsonIgnore
#LazyCollection(LazyCollectionOption.FALSE)
#OneToMany(cascade = CascadeType.ALL, mappedBy = "incident", orphanRemoval = true)
private List<FileContent> fileContent;
}
#Entity
#Getter
#Setter
public class FileContent extends BaseEntity {
#Column
private String fileName;
#OneToOne(mappedBy = "fileContent", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private FileStorage fileStorage;
#JsonIgnore
#ManyToOne
#JoinColumn(name = "incident_id")
#LazyCollection(LazyCollectionOption.TRUE)
private Incident incident;
}
#Getter
#Setter
#Entity
public class FileStorage extends BaseEntity {
#Lob
#Column
private byte[] content;
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "file_content_id")
private FileContent fileContent;
}
#Service(value = "ins")
public class IncidentService {
public Page<Incident> findAll(Pageable pageable) {
Page<Incident> incidents = incidentRepository.findAll(pageable);
if (CollectionUtils.isEmpty(incidents.getContent())) {
return null;
}
return incidents;
}
}
And this is the yml properties file
application.yml
- open-in-view: false
spring boot jpa java

Changed #OneToOne(fetch = FetchType.LAZY), to #OneToOne(optional = false, fetch = FetchType.LAZY) and it worked fine.

Related

Spring Boot, Hibernate, bidirectional One-To-Many. Strange behaviour. Why is there two selects insdead of an error?

Spring Boot, Hibernate, bidirectional One-To-Many. Strange behavior. Why is there two selects instead of an error?
I have a basic Spring boot application.
It simulates throwing dices.
I have two entity classes Dice and DiceBatch.
DiceBatch has List<Dice> dices;
Dice has DiceBatch diceBatch; as two sides of bidirectional ManyToOne, or OneToMany.
I use JpaRepository<DiceBatch, UUID> to get one instance of DiceBatch by callig a method of JpaRepository findById(UUID id)
I call this method inside DiceBatchService's method findDiceBatchById(UUID diceBatchId).
Method is marked as #Transactional.
When i do that Hibernate logs one SQL select:
/* select
d
from
DiceBatch d
where
d.id = ?1 */ select
dicebatch0_.dice_batch_id as dice_bat1_1_,
dicebatch0_.batch_creation_time as batch_cr2_1_,
dicebatch0_.batch_name as batch_na3_1_
from
dice_batch dicebatch0_
where
dicebatch0_.dice_batch_id=?
At this point everything is ok.
Method returns DiceBatch entity with lazily initialized List<Dice> dices.
This is important. Method is #Transactional when method returns I should leave transactionla context.
Lazy fields should stay lazy and should cause LazyInitializationException if I try to access them.
Now control goes back to the controller method of DiceBatchController findDiceBatchById(UUID diceBatchId)
And here something strange happens.
Hibernate logs another select
select
dices0_.dice_batch_id as dice_bat5_0_0_,
dices0_.dice_id as dice_id1_0_0_,
dices0_.dice_id as dice_id1_0_1_,
dices0_.dice_batch_id as dice_bat5_0_1_,
dices0_.sequential_number as sequenti2_0_1_,
dices0_.throw_result as throw_re3_0_1_,
dices0_.throw_time as throw_ti4_0_1_
from
dice dices0_
where
dices0_.dice_batch_id=?
...and response JSON contains DiceBatch with all Dice entities related to it.
So I have several question.
Why didn't I get LazyInitializationException?
How come the List<Dice> inside DiceBatch got initialized outside of Transactional context?
How Spring managed to build a complete entity of DiceBatch, including the content of the List<Dice> without any exceptions?
How to modify my code to avoid this strange implicit bahavior?
Here is all the relevant code.
package org.dice.model;
#Entity
#Builder
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
public class Dice {
#Id
#GenericGenerator(name = "UUID",
strategy = "org.hibernate.id.UUIDGenerator")
#GeneratedValue(strategy = javax.persistence.GenerationType.AUTO,
generator = "UUID")
#Column(name = "dice_id",
nullable = false)
private UUID id;
#Column(name = "throw_result",
nullable = false)
private Integer throwResult;
#Column(name = "throw_time",
nullable = false)
private LocalDateTime throwTime;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "dice_batch_id",
nullable = false,
foreignKey = #ForeignKey(name = "fk_dice_dice_batch_id_dice_batch_dice_batch_id")
)
#JsonBackReference
private DiceBatch diceBatch;
#Embedded
private SequentialNumber sequentialNumber;
}
package org.dice.model;
#Entity
#Builder
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
public class DiceBatch {
#Id
#GenericGenerator(name = "UUID",
strategy = "org.hibernate.id.UUIDGenerator")
#GeneratedValue(strategy = GenerationType.AUTO,
generator = "UUID")
#Column(name = "dice_batch_id",
nullable = false)
private UUID id;
#Column(name = "batch_name",
nullable = false)
private String batchName;
#Column(name = "batch_creation_time",
nullable = false)
private LocalDateTime batchCreationTime;
#OneToMany(
mappedBy = "diceBatch",
cascade = CascadeType.ALL,
orphanRemoval = true,
fetch = FetchType.LAZY)
#JsonManagedReference
private List<Dice> dices = new ArrayList<>();
public void addDice(Dice dice) {
dices.add(dice);
dice.setDiceBatch(this);
}
public void removeDice(Dice dice) {
dices.remove(dice);
dice.setDiceBatch(null);
}
}
package org.dice.repo;
#Repository
public interface DiceBatchRepo extends JpaRepository<DiceBatch, UUID> {}
package org.dice.service;
#Service
#RequiredArgsConstructor
public class DiceBatchService {
#Transactional
public DiceBatch findDiceBatchById(UUID diceBatchId) {
DiceBatch diceBatch = diceBatchRepo
.findById_my(diceBatchId)
.orElseThrow();
return diceBatch;
}
}
package org.dice.controller;
public class DiceBatchController {
#GetMapping(path = "/get/{diceBatchId}")
public ResponseEntity<DiceBatch> findDiceBatchById(
#PathVariable(name = "diceBatchId") UUID diceBatchId) {
log.info("<C>[/batch/get] endpoint reached.\n" +
"Dice Batch Id: {}\n",
diceBatchId);
return ResponseEntity.ok(diceBatchService.findDiceBatchById(diceBatchId));
}
}

JpaRepository returning child for the first item in the list and then only the id for the rest

I have the following Post class:
#Entity
#Table(name = "posts")
#Getter
#Setter
#JsonIdentityInfo( generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id",
scope = Long.class)
public class Post {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String subtitle;
private String content;
private String img_url;
#CreationTimestamp
private Timestamp created_on;
#UpdateTimestamp
private Timestamp last_updated_on;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "owner_id", nullable=false)
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
private User creator;
}
And the following repository that extends JpaRepository
#Repository
public interface PostRepository extends JpaRepository<Post, Long> {
Optional<Post> findById(Long id);
List<Post> findAll();
}
When returning the result of findAll() inside the following controller, only the first creator item is sent completely and the rest just contain the id:
#GetMapping("/news")
public List<Post> getNews() {
return postRepository.findAll();
}
This is the JSON I get as result:
[
{"id":15,"title":"Title example #1","subtitle":"Subtitle example #1","content":"Lorem #1 ipsum dolor sit amet","img_url":null,"created_on":"2021-12-01T00:00:00.000+00:00","last_updated_on":"2021-12-01T00:00:00.000+00:00","creator":{"id":1,"username":"user-example","email":"blablabla#gmail.com","roles":[{"id":1,"name":"ROLE_USER"}]}}
,{"id":25,"title":"Title example #2","subtitle":"Subtitle example #2","content":"Lorem #2 ipsum dolor sit amet","img_url":null,"created_on":"2021-12-01T00:00:00.000+00:00","last_updated_on":"2021-12-01T00:00:00.000+00:00","creator":1}
]
Why is this happening? Is there a way I can get the whole child object for every element in the JSON array?
Thanks
EDIT: added the User class
#Entity
#Table( name = "users",
uniqueConstraints = {
#UniqueConstraint(columnNames = "username"),
#UniqueConstraint(columnNames = "email")
})
#DiscriminatorValue(value="USER")
public class User extends OwnerEntity {
#NotBlank
#NotNull
#Size(max = 20)
private String username;
#NotBlank
#NotNull
#Size(max = 50)
#Email
private String email;
#NotBlank
#Size(max = 120)
#JsonIgnore
private String password;
#CreationTimestamp
private Timestamp created_on;
#UpdateTimestamp
private Timestamp last_updated_on;
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable( name = "user_roles",
joinColumns = #JoinColumn(name = "user_id"),
inverseJoinColumns = #JoinColumn(name = "role_id"))
private Set<Role> roles = new HashSet<>();
#ManyToMany(fetch = FetchType.LAZY)
private Set<Institution> institutions;
#OneToMany(mappedBy="creator", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
protected Set<Post> posts;
#ManyToMany(fetch = FetchType.LAZY)
private Set<Institution> following;
}
EDIT 2: Added the OwnerEntity class
#Entity
#Table(name = "entities")
#Inheritance(strategy = InheritanceType.JOINED)
#DiscriminatorColumn
#Getter
#Setter
#JsonIdentityInfo( generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id",
scope = Long.class)
public class OwnerEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
protected Long id;
}
Your OwnerEntity also has #JsonIdentityInfo. In its reference documentation we can read the following:
Annotation used for indicating that values of annotated type or
property should be serializing so that instances either contain
additional object identifier (in addition actual object properties),
or as a reference that consists of an object id that refers to a full
serialization. In practice this is done by serializing the first
instance as full object and object identity, and other references to
the object as reference values.
This perfectly explains why you are getting the JSON like that. If you don't want this, just remove #JsonIdentityInfo but it might be there to fix an infinite recursion while serializing bidirectional relationships (you can read more about this in the following online resource https://www.baeldung.com/jackson-bidirectional-relationships-and-infinite-recursion).

LazyInitializationException when get EAGER fetch object OneToOne

I have two entity
#Entity
#Table(name = "user")
#Data
#Builder
#EqualsAndHashCode(callSuper=false)
#ToString
#AllArgsConstructor
#NoArgsConstructor
public class User implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Column(name = "name")
private String name;
#OneToOne(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#PrimaryKeyJoinColumn
private UserLastLogin userLastLogin;
}
#Entity
#Table(name = "lastLogin")
#EqualsAndHashCode(onlyExplicitlyIncluded = true)
#ToString(onlyExplicitlyIncluded = true)
#Data
#Builder
#AllArgsConstructor
#NoArgsConstructor
public class UserLastLogin implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Column(name = "name")
private String userName;
#Column(name = "date")
private LocalDateTime date;
#OneToOne
#MapsId
#JoinColumn(name = "name")
private User user;
}
I use spring boot with spring data and jpa, hibernate in latest version.
In documentation is that #OneToOne is default EAGER, but when i get eager fetch object, i get lazyInitializationException when i not use #Transactional in get method. I don't understant why...
public UserDto getUser(String userName) {
var user= userRepository.getById(userName);
d.getSystemUserLastLogin(); // this throw lazy initialization exception
return mapper.entityToDto(d);
}
When i'will mark this method #Transactioal, this work. But, not recommendend used transactions in get method. I need use EAGER fetch in this relationship.
When i view query hibernate, i have one select, but children object is not available.
Hibernate:
select
user0_.name as nazwa1_4_0_,
user2_.name as name1_23_2_,
user2_.data as data3_23_2_
from
user0_
left outer join
last_login user2_
on user0_.name=user2_.name
where
user0_.name=?
The problem was that despite the fetch eager, lazy was used. This was due to the use of the getById method from the repository, which retrieves only the object's references and snaps all the fields when lazy is retrieved. Changing to findById solves the problem as findById takes an object, not a reference.
I would recommend you to use secondary tables instead like this:
#Entity
#Table(name = "user")
#Data
#Builder
#EqualsAndHashCode(callSuper=false)
#ToString
#AllArgsConstructor
#NoArgsConstructor
#SecondaryTable(name = "lastLogin", pkJoinColumns = #PrimaryKeyJoinColumn(name = "name"))
public class User implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Column(name = "name")
private String name;
#Column(table = "lastLogin", name = "date")
private LocalDateTime date;
}
Also see https://www.baeldung.com/jpa-mapping-single-entity-to-multiple-tables for more details.

Cascade parent-child with not-autogenerated id using spring boot, crud repository, hibernate and postgres

I have an issue with crudRepository (Spring boot-hibernate-postgres) and cascade when I try to save some related entities with a specific id (not autogenerated).
I spent some time googling without success.
This is the method:
private String importSourceInfo() {
SourceCompany sourceCompany= new SourceCompany();
sourceCompany.setName("companyName");
sourceCompany.setId(UUID.fromString("2bf05cbc-d530-11eb-b8bc-0242ac130003"));
Set<SourceUser> sourceUsers = new HashSet<>();
SourceUser sourceUser= new SourceUser();
sourceUser.setName("dev-team");
sourceUser.setId(UUID.fromString("4bede7a0-d530-11eb-b8bc-0242ac130003"));
sourceUser.setCompany(sourceCompany);
sourceUsers.add(sourceUser);
sourceCompany.setUsers(sourceUsers);
SourceInfo sourceInfo= new SourceInfo();
sourceInfo.setSourceUser(sourceUser);
sourceInfo.setSourceCompany(sourceCompany);
sourceInfo.setId(UUID.fromString("74a52aa0-d530-11eb-b8bc-0242ac130003"));
sourceInfo.setVersion("v1");
sourceInfo.setDescription("company v1");
sourceInfoRepository.save(sourceInfo);
return "SourceInfo has been imported!";
}
and these are the models:
SourceInfo:
#Data
#Entity
#EqualsAndHashCode(onlyExplicitlyIncluded = true)
#Table(name = "source_info")
public class SourceInfo {
#Id
#EqualsAndHashCode.Include
private UUID id;
#ManyToOne(cascade=CascadeType.PERSIST)
#JoinColumn(name = "fk_source_user", referencedColumnName = "id")
private SourceUser sourceUser;
#ManyToOne(cascade=CascadeType.PERSIST)
#JoinColumn(name = "fk_source_company", referencedColumnName = "id")
private SourceCompany sourceCompany;
#Column(columnDefinition = "text")
private String description;
private String version;
}
SourceCompany:
#Data
#Entity
#EqualsAndHashCode(onlyExplicitlyIncluded = true)
#Table(name = "source_company")
public class SourceCompany {
#Id
#EqualsAndHashCode.Include
private UUID id;
private String name;
#OneToMany(cascade = CascadeType.PERSIST, mappedBy = "company")
private Set<SourceUser> users;
}
SourceUser:
#Data
#Entity
#EqualsAndHashCode(onlyExplicitlyIncluded = true)
#Table(name = "source_user")
public class SourceUser {
#Id
#EqualsAndHashCode.Include
private UUID id;
private String name;
//ref
#ManyToOne
#JoinColumn(name = "fk_source_company")
private SourceCompany company;
}
And this is the error:
javax.persistence.EntityNotFoundException: Unable to find xxx.sp_model.sustainability.model.utils.SourceCompany with id 2bf05cbc-d530-11eb-b8bc-0242ac130003
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl$JpaEntityNotFoundDelegate.handleEntityNotFound(EntityManagerFactoryBuilderImpl.java:163) ~[hibernate-core-5.4.27.Final.jar!/:5.4.27.Final]
at org.hibernate.event.internal.DefaultLoadEventListener.load(DefaultLoadEventListener.java:216) ~[hibernate-core-5.4.27.Final.jar!/:5.4.27.Final]
at org.hibernate.event.internal.DefaultLoadEventListener.proxyOrLoad(DefaultLoadEventListener.java:332) ~[hibernate-core-5.4.27.Final.jar!/:5.4.27.Final]
at org.hibernate.event.internal.DefaultLoadEventListener.doOnLoad(DefaultLoadEventListener.java:108) ~[hibernate-core-5.4.27.Final.jar!/:5.4.27.Final]
at org.hibernate.event.internal.DefaultLoadEventListener.onLoad(DefaultLoadEventListener.java:74) ~[hibernate-core-5.4.27.Final.jar!/:5.4.27.Final]
at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:113) ~[hibernate-core-5.4.27.Final.jar!/:5.4.27.Final]
at org.hibernate.internal.SessionImpl.fireLoadNoChecks(SessionImpl.java:1186) ~[hibernate-core-5.4.27.Final.jar!/:5.4.27.Final]
at org.hibernate.internal.SessionImpl.internalLoad(SessionImpl.java:1051) ~[hibernate-core-5.4.27.Final.jar!/:5.4.27.Final]
at org.hibernate.type.EntityType.resolveIdentifier(EntityType.java:697) ~[hibernate-core-5.4.27.Final.jar!/:5.4.27.Final]
I think that hibernate want to update the child sourceCompany instead of saving it if new. But I don't know how to solve it.
(I need also to extend the eventual solution in a more complex structure so I need a general solution, because at principle the id of the application are autogenerated and all works, now the need is to set these IDs in the backend and so to save specific IDs.)
I think you are running into a limitation of Spring Data JPA repositories not being able to detect whether to call EntityManager.persist vs EntityManager.merge. You need to call EntityManager.persist to make this work.

Infinite JSON in ManyToMany relationship mapped by Intermediary Table

I have 2 entities that relate to one another. These 2 entities should map to each other in a Many-To-Many relationship, however, I need to also have a timestamp of their respective relationship (when it happened), so I am trying to map them using an intermediary table.
Initially, the relationship was One-To-Many, but I realized that I actually need a Many-To-Many as the business logic requires this. The structure is still the same, as in there is a Parent-Child relationship, but this time, a child should have multiple parents as well.
My BaseEntity is an abstract class that contains the fields present in all the other entities:
#Data
#MappedSuperclass
public abstract class BaseEntity {
#Id
#Min(100)
#Max(Integer.MAX_VALUE)
#GeneratedValue(strategy = GenerationType.IDENTITY)
protected Long id;
#CreationTimestamp
#Column(name = "Created_At", updatable = false)
protected ZonedDateTime createdDate;
#UpdateTimestamp
#Column(name = "Updated_At")
protected ZonedDateTime updatedDate;
#NotNull
#Column(name = "Is_Active")
protected Boolean active = true;
}
Then I have my 2 entities that should relate in a Many-To-Many style. This is my first entity and should be the parent:
#Data
#Entity
#NoArgsConstructor
#AllArgsConstructor
#Table(name = "User")
#EqualsAndHashCode(callSuper = true)
#TypeDefs( {
#TypeDef(name = "json", typeClass = JsonStringType.class),
#TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)
})
public class UserEntity extends BaseEntity {
#NotBlank
#Column(name = "User_Name", columnDefinition = "varchar(255) default 'N/A'")
private String userName;
#Nullable
#JoinColumn(name = "User_Id")
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<UserRole> roleList = new ArrayList<>();
}
My second entity is considered the child entity:
#Data
#Entity
#NoArgsConstructor
#AllArgsConstructor
#Table(name = "Role")
#Where(clause = "is_active = true")
#EqualsAndHashCode(callSuper = true)
public class RoleEntity extends BaseEntity {
#NotBlank
#Column(name = "Name")
private String name;
#JsonIgnore
#JoinColumn(name = "Role_Id")
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<UserRole> userList = new ArrayList<>();
}
I also have my intermediary entity:
#Data
#Entity
#Getter
#NoArgsConstructor
#AllArgsConstructor
#Where(clause = "is_active = true")
#EqualsAndHashCode(callSuper = true)
#Table(name = "User_Role", uniqueConstraints= #UniqueConstraint(columnNames={"User_Id", "Role_Id"}))
public class UserRole extends BaseEntity {
// Adding #JsonIgnore here will only cause an error
#JoinColumn(name = "User_Id")
#ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, optional = false, targetEntity = UserEntity.class)
private UserEntity user;
#JoinColumn(name = "Role_Id")
#ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, optional = false, targetEntity = RoleEntity.class)
private RoleEntity role;
}
Problem now is that when I try to get my UserEntity, I get infinite recursion.
So far I've tried using #JsonIgnore, #JsonManagedReference, #JsonBackReference and it did not work or I simply don't know where or how to use them properly.
Recap:
2 entities mapped by Many-To-Many relationship;
Many-To-Many implemented using an intermediary entity and One-To-Many + Many-To-One associations;
Getting recursion when showing my UserEntity;
Update: I managed to get this fixed using a different approach described in my answer to this question.
I fixed this by implementing a Composite Key structure and just using the #JsonIgnore annotation:
#Getter
#Setter
#Embeddable
#EqualsAndHashCode
#NoArgsConstructor
#AllArgsConstructor
public class UserRoleKey implements Serializable {
#Column(name = "User_Id")
Long userId;
#Column(name = "Role_Id")
Long roleId;
}
This gets to be used in the intermediary entity, which now doesn't use my BaseEntity anymore.
#Data
#Entity
#NoArgsConstructor
#AllArgsConstructor
#Table(name = "User_Role", uniqueConstraints= #UniqueConstraint(columnNames={"User_Id", "Role_Id"}))
public class UserRole {
#JsonIgnore
#EmbeddedId
private UserRoleKey id;
#JsonIgnore
#MapsId("userId")
#JoinColumn(name = "User_Id")
#ManyToOne(optional = false, targetEntity = UserEntity.class)
private UserEntity user;
#MapsId("roleId")
#JoinColumn(name = "Role_Id")
#ManyToOne(optional = false, targetEntity = RoleEntity.class)
private RoleEntity role;
#CreationTimestamp
#Column(name = "Created_At", updatable = false)
private ZonedDateTime createdDate;
}
Now, for my two entities, I have this definition:
UserEntity class (definition of the role):
#Nullable
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "user", orphanRemoval = true)
private List<UserRole> roleList = new ArrayList<>();
RoleEntity class (definition of the user)
#Nullable
#JsonIgnore
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "role", orphanRemoval = true)
private List<UserRole> userList = new ArrayList<>();
This seems to be working and no longer returns an infinite JSON recursion.

Resources