Почему findById() возвращает прокси?
В приложении есть сущность Student. Я тестирую метод update() в её контроллере и сервисе. Тест метода update() заключается в следующем: сначала я обновляю студента в базе, а потом вытаскиваю его из базы и сравниваю с эталонным образцом. В тесте сервиса всё срабатывает отлично, а в тесте контроллера я получаю ошибку. Проследив путь этой ошибки, я выяснил, что метод JpaRepository#findById возвращает прокси, а не реального студента, и когда я пытаюсь сравнить это прокси с эталонным объектом, я получаю ошибку.
Вот часть ошибки:
java.lang.AssertionError:
...
ru.zhenyria.restaurants.model.Student$HibernateProxy$KBik0QLG can't be compared to
ru.zhenyria.restaurants.model.Student as Student does not declare all Student$HibernateProxy$KBik0QLG
fields, it lacks these: [$$_hibernate_interceptor]
Возможную причину такого странного поведения findById() я нашёл вот здесь.
Код и мои попытки решения под катом.
Я покажу вам только часть кода, потому что ошибка локализована и не имеет отношение к самой сущности или контроллеру.
CRUD-репозиторий:
@Transactional(readOnly = true)
public interface CrudStudentRepository extends JpaRepository<Student, Integer> {
}
Репозиторий:
@Repository
public class StudentRepository {
private final CrudStudentRepository repository;
public StudentRepository(CrudStudentRepository repository) {
this.repository = repository;
}
public Student get(int id) {
return repository.findById(id).orElse(null);
}
// другие методы
Сервис:
@Service
public class StudentService {
private final StudentRepository repository;
public StudentService(StudentRepository repository) {
this.repository = repository;
}
public Student get(int id) {
return repository.get(id);
}
@Transactional
public void update(StudentTo studentTo) {
Student student = // логика по получению Student из StudentTo
repository.save(student);
}
// другие методы
Тест сервиса:
@SpringJUnitConfig(locations = {
"classpath:spring/spring-app.xml",
"classpath:spring/spring-db.xml"
})
@Sql(scripts = "classpath:db/populateDB.sql", config = @SqlConfig(encoding = "UTF-8"))
@ExtendWith(TimingExtension.class)
public class StudentServiceTest {
@Autowired
private StudentService service;
@Test
void update() {
StudentTo student = getUpdatedTo(); // метод getUpdatedTo() возвращает TO обновлённого студента
service.update(student);
assertThat(service.get(updated.getId())).isEqualTo(getUpdated());
}
// другие тесты
Тест контроллера:
@SpringJUnitWebConfig(locations = {
"classpath:spring/spring-app.xml",
"classpath:spring/spring-mvc.xml",
"classpath:spring/spring-db.xml"
})
@Transactional
public class StudentControllerTest {
private static final String REST_URL = StudentController.REST_URL + "/";
@Autowired
private StudentService service;
@Test
void update() throws Exception {
StudentTo updated = getUpdatedTo();
perform(MockMvcRequestBuilders.put(REST_URL + "/" + updated.getId()))
.contentType(MediaType.APPLICATION_JSON)
.content(jsonWithPassword(updated, updated.getPassword())))
.andDo(print())
.andExpect(status().isNoContent());
assertThat(service.get(updated.getId())).isEqualTo(getUpdated());
}
// другие тесты
Насколько вы видите, общий принцип тестирования в StudentServiceTest и StudentControllerTest схож: сначала обновляем студента в базе, а потом вытаскиваем из базы и сравниваем с эталоном вот в этой строке: STUDENT_MATCHER.assertMatch(service.get(updated.getId()), getUpdated()), только StudentControllerTest получает прокси при попытки вытаскивания студента из базы, из-за чего валится.
Вероятно, проблема возникла из-за того, что StudentControllerTest аннотирован @Transactional, но это нужно для отката изменений в базе. Я пробовал сделать вот так:
@Transactional(propagation = Propagation.NEVER)
@Test
void update() throws Exception {
Student updated = getUpdated();
perform(MockMvcRequestBuilders.put(REST_URL + "/" + updated.getId()))
.contentType(MediaType.APPLICATION_JSON)
.content(jsonWithPassword(updated, updated.getPassword())))
.andDo(print())
.andExpect(status().isNoContent());
STUDENT_MATCHER.assertMatch(service.get(updated.getId()), getUpdated());
}
Теперь из базы приходит не прокси, а реальный объект. Но из-за этого стали валиться другие тесты, так как изменения в базе перестали откатываться.
Таким образом у меня возникло несколько вопросов:
- Как в моём случае избежать того, чтобы findById возвращал прокси?
- Или как мне сделать так, чтобы сохранения в базе откатывались, но при было
propagation=Propagation.NEVER? - Есть ли ещё какой-то вариант решить мою проблему?
Обратите внимание, что в принципе сам контроллер работает полностью адекватно, проверено через Postman. Проблема исключительно в тесте. Причём проблема не возникает в тесте сервиса, из чего я делаю вывод, что проблема может быть отчасти из-за того, что над классом теста контроллера установлена аннотация @Transactional, но как иначе?
Ответы (1 шт):
Ответ меня удивил.
Я нашёл его вот здесь.
Суть следующая. У нас есть два метода: getOne(), который возвращает прокси, и findById(), который возвращает полностью проинициализированный объект.
Но если мы вызываем оба эти метода в одной транзакции, то их поведение может измениться. Например, если мы в одной транзакции сначала вызываем findById(), то повторно вызванный в той же транзакции getOne(), при помощи которого мы пытаемся получить тот же объект, будет возвращать полностью проинициализированный объект, а не прокси (если я не ошибаюсь, на время транзакции объект хранится в памяти).
Такое же поведение происходит и в обратную сторону:
@Transactional
public void test(int id) {
Person lazyPerson = getProxy(id);
Person initializedPerson = getInitialized(id);
}
public Person getProxy(int id) {
return dao.getOne(id);
}
public Person getInitialized(int id) {
return dao.findById(id).orElse(null);
}
В данном случае переменная initializedPerson в методе test() будет проинициализирована не реальным объектом, а всего лишь прокси-обёрткой, в которой есть только id. Будьте внимательны с этим. Я столкнулся с таким поведением в ходе тестов, которые открывали свою транзакцию, и для решения проблемы использовал обычный метод Hibernate.initialize().