Panduan untuk Sesi Terbuka Musim Semi Dalam Pandangan

1. Gambaran keseluruhan

Sesi setiap permintaan adalah corak transaksional untuk mengikat sesi ketekunan dan meminta kitaran hidup bersama. Tidak hairanlah, Spring hadir dengan pelaksanaan corak ini sendiri, yang diberi nama OpenSessionInViewInterceptor , untuk memudahkan bekerja dengan persatuan yang malas dan dengan itu, meningkatkan produktiviti pemaju.

Dalam tutorial ini, pertama, kita akan belajar bagaimana pemintas berfungsi secara dalaman, dan kemudian, kita akan melihat bagaimana corak kontroversi ini boleh menjadi pedang bermata dua untuk aplikasi kita!

2. Memperkenalkan Sesi Terbuka dalam Paparan

Untuk lebih memahami peranan Open Session in View (OSIV), anggap kami mempunyai permintaan masuk:

  1. Spring membuka Sesi Hibernate baru pada awal permintaan. Ini Sesyen tidak semestinya dihubungkan dengan pangkalan data.
  2. Setiap kali aplikasi memerlukan Sesi, aplikasi akan menggunakan semula yang sudah ada.
  3. Pada akhir permintaan, pemintas yang sama menutup Sesi tersebut.

Pada pandangan pertama, mungkin masuk akal untuk mengaktifkan ciri ini. Bagaimanapun, kerangka kerja menangani pembuatan dan penamatan sesi, jadi pembangun tidak mementingkan diri sendiri dengan perincian tahap yang kelihatan rendah ini. Ini seterusnya meningkatkan produktiviti pemaju.

Namun, kadang-kadang, OSIV boleh menyebabkan masalah prestasi halus dalam pengeluaran . Biasanya, masalah seperti ini sangat sukar untuk didiagnosis.

2.1. Spring Boot

Secara lalai, OSIV aktif dalam aplikasi Spring Boot . Walaupun begitu, pada Spring Boot 2.0, ia memperingatkan kita bahawa ia diaktifkan pada permulaan aplikasi jika kita belum mengkonfigurasinya secara eksplisit:

spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering.Explicitly configure spring.jpa.open-in-view to disable this warning

Bagaimanapun, kita boleh mematikan OSIV dengan menggunakan spring.jpa.konfigurasi open-in-view properti:

spring.jpa.open-in-view=false

2.2. Corak atau Anti-Corak?

Selalu ada reaksi bercampur-campur terhadap OSIV. Hujah utama kem pro-OSIV adalah produktiviti pemaju, terutama ketika berurusan dengan pergaulan yang malas.

Sebaliknya, masalah prestasi pangkalan data adalah hujah utama kempen anti-OSIV. Kemudian, kami akan menilai kedua-dua hujah tersebut secara terperinci.

3. Wira Permulaan Malas

Sejak OSIV mengikat Sesi kitaran hayat untuk setiap permintaan, Hibernate boleh menyelesaikan persatuan malas walaupun selepas kembali dari eksplisit @Transactional perkhidmatan .

Untuk lebih memahami hal ini, anggaplah kita memperagakan pengguna dan kebenaran keselamatan mereka:

@Entity @Table(name = "users") public class User { @Id @GeneratedValue private Long id; private String username; @ElementCollection private Set permissions; // getters and setters }

Sama seperti hubungan satu-ke-banyak dan banyak-ke-banyak yang lain, harta kebenaran adalah koleksi malas.

Kemudian, dalam pelaksanaan lapisan perkhidmatan kami, mari kita jelaskan sempadan transaksional kami dengan menggunakan @Transactional :

@Service public class SimpleUserService implements UserService { private final UserRepository userRepository; public SimpleUserService(UserRepository userRepository) { this.userRepository = userRepository; } @Override @Transactional(readOnly = true) public Optional findOne(String username) { return userRepository.findByUsername(username); } }

3.1. Harapan

Inilah yang kami jangkakan apabila kod kami memanggil kaedah findOne :

