Rozważmy prostą aplikację uruchamiającą Spring:

pom.xml Zależności Springa

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

Bez żadnych szczególnych ustawień w naszych plikach właściwości, z wyjątkiem tych wymaganych do połączenia z bazą danych.

Używanie transakcji zarządzanej przez kontener JPA EntityManager do zarządzania trwałością.

Chcę się z wami podzielić tym, że wystarczyło kilka linijek kodu, żeby podać w wątpliwość koncepcje, które uważałem za dość dobrze znane.

Rozważmy następujące fragmenty, to całkiem proste, mamy dwa podmioty kontroler i usługę.

Element pytania

@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;
 
    ...
}

Element ankiety

@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;
   ...
}

Kontroler odpoczynku

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

  
  @Autowired
  private MyService myService;

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

Usługa

@Service
public class MyService {

    @PersistenceContext
    private EntityManager entityManager;

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

Przyjrzyj się bliżej klasie MyService.

Czy potrafisz dostrzec, dlaczego byłem zdziwiony, gdy zobaczyłem rzeczywistą liczbę pytań ankiety wydrukowaną ładnie na mojej konsoli?

Dwa powody:

  • Ankieta nie jest już powiązana z kontekstem trwałości, ponieważ nie ma aktywnego kontekstu trwałości, ponieważ metoda nie posiada adnotacji @Transactional.
  • Polityka pobierania pytań jest LENIWA, więc rzeczywiście kontekst trwałości musi być aktywny, aby wyniki mogły zostać pobrane z bazy danych.

Więc to był czas, kiedy dowiedziałem się o wzorcu OSIV (Open Session In View).

Zasadniczo chodzi o unikanie: org.hibernate.LazyInitializationException, który byłby wynikiem wykonania powyższego kodu przy braku OSIV. Wyjątek informuje, że encja (w naszym przypadku ankieta) jest w stanie odłączonym, ponieważ kontekst trwałości użyty do jej pobrania został już zamknięty, a dane, o które prosimy, nie zostały pobrane, ze względu na Przyjęto politykę LENIWEGO pobierania.

Sposób, w jaki OSIV radzi sobie z tą sytuacją, jest trywialny, utrzymuje kontekst trwałości aktywny, nawet jeśli nie poprosimy o to wyraźnie za pomocą adnotacji @Transactional.

Rzecz w tym, że zawsze uważałem ten wyjątek i zachowanie za bardzo przydatne, ponieważ pomaga egzekwować wiele najlepszych praktyk:

  • Rozdzielenie obaw pomiędzy warstwami: Podmioty nie powinny docierać do warstwy prezentacji, powinny to robić jedynie DTO.
  • Oddzielenie między jednostkami i DTO: Dzięki temu oddzieleniu logika prezentacji i logika biznesowa/trwałość są oddzielone i mogą ewoluować z dużą swobodą od siebie.
  • Ograniczenie obciążenia bazy danych: Podczas odczytu połączenie z bazą danych pozostaje utrzymywane tylko przez czas niezbędny do pobrania danych do zapełnienia DTO.

Co gorsza, mechanizm autokonfiguracji Spring Boot domyślnie aktywuje OSIV.

Aby zrezygnować z tego mechanizmu, musisz wyraźnie go wyłączyć, dodając następujący wiersz w pliku właściwości.

spring.jpa.open-in-view=false

Zasadniczo mamy mechanizm, który pomaga programistom pisać niechlujny i nieefektywny kod i jest domyślnie włączony. Czy może być jeszcze gorzej? To może.

Rozważmy teraz tę alternatywną wersję naszej Usługi:

Zauważ, że celowo pominąłem @Transactional w metodzie foo, aby pokazać, jak OSIV może powodować ekstrawaganckie i szkodliwe zachowanie, gdy jest wspierany przez nieostrożny kod.

@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);
    }
}

Repozytorium pytań

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

Wiosenna implementacja metody zapisu danych

@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);
    }
}

Spróbujmy zrozumieć, co by się stało w normalnym scenariuszu, z wyłączonym OSIV.

Pytanie zostałoby zapisane poprawnie, a nasza próba zmiany pola nazwy w pobranej encji ankiety zostałaby ładnie zignorowana, ponieważ encja nie jest powiązana z żadnym kontekstem trwałości i nie używamy żadnej logiki kaskadowej (patrz ponownie klasy Ankieta i Pytanie).

To, co dzieje się, gdy włączony jest OSIV, jest co najmniej dziwne.

Zmiana nazwy w ankiecie aktualizuje odpowiednie pole w wierszu tabeli.

W uproszczeniu dzieje się to tak, że kontekst trwałości pozostaje aktywny po operacji find, więc jednostka ankiety również pozostaje do niej dołączona, operacja zapisywania ponownie wykorzystuje ten sam aktywny kontekst trwałości, wiążąc go z transakcją (zwróć uwagę na @Transakcyjne w przypadku metody zapisywania).

Metoda Save przeprowadza operację zapisu, więc na koniec transakcji opróżnia kontekst trwałości, uruchamiając mechanizm sprawdzania brudnego dla jednostki ankiety.

Zwłaszcza jeśli weźmie się pod uwagę, że łańcuch wywołań jest nieco dłuższy, wydaje się oczywiste, że ten mechanizm może prowadzić do nieoczekiwanego zachowania, które jest bardzo trudne do rozwiązania.

Łatwo założyć, że wiele aplikacji może już polegać na tego rodzaju efektach, a ich twórcy nawet nie są tego świadomi.

Jeśli jesteś zainteresowany tym, co czytasz, sugeruję również dokładne zapoznanie się z tymi artykułami, w których możesz znaleźć więcej szczegółów na temat ukrytych prac OSIV.