When loading entity not fully with specification and entity graph in repository query, the data were cached. But only when method is called over REST endpoint. When the method were called scheduled all is fine and data were not cached.
Data model:
School
|_ id
|_ name
|_ SchoolClass[]
|______ id
|______ name
@Entity
@NamedEntityGraphs(
value = [
NamedEntityGraph(
name = "School.classes",
attributeNodes = [
NamedAttributeNode(
"classes"
)
]
)
]
)
class School {
constructor(name: String) {
this.name = name
}
@Id
@GeneratedValue
var id: Long = 0
@Column(length = 30, nullable = false)
var name: String = ""
@OneToMany(cascade = [CascadeType.ALL], orphanRemoval = false, mappedBy = "school")
var classes: MutableSet<SchoolClass> = mutableSetOf()
override fun toString(): String {
return "School(id=$id, name='$name', classes=${classes.joinToString { it.name }})"
}
}
interface SchoolRepository : JpaRepository<School, Long>, JpaSpecificationExecutor<School> {
@EntityGraph("School.classes")
override fun findAll(spec: Specification<School>?): List<School>
@Query(
"""
select s from School s where s.id in :ids
"""
)
@EntityGraph("School.classes")
fun findAllByIdWithClasses(@Param("ids") ids: Set<Long>): List<School>
}
object SchoolSpecifications {
fun name(name: String) =
Specification<School> { root, _, criteriaBuilder ->
criteriaBuilder.equal(root.get<String>("name"), name)
}
fun className(className: String) =
Specification<School> { root, _, criteriaBuilder ->
val classes = root.join<School, SchoolClass>("classes")
criteriaBuilder.equal(classes.get<String>("name"), className)
}
}
@Entity
class SchoolClass {
constructor(name: String, school: School) {
this.name = name
this.school = school
}
@Id
@GeneratedValue
var id: Long = 0
@Column(length = 30, nullable = false)
var name: String = ""
@ManyToOne(optional = false)
var school: School? = null
override fun toString(): String {
return "SchoolClass(id=$id, name='$name', school=${school?.name})"
}
}
interface SchoolClassRepository : JpaRepository<SchoolClass, Long>
@Service
class SchoolService
@Autowired
constructor(
private val schoolRepository: SchoolRepository,
private val schoolClassRepository: SchoolClassRepository
) {
fun initData() {
schoolClassRepository.deleteAll()
schoolRepository.deleteAll()
createSchool("Goethe-School", listOf("1A", "1B", "1C", "1D"))
createSchool("Schiller-School", listOf("5A", "6A", "5B", "6B"))
}
private fun createSchool(schoolName: String, classes: List<String>) {
val school = School(schoolName)
val savedSchool = schoolRepository.saveAndFlush(school)
classes.forEach {
val newClass = SchoolClass(it, savedSchool)
schoolClassRepository.saveAndFlush(newClass)
}
}
fun allFine(): List<School> {
return schoolRepository.findAll(SchoolSpecifications.name("Goethe-School"))
}
fun crazyStuff(): List<School> {
val schoolIds = schoolRepository.findAll(SchoolSpecifications.className("1C"))
.map { it.id }
.toSet()
return schoolRepository.findAllByIdWithClasses(schoolIds)
}
}
The interesting part:
@RestController
class Controller {
@Autowired
lateinit var schoolService: SchoolService
@GetMapping("/")
fun query() {
printSchool()
}
@Scheduled(fixedDelay = 5000)
fun scheduled() {
printSchool()
}
private fun printSchool() {
val schools = schoolService.crazyStuff()
println("=crazy stuff= $schools")
val schools2 = schoolService.allFine()
println("=all fine= $schools2")
}
}
The business logic is: load all schools with class '1C', extract the id and then load all schools with these id's fresh.
In the REST-Controller the output is:
=crazy stuff= [School(id=1, name='Goethe-School', classes=1C)]
=all fine= [School(id=1, name='Goethe-School', classes=1C)]
In the scheduled task the output is like expected:
=crazy stuff= [School(id=1, name='Goethe-School', classes=1D, 1B, 1C, 1A)]
=all fine= [School(id=1, name='Goethe-School', classes=1A, 1C, 1B, 1D)]
Whats the reason for this behaviour? And how to disable the caching of the not fully loaded entity with specification query and entity graph?