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
CodePudding user response:
@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.
CodePudding user response:
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;
}