REST + Spring Data JPA entity graph + entity not fully load + cache

Viewed 34

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?

1 Answers

This is most certainly due to transaction scoping.

It is hard to be certain, but since you have no transaction demarkation what so ever I'm going to assume that you're using the Open Session In View Anti-Pattern

This makes you use for your REST call everything happen in a single transaction. Therefore the first loading of a school puts it in the 1st level cache and it stays there and gets returned on following calls as well.

On the scheduled call you don't have any explicit transaction at all and OSIV doesn't have an effect because that is bound to web requests which don't exist. Therefore new transactions are created and closed for each repository call, recreating and destroying the 1st level cache each time. Therefore, you essentially don't have any cache at all.

Related