0

I see 'strange' behavior of JPA while retrieving entity from one thread and while its modified in another. Following is my sample code to explain the issue. Typical scenario:

  • create a student s1
  • get/print student details for s1 in loop
  • update s1 details
  • check whether updated student details are printed in loop
    • It still prints old data

As I understand that JPA has persistence context(PC) associated with each thread and hence stores entities in it. Thread's can't see each other's PC. I think PC gets updated if corresponding thread does create/update operation on the entity. I think this behavior of JPA breaks database isolation principle. Shouldn't GET be returning updated data out of the box ? OR Am I missing anything here ?

Log

2021-05-28 07:13:03.503  INFO 7425 --- [nio-8080-exec-2] com.example.demo.StudentController       : Created student... Student{id=1, name='Omkar', score=50}
2021-05-28 07:14:22.272  INFO 7425 --- [nio-8080-exec-4] com.example.demo.StudentController       : Get student Student{id=1, name='Omkar', score=50}
2021-05-28 07:14:27.280  INFO 7425 --- [nio-8080-exec-4] com.example.demo.StudentController       : Get student Student{id=1, name='Omkar', score=50}
2021-05-28 07:14:32.282  INFO 7425 --- [nio-8080-exec-4] com.example.demo.StudentController       : Get student Student{id=1, name='Omkar', score=50}
2021-05-28 07:14:33.748  INFO 7425 --- [nio-8080-exec-5] com.example.demo.StudentController       : Updating student... Student{id=1, name='Omkar', score=75}
2021-05-28 07:14:37.284  INFO 7425 --- [nio-8080-exec-4] com.example.demo.StudentController       : Get student Student{id=1, name='Omkar', score=50}
2021-05-28 07:14:42.288  INFO 7425 --- [nio-8080-exec-4] com.example.demo.StudentController       : Get student Student{id=1, name='Omkar', score=50}

Entity

@Entity
public class Student {

    @Id
    private int id;

    private String name;

    private int score;
    ...

Repository

public interface StudentRepository extends JpaRepository<Student, Integer> {
}

Controller

@RestController
@RequestMapping("/api/student")
public class StudentController {

    @Autowired
    private StudentRepository studentRepository;

    private final static Logger LOGGER = LoggerFactory.getLogger(StudentController.class);

    @GetMapping
    public void getStudent() {
        while (true) {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Student student = studentRepository.findById(1).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
            LOGGER.info("Get student {}",student.toString());
        }
    }

    @PostMapping
    public ResponseEntity<Student> updateStudent(@RequestParam("score") int score) {
        Student student = studentRepository.findById(1).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
        student.setScore(score);
        Student updated = studentRepository.save(student);
        LOGGER.info("Updating student... {}", updated);
        return ResponseEntity.ok(updated);
    }

    @PutMapping
    public ResponseEntity<Student> createStudent(Student student) {
        Student saved = studentRepository.save(student);
        LOGGER.info("Created student... {}", saved);
        return ResponseEntity.created(URI.create("/api/student/" + saved.getId())).body(saved);
    }

}
Omkar Shetkar
  • 3,488
  • 5
  • 35
  • 50

1 Answers1

0

change your loop to this

@GetMapping
public void getStudent() {
    while (true) {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        entityManager.clear();
        Student student = studentRepository.findById(1).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
        LOGGER.info("Get student {}",student.toString());
    }
}

basically the result of findById is attached to the current transaction and the query will be invoked only once (even if it is in a loop). to hit the database again you should detach the entity. you can detach the entity by invoking entityManager.clear()

tremendous7
  • 721
  • 6
  • 9
  • By making us call `entityManager.clear();`, I think JPA makes the code complex and unmaintainable. Also, `clear()` without `flush()` may lead to random bugs. My point in the question is more of 'Why' of JPA behavior. JPA is a intermediary between DB and application. I think it should reflect the DB status as it is. – Omkar Shetkar May 28 '21 at 05:04