# Spring Data JPA - MySQL using streams (2) 承接[上一集](https://hackmd.io/KePD8UeBQ4K0rf2-rflroQ),這篇想要跟大家分享的是在 stream 的操作過程中需要注意的一些眉眉角角! ## 除了 try-with-resources 還有? 這是上次說到的,操作 stream 的時候必須要使用 try-with-resources 來正確關閉 stream,還有其他坑埋在這裡面嗎?我不信! ```java= private final UserBrowsingHistoryRepository repository; @Transactional(readOnly = true) public void sendPromotionMessage(){ try(var historyStream = repository.streamAllByCreatedTimeIsGreaterThan(Instant.now())){ historyStream.forEach(this::sendMessage); } } ``` ## 走起 實際上執行一下程式,來看看我們精美的 stream()~ 啊!坑爹,怎麼一執行就出錯了? ```log! Name:MySQL[RW], Connection:4, Time:7, Success:False Type:Prepared, Batch:False, QuerySize:1, BatchSize:0 Query:[" select user0_.id as userId, user0_.name as name from "user" user0_ where user0_.id=?"] Params:[(5487)] 2022-11-15 17:09:43.226 [http-nio-8099-exec-9] WARN org.hibernate.engine.jdbc.spi.SqlExceptionHelper - SQL Error: 0, SQLState: S1000 2022-11-15 17:09:43.226 [http-nio-8099-exec-9] ERROR org.hibernate.engine.jdbc.spi.SqlExceptionHelper - Streaming result set com.mysql.cj.protocol.a.result.ResultsetRowsStreaming@1429236 is still active. No statements may be issued when any streaming result sets are open and in use on a given connection. Ensure that you have called .close() on any active streaming result sets before attempting more queries. ``` ## 驗屍 先從錯誤訊息中抽出幾項可疑的項目 1. select ... from "user" 2. Ensure that you have called `.close()` on any active streaming result sets before attempting more queries. 就讓我們繼續看下去! ![](https://i.imgur.com/Wzae5tz.png) 針對這兩個可疑項目找出原因 ### 奇怪的 select ... from "user" 查詢 為什麼這句 SQL 的出現很奇怪?因為我們這次查詢的 Entity 是 `UserBrowsingHistoryRepository` 啊!那怎麼會扯到 `User` Entity 了 靈活的小腦瓜兒轉啊轉,想起了 Entiy Definition ```java @Entity public class UserBrowsingHistory{ @Id long id; @Basic Instant createdTime; @ManyToOne User user; } ``` 各位客官是否瞧出了一些端倪?對!就是那個 @ManyToOne Assoication ```java @Target({ElementType.METHOD, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface ManyToOne { Class targetEntity() default void.class; CascadeType[] cascade() default {}; FetchType fetch() default FetchType.EAGER; boolean optional() default true; } ``` 真相永遠只有一個,眼尖的我們發現,`@ManyToOne` Association 預設行為是 Eager Fetch 哦~那有什麼效果嗎? 如果 Entity 中有屬於 Eager Fetch 的關聯,那就會一併跟著查詢出來,所以才會看到這句怪的 SQL 在這個時間點出現 [這邊有個關於 JPA N+1 Query 的小補帖影片](https://www.youtube.com/watch?v=fpQ3ViM5v0c&ab_channel=JCConfTaiwan) ### Ensure that you have called `.close()` >Ensure that you have called `.close()` on any active streaming result sets before attempting more queries. 關於這個,需要回顧 [MySQL JDBC Doc](https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-implementation-notes.html) >There are some caveats with this approach. You must read all of the rows in the result set (or close it) before you can issue any other queries on the connection, or an exception will be thrown. 補充一下 [MySQL 的運作模式](https://mybaseball52.medium.com/mysql-%E6%9F%A5%E8%A9%A2%E9%81%8B%E4%BD%9C%E6%A8%A1%E5%BC%8F-13c2782fc861) >當我們啟動一個查詢的時候,會透過 Client Server Protocol 跟 MySQL Server 要資料,這個過程是一個 half-duplex,意思是說,要等到查詢的整個結果回來之後,才可以根據這個查詢結果作下一個操作。這個意思是說當我們執行 SELECT * FROM [Table] ; 如果結果尚未回傳,整個查詢的過程是被鎖住的, Client 如果要修改查詢內容必須等到整個查詢結果回來才能進行下一步。 所以在 stream 的過程中,如果我們又落井下石在這條連線上面多打了一個 `select ... from "user"` 就導致這個意外發生。 ## 妙手回春 那這樣的話,我們有何方式可以避免這樁慘案的發生 ### LAZY Fetch ```java @ManyToOne(fetch = FetchType.LAZY) ``` 既然是因為 Eager Fetch 追打的查詢導致,那我們改 LAZY 就可以先暫時避免這個情況發生。 ### 不要使用 Entity Query 創建一個新的 Dto 去查詢,等於解除掉 @ManyToOne 的關聯束縛,就不會有意料之外的查詢出現。 ### Stream Block 內其餘操作另開連線處理 ```java @Transactional(propagation = Propagation.REQUIRES_NEW) ``` ### 叫你別動還敢動啊! 安分地不要妄想執行其他 SQL 就是了,乖! ###### tags: `blog` `java` `spring data jpa` `MySQL`