  1. Pada mulanya, proksi Spring memintas panggilan dan mendapatkan transaksi semasa atau membuat transaksi sekiranya tidak ada.
  2. Kemudian, ia menyerahkan kaedah panggilan untuk pelaksanaan kami.
  3. Akhirnya, proksi melakukan transaksi dan seterusnya menutup Sesi yang mendasari . Lagipun, kita hanya memerlukan Sesi itu di lapisan perkhidmatan kita.

Dalam pelaksanaan kaedah findOne , kami tidak memulakan koleksi izin . Oleh itu, kita tidak boleh menggunakan kebenaran setelah kaedah kembali. Sekiranya kita melakukan iterasi pada harta tanah ini , kita harus mendapat LazyInitializationException.

3.2. Selamat datang ke Dunia Sebenar

Mari tulis pengawal REST ringkas untuk melihat sama ada kita boleh menggunakan harta kebenaran :

@RestController @RequestMapping("/users") public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; } @GetMapping("/{username}") public ResponseEntity findOne(@PathVariable String username) { return userService .findOne(username) .map(DetailedUserDto::fromEntity) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } }

Di sini, kami mengulangi kebenaran semasa penukaran entiti ke DTO. Oleh kerana kami menjangkakan penukaran tersebut gagal dengan LazyInitializationException, ujian berikut tidak boleh lulus:

@SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("test") class UserControllerIntegrationTest { @Autowired private UserRepository userRepository; @Autowired private MockMvc mockMvc; @BeforeEach void setUp() { User user = new User(); user.setUsername("root"); user.setPermissions(new HashSet(Arrays.asList("PERM_READ", "PERM_WRITE"))); userRepository.save(user); } @Test void givenTheUserExists_WhenOsivIsEnabled_ThenLazyInitWorksEverywhere() throws Exception { mockMvc.perform(get("/users/root")) .andExpect(status().isOk()) .andExpect(jsonPath("$.username").value("root")) .andExpect(jsonPath("$.permissions", containsInAnyOrder("PERM_READ", "PERM_WRITE"))); } }

Walau bagaimanapun, ujian ini tidak memberikan pengecualian, dan lulus.

Kerana OSIV membuat Sesi pada awal permintaan, proksi transaksimenggunakan Sesi yang ada sekarang dan bukannya membuat yang baru .

Jadi, di sebalik apa yang kami jangkakan, kami sebenarnya boleh menggunakan hak milik walaupun di luar @Transactional yang eksplisit . Lebih-lebih lagi, jenis pergaulan malas ini dapat diambil di mana sahaja dalam skop permintaan semasa.

3.3. Mengenai Produktiviti Pembangun

Sekiranya OSIV tidak diaktifkan, kita harus memulakan secara manual semua persatuan malas yang diperlukan dalam konteks transaksi . Cara paling asas (dan biasanya salah) adalah menggunakan kaedah Hibernate.initialize () :

@Override @Transactional(readOnly = true) public Optional findOne(String username) { Optional user = userRepository.findByUsername(username); user.ifPresent(u -> Hibernate.initialize(u.getPermissions())); return user; }

Kini, kesan OSIV terhadap produktiviti pembangun sudah jelas. Walau bagaimanapun, ini tidak selalu mengenai produktiviti pemaju.

4. Penjahat Prestasi

Suppose we have to extend our simple user service to call another remote service after fetching the user from the database:

