Why does DataJpaTest fail when saving OneToMany-related data in this pattern? - spring-boot

I have three Hibernate #Entity's below that mimic a failure in my production app:
#Entity
#Data
#SuperBuilder(toBuilder = true)
#EqualsAndHashCode(callSuper = true)
#NoArgsConstructor
#AllArgsConstructor
public class Dog extends Animal {
String barkType;
}
The Dog entity uses JOINED inheritance with this class, Animal:
#Entity
#Inheritance(strategy = InheritanceType.JOINED)
#Data
#SuperBuilder(toBuilder = true)
#NoArgsConstructor
#AllArgsConstructor
public class Animal {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Type(type = "uuid-char")
private UUID id;
#OneToMany(cascade = CascadeType.REMOVE)
#JoinColumn(name = "animalId", referencedColumnName = "id", insertable = false, updatable = false)
#Builder.Default
private List<Toy> toys = new ArrayList<>();
}
This Toy Entity is related to the parent class, Animal
#Entity
#Data
#Builder
#NoArgsConstructor
#AllArgsConstructor
public class Toy {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Type(type = "uuid-char")
private UUID id;
#Type(type = "uuid-char")
private UUID animalId;
private String shape;
}
And here is my implementation I am testing:
#Service
#AllArgsConstructor
public class DogService {
DogRepository repository;
ToyRepository toyRepository;
#Transactional
public Dog saveDogDTO(DogDTO dogDTO) {
Dog entity = Dog.builder()
.barkType(dogDTO.getBarkType())
.build();
repository.save(entity);
toyRepository.save(Toy.builder()
.shape(dogDTO.getToyShape())
.animalId(entity.getId())
.build());
return entity;
}
}
Here is my failing Test, which fails on the LAST line:
#DataJpaTest
class DogServiceTests {
private DogService dogService;
#Autowired
private DogRepository dogRepository;
#Autowired
private ToyRepository toyRepository;
#Test
void save_not_working_example() {
dogService = new DogService(dogRepository, toyRepository);
var dogDTO = DogDTO.builder()
.barkType("big bark")
.toyShape("some shape")
.build();
var savedDog = dogService.saveDogDTO(dogDTO);
assertThat(dogRepository.count()).isEqualTo(1);
assertThat(toyRepository.count()).isEqualTo(1);
var findByIdResult = dogRepository.findById(savedDog.getId());
assertThat(findByIdResult.get().getToys()).hasSize(1);
}
}
The test failure message:
Expected size: 1 but was: 0 in:
[]
java.lang.AssertionError:
Expected size: 1 but was: 0 in:
[]
The issue seems to be that the double JPA repository save clashes within the #Transaction. Is there a way to overcome this issue? I tried adding #Transactional(propagation = Propagation.NEVER) to the test, but then I get this failure:
failed to lazily initialize a collection of role: com.example.datajpatest.demo.models.Animal.toys, could not initialize proxy - no Session
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.example.datajpatest.demo.models.Animal.toys, could not initialize proxy - no Session

#DataJpaTest is annotated #Transactional so your test method is all wrapped in a single transaction, and hence a single EntityManager. You could make your test pass by calling EntityManager.detach() on the savedDog before querying using findById(). You could also fix it by manually setting up the dog's toys in the DogService. That would be my recommendation because otherwise sooner or later you might find the same inconsistency bug in production code - the transaction boundaries just have to shift a bit and that would be quite hard to spot. In a way #DataJpaTest has done you a favour by pointing out the problem, albeit somewhat indirectly.
Ultimately, the database state doesn't match the state of the EntityManager cache, so you have to clear the cache to get the result you want. Starting a new transaction would clear the cache too, and that's what is probably happening in production. Hibernate trusts you to make the object graph match the database state when you save (or flush). If they don't match then Hibernate has no way of knowing without querying the database, which it would regard as redundant and inefficient.

Try this mapping here instead:
#Entity
#Inheritance(strategy = InheritanceType.JOINED)
#Data
#SuperBuilder(toBuilder = true)
#NoArgsConstructor
#AllArgsConstructor
public class Animal {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Type(type = "uuid-char")
private UUID id;
#OneToMany(mappedBy = "animal", cascade = CascadeType.REMOVE)
#Builder.Default
private List<Toy> toys = new ArrayList<>();
}
#Entity
#Data
#Builder
#NoArgsConstructor
#AllArgsConstructor
public class Toy {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Type(type = "uuid-char")
private UUID id;
#ManyToOne(fetch = LAZY)
#JoinColumn(name = "animalId")
private Animal animal;
private String shape;
}

