Să luăm în considerare o aplicație Spring boot simplă:

Dependențe pom.xml Spring

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

Fără setări speciale în fișierele noastre de proprietăți, cu excepția celor necesare pentru conexiunea la baza de date.

Utilizarea JPA EntityManager în domeniul tranzacțiilor gestionate de container pentru a gestiona persistența.

Vreau să vă împărtășesc cum au fost suficiente câteva rânduri de cod pentru a pune la îndoială concepte cu care mă consideram destul de familiar.

Să luăm în considerare următoarele fragmente, este destul de simplu, avem două entități, un controller și un serviciu.

Entitate de întrebare

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

Entitate de sondaj

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

Rest Controller

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

  
  @Autowired
  private MyService myService;

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

Serviciul

@Service
public class MyService {

    @PersistenceContext
    private EntityManager entityManager;

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

Aruncă o privire mai atentă la clasa MyService.

Puteți vedea de ce am fost nedumerit când am văzut numărul real de întrebări din sondaj tipărit frumos acolo pe consola mea?

Două motive:

  • Sondajul nu mai este atașat contextului de persistență, deoarece nu există un context de persistență activ, deoarece metoda nu are adnotare @Transactional.
  • Politica de preluare a întrebărilor este LENESĂ, așa că într-adevăr un context de persistență trebuie să fie activ pentru ca rezultatele să fie preluate din baza de date.

Așa că acesta a fost momentul în care am aflat despre modelul OSIV (Open Session In View).

Și, practic, totul este despre evitarea: org.hibernate.LazyInitializationExceptioncare ar rezulta din execuția codului de mai sus cu OSIV neexistând. Excepția informează că entitatea (entitate de sondaj în cazul nostru) se află într-o stare detașată, deoarece contextul de persistență folosit pentru a o prelua a fost deja închis și datele pe care le solicităm nu au fost preluate, din cauza Politica de preluare LAZYa fost adoptată.

Modul în care OSIV depășește această situație este banal, menține activ contextul de persistență, chiar și atunci când nu îl solicităm în mod explicit folosind adnotarea @Transactional.

Ideea este că întotdeauna am considerat această excepție și acest comportament ca fiind foarte utile, deoarece ajută la aplicarea multor bune practici:

  • Separarea preocupărilor între straturi: entitățile nu ar trebui să ajungă la nivelul de prezentare, ci doar DTO-urile.
  • Decuplare între entități și DTO: cu această decuplare, logica de prezentare și logica de afaceri/persistență sunt decuplate și pot evolua cu un grad bun de libertate una față de cealaltă.
  • Limitarea stresului asupra bazei de date: în timp ce citirea, conexiunea la baza de date rămâne reținută doar pentru timpul necesar pentru a prelua datele cu care să populați DTO-urile.

Ceea ce este și mai rău este că mecanismul de autoconfigurare prin pornire cu arc face ca OSIV să fie activ în mod implicit.

Vi se cere să îl dezactivați în mod explicit pentru a renunța la acest mecanism, adăugând următoarea linie în fișierul de proprietăți.

spring.jpa.open-in-view=false

Deci, practic, avem un mecanism care îi ajută pe dezvoltatori să scrie cod dezordonat și ineficient și este activat implicit, se poate înrăutăți? Se poate.

Să luăm acum în considerare această versiune alternativă a Serviciului nostru:

Rețineți că am omis în mod intenționat @Transactional din metoda foo pentru a arăta cum OSIV poate produce un comportament extravagant și dăunător atunci când este susținut de un cod neglijent.

@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> {
}

Implementarea datelor de primăvară a metodei de salvare

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

Să încercăm să înțelegem ce s-ar întâmpla într-un scenariu normal, cu OSIV dezactivat.

Întrebarea ar fi salvată corect, iar încercarea noastră de a modifica câmpul de nume de pe entitatea de sondare preluată ar fi ignorată, deoarece entitatea nu este atașată la niciun context de persistență și nu folosim nicio logică în cascadă (vezi din nou clasele Sondaj și Întrebări).

Ce se întâmplă când OSIV este activat este cel puțin ciudat.

Schimbarea numelui din sondaj actualizează câmpul corespunzător din rândul său tabel.

Ceea ce se întâmplă aici, în cuvinte simplificate, este că contextul de persistență rămâne activ după operația de căutare, astfel încât entitatea de sondaj rămâne și ea atașată, operația de salvare folosește același context de persistență activ legându-l din nou de tranzacția sa (rețineți că @Transactional pe metoda de salvare).

Metoda de salvare efectuează o operație de scriere, astfel încât șterge contextul de persistență la sfârșitul tranzacției, declanșând mecanismul de verificare murdară pentru entitatea de sondaj.

Acum, mai ales dacă luați în considerare un lanț de apeluri puțin mai lung decât acesta, pare evident cum acest mecanism ar putea duce la un comportament neașteptat foarte greu de depanat.

Este ușor să presupunem că multe aplicații de acolo s-ar putea baza deja pe acest tip de efecte fără ca dezvoltatorii lor să fie conștienți de acest lucru.

Daca te intereseaza ceea ce citesti iti sugerez si sa dai o lectura buna acestor articole, unde gasesti si mai multe detalii despre lucrarile OSIV sub capota.