@Override public Optional findOne(String username) { Optional user = userRepository.findByUsername(username); if (user.isPresent()) { // remote call } return user; }

Here, we're removing the @Transactional annotation since we clearly won't want to keep the connected Session while waiting for the remote service.

4.1. Avoiding Mixed IOs

Let's clarify what happens if we don't remove the @Transactional annotation. Suppose the new remote service is responding a little more slowly than usual:

  1. At first, the Spring proxy gets the current Session or creates a new one. Either way, this Session is not connected yet. That is, it's not using any connection from the pool.
  2. Once we execute the query to find a user, the Session becomes connected and borrows a Connection from the pool.
  3. If the whole method is transactional, then the method proceeds to call the slow remote service while keeping the borrowed Connection.

Imagine that during this period, we get a burst of calls to the findOne method. Then, after a while, all Connections may wait for a response from that API call. Therefore, we may soon run out of database connections.

Mixing database IOs with other types of IOs in a transactional context is a bad smell, and we should avoid it at all costs.

Anyway, since we removed the @Transactional annotation from our service, we're expecting to be safe.

4.2. Exhausting the Connection Pool

When OSIV is active, there is always a Session in the current request scope, even if we remove @Transactional. Although this Session is not connected initially, after our first database IO, it gets connected and remains so until the end of the request.

So, our innocent-looking and recently-optimized service implementation is a recipe for disaster in the presence of OSIV:

@Override public Optional findOne(String username) { Optional user = userRepository.findByUsername(username); if (user.isPresent()) { // remote call } return user; }

Here's what happens while the OSIV is enabled:

  1. At the beginning of the request, the corresponding filter creates a new Session.
  2. When we call the findByUsername method, that Session borrows a Connection from the pool.
  3. The Session remains connected until the end of the request.

Even though we're expecting that our service code won't exhaust the connection pool, the mere presence of OSIV can potentially make the whole application unresponsive.

To make matters even worse, the root cause of the problem (slow remote service) and the symptom (database connection pool) are unrelated. Because of this little correlation, such performance issues are difficult to diagnose in production environments.

4.3. Unnecessary Queries

Unfortunately, exhausting the connection pool is not the only OSIV-related performance issue.

Since the Session is open for the entire request lifecycle, some property navigations may trigger a few more unwanted queries outside of the transactional context. It's even possible to end up with n+1 select problem, and the worst news is that we may not notice this until production.

Adding insult to injury, the Session executes all those extra queries in auto-commit mode. In auto-commit mode, each SQL statement is treated as a transaction and is automatically committed right after it is executed. This, in turn, puts a lot of pressure on the database.

5. Choose Wisely

Whether the OSIV is a pattern or an anti-pattern is irrelevant. The most important thing here is the reality in which we're living.

If we're developing a simple CRUD service, it might make sense to use the OSIV, as we may never encounter those performance issues.

On the other hand, if we find ourselves calling a lot of remote services or there is so much going on outside of our transactional contexts, it's highly recommended to disable the OSIV altogether.

When in doubt, start without OSIV, since we can easily enable it later. On the other hand, disabling an already enabled OSIV may be cumbersome, as we may need to handle a lot of LazyInitializationExceptions.

The bottom line is that we should be aware of the trade-offs when using or ignoring the OSIV.

6. Alternatives

If we disable OSIV, then we should somehow prevent potential LazyInitializationExceptions when dealing with lazy associations. Among a handful of approaches to coping with lazy associations, we're going to enumerate two of them here.

6.1. Entity Graphs

When defining query methods in Spring Data JPA, we can annotate a query method with @EntityGraph to eagerly fetch some part of the entity:

public interface UserRepository extends JpaRepository { @EntityGraph(attributePaths = "permissions") Optional findByUsername(String username); }

Here, we're defining an ad-hoc entity graph to load the permissions attribute eagerly, even though it's a lazy collection by default.

If we need to return multiple projections from the same query, then we should define multiple queries with different entity graph configurations:

public interface UserRepository extends JpaRepository { @EntityGraph(attributePaths = "permissions") Optional findDetailedByUsername(String username); Optional findSummaryByUsername(String username); }

6.2. Caveats When Using Hibernate.initialize()

One might argue that instead of using entity graphs, we can use the notorious Hibernate.initialize() to fetch lazy associations wherever we need to do so:

@Override @Transactional(readOnly = true) public Optional findOne(String username) { Optional user = userRepository.findByUsername(username); user.ifPresent(u -> Hibernate.initialize(u.getPermissions())); return user; }

They may be clever about it and also suggest to call the getPermissions() method to trigger the fetching process:

Optional user = userRepository.findByUsername(username); user.ifPresent(u -> { Set permissions = u.getPermissions(); System.out.println("Permissions loaded: " + permissions.size()); });

Both approaches aren't recommended since they incur (at least) one extra query, in addition to the original one, to fetch the lazy association. That is, Hibernate generates the following queries to fetch users and their permissions:

> select u.id, u.username from users u where u.username=? > select p.user_id, p.permissions from user_permissions p where p.user_id=? 

Although most databases are pretty good at executing the second query, we should avoid that extra network round-trip.

On the other hand, if we use entity graphs or even Fetch Joins, Hibernate would fetch all the necessary data with just one query:

> select u.id, u.username, p.user_id, p.permissions from users u left outer join user_permissions p on u.id=p.user_id where u.username=?

7. Kesimpulannya

Dalam artikel ini, kami mengalihkan perhatian ke arah ciri yang cukup kontroversial pada musim bunga dan beberapa kerangka kerja perusahaan lain: Sesi Terbuka dalam Paparan. Pertama, kita mendapat corak ini dengan konsep dan konsep pelaksanaan. Kemudian kami menganalisisnya dari perspektif produktiviti dan prestasi.

Seperti biasa, kod sampel boleh didapati di GitHub.