Related

Why is EntityGraph not working with DataJpaTest?

Hey today i found out that my Repository-Test runs perfectly fine when i use the #SpringBootTest-Annotation. But when i switch it to #DataJpaTest-Annotation, my #OneToMany-Annotated Collection of child elements is null.
Here an example:
ParentEntity.class
#Data
#NoArgsConstructor
#AllArgsConstructor
#Entity
#NamedEntityGraph(name="parent.childs", attributeNodes = #NamedAttributeNode("childEntities"))
#Table(name = "parent")
public class ParentEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
Integer id;
#OneToMany(mappedBy = "parentId")
Collection<ChildEntity> childEntities;
}
ParentRepository.class
#Repository
public interface ParentRepository extends JpaRepository<ParentEntity, Integer> {
#EntityGraph(value = "parent.childs")
Optional<ParentEntity> findById(Integer id);
}
ChildEntity.class
#Data
#Entity
#Table(name = "child")
public class ChildEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)#Column(name = "id", nullable = false)
private Integer id;
#Column(name = "parentId", nullable = false, insertable = false, updatable = false)
private Integer parentId;
#ManyToOne#JoinColumn(name = "parentId", referencedColumnName = "id", nullable = false)
private ParentEntity parentEntity;
}
ChildRepository.class
#Repository
public interface ChildRepository extends JpaRepository<ChildEntity, Integer> {
}
And this is the Test:
#SpringBootTest
#AutoConfigureTestDatabase
public class RepoTest {
#Autowired
ParentRepository parentRepository;
#Autowired
ChildRepository childRepository;
#Commit
#Rollback(false)
#Test
void test(){
/* arrange */
ParentEntity parent = new ParentEntity();
var parentId = parentRepository.save(parent).id;
ChildEntity child = new ChildEntity();
child.setParentEntity(parent);
childRepository.save(child);
/* act */
/* Yes, i know there should be an exists()-check but lets keep it simple */
ParentEntity returnedParent = parentRepository.findById(parentId).get();
/* assert */
assertThat(returnedParent.getChildEntities()).hasSize(1);
}
}
This test works as expected.
But when i change the #SpringBootTest-Annotation to #DataJpaTest, the childEntities-Field of the ParentEntity.class stays null
I tried to delombok the code and find the cause by debugging each step of the query but i couldnt make it out right now. The resulting hibernate query contains the left outer join that i would expect. So my guess is that the error has to do with Data-Binding. Maby some type of (auto-)configuration is not loaded when i run the test with the other annotation.
I am very interested in the cause, so I would appreciate an explanation
After a lot of further research, i found the following helpful link:
Spring Data Jpa Test returns null list even after child is saved
There is explained what the cause of the problem is:
The parent
gets not loaded for the database but from the internal cache.
And to solve this problem:
You need to write a FlushAndClear method
#PersistenceContext
private EntityManager em;
private void flushAndClear() {
em.flush();
em.clear();
}

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));
}
}

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.

Springboot add problem in oneTOMany relation

