Давайте рассмотрим простое загрузочное приложение Spring:

pom.xml Зависимости Spring

...
<!-- spring -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
...

Без каких-либо особых настроек в наших файлах свойств, кроме тех, которые необходимы для подключения к базе данных.

Использование управляемой контейнером транзакции JPA EntityManager для управления постоянством.

Я хочу поделиться с вами тем, что нескольких строк кода было достаточно, чтобы поставить под сомнение концепции, с которыми я считал себя хорошо знакомыми.

Давайте рассмотрим следующие фрагменты, это довольно просто, у нас есть два объекта: контроллер и служба.

Сущность вопроса

@Entity
@Table(name = "Question")
public class Question {
    @Id
    @GeneratedValue
    @Column(name = "id")
    private Long id;
    @Column(name = "description")
    private String description;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "survey_id")
    private Survey survey;
 
    ...
}

Организация опроса

@Entity
@Table(name = "Survey")
public class Survey implements Serializable {
    @Id
    @GeneratedValue
    @Column(name = "id")
    private Long id;
    @Column(name = "name")    
    private String name;
   @OneToMany(fetch = FetchType.LAZY, mappedBy = "survey")
   private List<Question> questions;
   ...
}

Контроллер отдыха

@RestController
@RequestMapping("/default")
public class DefaultEndpoint {

  
  @Autowired
  private MyService myService;

  
  @PostMapping(value = "/foo")
  public void foo() {
        myService.foo();
    }
 ...
}

Сервис

@Service
public class MyService {

    @PersistenceContext
    private EntityManager entityManager;

    public void foo() {
        Survey survey = entityManager.find(Survey.class, 1L);
        System.out.println(survey.getQuestions().size());
    }
}

Присмотритесь к классу MyService.

Можете ли вы понять, почему я был озадачен, когда увидел фактическое количество вопросов, красиво напечатанных на моей консоли?

Две причины:

  • Опрос больше не привязан к контексту постоянства, потому что нет активного контекста постоянства, так как метод не имеет аннотации @Transactional.
  • Политика выборки для вопросов - LAZY, поэтому действительно должен быть активен контекст сохранения, чтобы результаты можно было получить из базы данных.

Именно тогда я узнал о шаблоне OSIV (Open Session In View).

По сути, все дело в том, чтобы избежать: org.hibernate.LazyInitializationException, которое могло бы возникнуть в результате выполнения приведенного выше кода при отсутствии OSIV. Исключение сообщает, что объект (объект опроса в нашем случае) находится в отсоединенном состоянии, поскольку контекст сохранения, используемый для его извлечения, уже закрыт, а запрашиваемые нами данные не были получены из-за Приняты правила LAZY fetch.

OSIV преодолевает эту ситуацию тривиально: он поддерживает постоянный контекст активным, даже если мы не запрашиваем его явно с помощью аннотации @Transactional.

Дело в том, что я всегда считал это исключение и поведение очень полезными, потому что они помогают применять многие передовые практики:

  • Разделение проблем между уровнями: объекты не должны достигать уровня представления, должны только DTO.
  • Разделение сущностей и DTO. Благодаря этому разделению логика представления и бизнес-логика / логика сохраняемости отделены друг от друга и могут развиваться с хорошей степенью свободы друг от друга.
  • Ограничение нагрузки на базу данных: при чтении соединение с базой данных удерживается только на время, необходимое для извлечения данных для заполнения DTO.

Что еще хуже, механизм автоконфигурации весенней загрузки делает OSIV активным по умолчанию.

Вы должны явно отключить его, чтобы отказаться от этого механизма, добавив следующую строку в свой файл свойств.

spring.jpa.open-in-view=false

Итак, в основном у нас есть механизм, который помогает разработчикам писать беспорядочный и неэффективный код, и он включен по умолчанию, может ли быть хуже? Оно может.

Давайте теперь рассмотрим эту альтернативную версию нашей Службы:

Обратите внимание, что я намеренно пропустил @Transactional в методе foo, чтобы показать, как OSIV может вызывать экстравагантное и вредное поведение при поддержке какого-то неосторожного кода.

@Service
public class MyService {

    @PersistenceContext
    private EntityManager entityManager;
    @Autowired
    private QuestionRepo questionRepo;
   public void foo() {
        Survey survey = entityManager.find(Survey.class, 1L);
        survey.setName("example");
        Question question = new Question();
        question.setDescription("description");
        question.setSurvey(survey);
        questionRepo.save(question);
    }
}

QuestionRepo

public interface QuestionRepo extends CrudRepository<Question, Long> {
}

Реализация метода сохранения данных Spring

@Transactional
public <S extends T> S save(S entity) {
    if (this.entityInformation.isNew(entity)) {
        this.em.persist(entity);
        return entity;
    } else {
        return this.em.merge(entity);
    }
}

Попробуем понять, что произойдет в обычном сценарии с отключенным OSIV.

Вопрос будет сохранен правильно, и наша попытка изменить поле имени в полученном объекте опроса будет проигнорирована, потому что объект не привязан к какому-либо контексту сохранения, и мы не используем никакой каскадной логики (см. снова классы Survey и Question).

Что происходит, когда включен OSIV, по меньшей мере странно.

При изменении имени в опросе обновляется соответствующее поле в строке таблицы.

Проще говоря, здесь происходит то, что контекст постоянства остается активным после операции поиска, поэтому объект опроса также остается присоединенным к нему, операция сохранения использует тот же активный контекст постоянства, снова привязывая его к своей транзакции (обратите внимание на @Transactional в методе сохранения).

Метод сохранения выполняет операцию записи, поэтому он сбрасывает контекст сохранения в конце транзакции, вызывая механизм грязной проверки для объекта опроса.

Теперь, особенно если учесть, что цепочка вызовов немного длиннее этой, кажется очевидным, как этот механизм может привести к неожиданному поведению, которое очень трудно устранить.

Легко предположить, что многие приложения уже могут полагаться на такого рода эффекты, а их разработчики даже не подозревают об этом.

Если вам интересно то, что вы читаете, я также предлагаю вам внимательно прочитать эти статьи, где вы также можете найти более подробную информацию о том, как OSIV работает под капотом.