I'm writing 3 tables in the following relation:
Club class:
#Setter
#Getter
#Entity
#Table(name = "Club")
public class Club {
#Id
#GeneratedValue
private Long id;
private String name;
private String type;
private String mainPage;
private String logo;
#OneToMany(mappedBy="clubProductKey.club", cascade = CascadeType.ALL)
#JsonIgnoreProperties(value = "clubProductKey.club", allowSetters=true)
private Set<ClubProduct> clubProducts;
...
Product class:
#Setter
#Getter
#Entity
#Table(name = "Product")
public class Product {
#Id
#GeneratedValue
private Long id;
#OneToMany(mappedBy="clubProductKey.product", cascade = CascadeType.ALL)
#JsonIgnoreProperties(value = "clubProductKey.product", allowSetters=true)
private Set<ClubProduct> clubProducts;
...
ClubProduct class:
#Setter
#Getter
#Entity
#Table(name = "ClubProduct")
public class ClubProduct {
#EmbeddedId
private ClubProductKey clubProductKey;
...
ClubProductKey class:
#Setter
#Getter
#Embeddable
public class ClubProductKey implements Serializable {
#ManyToOne(cascade = {CascadeType.MERGE,CascadeType.REFRESH })
#JoinColumn(name = "club_id", referencedColumnName = "id")
#JsonIgnoreProperties(value = "clubProducts", allowSetters=true)
private Club club;
#ManyToOne(cascade = {CascadeType.MERGE,CascadeType.REFRESH })
#JoinColumn(name = "product_id", referencedColumnName = "id")
#JsonIgnoreProperties(value = "clubProducts", allowSetters=true)
private Product product;
...
ClubProductRepository class:
public interface ClubProductRepository extends JpaRepository<ClubProduct, ClubProductKey> {
public List<ClubProduct> findByClubProductKeyClub(Club club);
public List<ClubProduct> findByClubProductKeyProduct(Product product);
}
I try to save clubProduct like this:
#Service
public class ClubProductServiceImp implements ClubProductService {
#Autowired
private ClubProductRepository clubProductRepository;
...
ClubProduct savedClubProduct = clubProductRepository.save(clubProduct);
return savedClubProduct;
}
However I find that the clubProduct is not saved in the clubProducts list in the club or product entity, the list is null. Must I add lines like club.getClubProducts.add(clubProduct) or is there any other way to make it added automatically?
Thank you.
The #OnetoMany mapping in your Club class uses the attribute mappedby which means that it represents the owning side of the relation responsible for handling the mapping. However, we still need to have both sides in sync as otherwise, we break the Domain Model relationship consistency, and the entity state transitions are not guaranteed to work unless both sides are properly synchronized.
The answer is yes, you have to manage the java relations yourself so that the clubProducts gets persisted. You are using an instance of the repository class club to persist the data so , you should add a setter method like :
public void addClubProduct(ClubProduct clubProduct) {
if (clubProduct!= null) {
if (clubProduct== null) {
clubProduct= new ArrayList<ClubProduct>();
}
clubProducts.add(clubProduct);
clubProduct.setClubProduct(this);
}
}
also a method to remove it from the list and use these method in your code to set the values to the list properly before initiating save . Read related article

spring data jpa: No aliases found in result tuple! Make sure your query defines aliases

When I try to get the users using repository interface I received following exception "org.springframework.dao.InvalidDataAccessApiUsageException: No aliases found in result tuple! Make sure your query defines aliases!; nested exception is java.lang.IllegalStateException: No aliases found in result tuple! Make sure your query defines aliases!"
Repository:
#Repository
public interface UserRelationshipRepository
extends JpaRepository<UserRelationship, Long>, QueryDslPredicateExecutor<UserRelationship> {
#Query(value = "SELECT ur.id.toUser FROM UserRelationship ur WHERE ur.fromUser = :fromUser AND ur.relationshipTypeId = 1")
Set<User> findUserFriends(#Param("fromUser") User fromUser);
}
Entities:
#Entity
#NamedEntityGraph(name = "graph.User", attributeNodes = {})
#Table(name = "users")
public class User extends BaseEntity implements UserDetails {
private static final long serialVersionUID = 8884184875433252086L;
#Id
#SequenceGenerator(name = "users_id_seq", sequenceName = "users_id_seq", allocationSize = 1)
#GeneratedValue(strategy = GenerationType.AUTO, generator = "users_id_seq")
private Long id;
#Column(name = "first_name")
private String firstName;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "fromUser", cascade = CascadeType.ALL)
private Set<UserRelationship> relationships = new HashSet<UserRelationship>();
// getters setters
}
#Entity
#NamedEntityGraph(name = "graph.UserRelationship", attributeNodes = {})
#Table(name = "users_relationships")
public class UserRelationship extends BaseEntity implements Serializable {
private static final long serialVersionUID = -6367981399229734837L;
#EmbeddedId
private final UserRelationshipId id = new UserRelationshipId();
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "from_user_id", nullable = false)
#MapsId("fromUserId") // maps fromUserId attribute of the embedded id
private User fromUser;
#Column(name = "relationship_type_id")
private Long relationshipTypeId;
}
I am using '1.11.0.BUILD-SNAPSHOT' version of spring data jpa.
This is already known issue, and it is marked as resolved, but I am still get it.
Please, help me to solve this.
Update:
If I change repository method's return type to Set<Object> then all works fine.
You're running into DATAJPA-885, which is already fixed and will be part of the Spring Data Hopper SR2 release.

Resources