Bạn đã nghe về Projection trong Spring Boot chưa?
- Tram Ho
Nếu bạn đã xử dụng nhiều mapping framework như ModelMapper, MapStruct, JMapper,… bạn sẽ nhận ra mỗi loại framework có ưu điểm riêng biệt, ModelMapper cho phép sử dụng cực kì nhanh thông qua instance của nó, MapStruct cho phép ta định nghĩa các interface và thấy tường minh quá trình mapping bên trong thông qua auto-gen implementation. Vậy bạn có tự hỏi tại sao Spring Boot không tạo riêng cho mình 1 cách mapping duy nhất để thống nhất code. Thật ra có khá nhiều cơ chế như Converter, Projection,… và trong bài này mình sẽ giới thiệu về Projection.
Context
Trước khi đi vào chi tiết, mình sẽ mô tả case của project để dùng projection. Project gồm 2 entity là Book và Author.. không có magic vào khó hiểu cả
Mình có tạo seed cho Book và Author ở file DataSeedingRunner.java và để đây cho tiện theo dõi
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <span class="token comment">// seed data for author</span> <span class="token class-name">Author</span> auth0 <span class="token operator">=</span> <span class="token class-name">Author</span><span class="token punctuation">.</span><span class="token function">builder</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">name</span><span class="token punctuation">(</span><span class="token string">"Nguyen Van Teo"</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">address</span><span class="token punctuation">(</span><span class="token string">"Vietnam"</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">build</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> auth0 <span class="token operator">=</span> authorService<span class="token punctuation">.</span><span class="token function">createOne</span><span class="token punctuation">(</span>auth0<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// seed data for book</span> <span class="token class-name">Book</span> book0 <span class="token operator">=</span> <span class="token class-name">Book</span><span class="token punctuation">.</span><span class="token function">builder</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">title</span><span class="token punctuation">(</span><span class="token string">"Book of auth0"</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">verboseCode</span><span class="token punctuation">(</span><span class="token string">"00123"</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">author</span><span class="token punctuation">(</span>auth0<span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">build</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> bookService<span class="token punctuation">.</span><span class="token function">createOne</span><span class="token punctuation">(</span>book0<span class="token punctuation">)</span><span class="token punctuation">;</span> |
Dive into Projection
Nếu bạn muốn xem qua về định nghĩa, tham khảo tại Spring Document tại ĐÂY
Trong bài viết này mình sẽ đi thẳng vào ví dụ để trực quan hơn, Projection được dùng ở giai đoạn hứng dữ liệu từ Repository JPA trả về nên đầu tiên chúng ta sẽ cần có project Spring Boot sử dụng JPA, Projection không yêu cầu bất cứ dependency nào khác.
Interface-based Projection
Một trong 2 cách sử dụng Projection, mình hay dùng cách này vì viết code gọn hơn do chỉ cần khai báo interface và các method liên quan.
Okay, khi mình lấy hết book sẽ nhận được giá trị json như sau
1 2 3 4 5 6 7 8 9 10 11 12 13 | [ { "id": 1, "title": "Book of auth0", "author": { "id": 1, "name": "Nguyen Van Teo", "address": "Vietnam" }, "verboseCode": "00123" } ] |
Giờ thì requirement sẽ như sau: trong author chỉ lấy id
và bỏ verboseCode
. Chúng ta sẽ giải quyết bằng interface projection.
Tạo interface BookSlim.java (Projection)
1 2 3 4 5 6 7 8 9 10 11 12 | <span class="token keyword">public</span> <span class="token keyword">interface</span> <span class="token class-name">BookSlim</span> <span class="token punctuation">{</span> <span class="token class-name">Long</span> <span class="token function">getId</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token class-name">String</span> <span class="token function">getTitle</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token class-name">AuthorWithId</span> <span class="token function">getAuthor</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">interface</span> <span class="token class-name">AuthorWithId</span> <span class="token punctuation">{</span> <span class="token class-name">Long</span> <span class="token function">getId</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
Giải thích cơ chế:
- Method của attribute cần lấy phải khớp với getter method của nó trong entity.
- Đối với composition complex object (như Author), chúng ta có thể định nghĩa interface cho nó với cách viết method tương tự và nhớ là mọi method đều phải khớp với getter method trong entity.
Tiếp theo, làm sao để viết method trả về BookSlim như ta biết khi dùng Repository interface phải viết theo chuẩn Dynamic Method hoặc phải có annotaion @Query
, may thay Dynamic Method có thể dùng như sau
1 2 3 4 | <span class="token keyword">public</span> <span class="token keyword">interface</span> <span class="token class-name">BookRepository</span> <span class="token keyword">extends</span> <span class="token class-name">JpaRepository</span><span class="token generics"><span class="token punctuation"><</span><span class="token class-name">Book</span><span class="token punctuation">,</span> <span class="token class-name">Long</span><span class="token punctuation">></span></span><span class="token punctuation">{</span> <span class="token generics"><span class="token punctuation"><</span><span class="token class-name">T</span><span class="token punctuation">></span></span> <span class="token class-name">List</span><span class="token generics"><span class="token punctuation"><</span><span class="token class-name">T</span><span class="token punctuation">></span></span> <span class="token function">findBy</span><span class="token punctuation">(</span><span class="token class-name">Class</span><span class="token generics"><span class="token punctuation"><</span><span class="token class-name">T</span><span class="token punctuation">></span></span> classType<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> |
Và cách chúng ta gọi sử dụng
1 2 3 4 5 | <span class="token comment">// get without projection</span> bookRepository<span class="token punctuation">.</span><span class="token function">findAll</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// get with projection</span> bookRepository<span class="token punctuation">.</span><span class="token function">findBy</span><span class="token punctuation">(</span><span class="token class-name">BookSlim</span><span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">)</span><span class="token punctuation">;</span> |
Note: nhờ Generic, chúng ta có thể tạo ra nhiều interface khác nhau mà vẫn áp dụng được cho method
findBy
. Đây được gọi là Dynamic Projection (anh em của Dynamic Method đây mà)
Kết quả:
1 2 3 4 5 6 7 8 9 10 | <span class="token punctuation">[</span> <span class="token punctuation">{</span> <span class="token property">"id"</span><span class="token operator">:</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token property">"title"</span><span class="token operator">:</span> <span class="token string">"Book of auth0"</span><span class="token punctuation">,</span> <span class="token property">"author"</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">"id"</span><span class="token operator">:</span> <span class="token number">1</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">]</span> |
Trông ổn phết Vậy trong trường hợp lấy ra 1 object thôi thì thế nào? Chẳn hạn findById
. Rất đơn giản như sau
1 2 3 4 5 6 7 | <span class="token keyword">public</span> <span class="token keyword">interface</span> <span class="token class-name">BookRepository</span> <span class="token keyword">extends</span> <span class="token class-name">JpaRepository</span><span class="token generics"><span class="token punctuation"><</span><span class="token class-name">Book</span><span class="token punctuation">,</span> <span class="token class-name">Long</span><span class="token punctuation">></span></span><span class="token punctuation">{</span> <span class="token generics"><span class="token punctuation"><</span><span class="token class-name">T</span><span class="token punctuation">></span></span> <span class="token class-name">List</span><span class="token generics"><span class="token punctuation"><</span><span class="token class-name">T</span><span class="token punctuation">></span></span> <span class="token function">findBy</span><span class="token punctuation">(</span><span class="token class-name">Class</span><span class="token generics"><span class="token punctuation"><</span><span class="token class-name">T</span><span class="token punctuation">></span></span> classType<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">//findById with Projection</span> <span class="token generics"><span class="token punctuation"><</span><span class="token class-name">T</span><span class="token punctuation">></span></span> <span class="token class-name">T</span> <span class="token function">findById</span><span class="token punctuation">(</span><span class="token class-name">Long</span> id<span class="token punctuation">,</span> <span class="token class-name">Class</span><span class="token generics"><span class="token punctuation"><</span><span class="token class-name">T</span><span class="token punctuation">></span></span> type<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> |
Các bạn tự test xem kết quả cho có hứng thú
Closed Projections
Cũng là 1 interface projection nhưng có đặc điểm là các method bên trong đều match với các property thuần của entity đó, interface BookSlim là 1 closed projection.
Open Projections
Một số trường hợp ta cần trả thêm thông tin là kết hợp của nhiều trường phức tạp, chẳn hạn cần trả thêm verId
là kết hợp giữa verboseCode
và id
. Chúng ta làm như sau
1 2 3 4 5 6 7 8 9 | <span class="token keyword">public</span> <span class="token keyword">interface</span> <span class="token class-name">BookSlimWithVerId</span> <span class="token punctuation">{</span> <span class="token class-name">Long</span> <span class="token function">getId</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token class-name">String</span> <span class="token function">getTitle</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token annotation punctuation">@Value</span><span class="token punctuation">(</span><span class="token string">"#{target.id.toString() + ' ' + target.title}"</span><span class="token punctuation">)</span> <span class="token class-name">String</span> <span class="token function">getVerId</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> |
Vì chúng ta đã dùng generic, chỉ việc dùng lại method và thay đổi tham số là được
1 2 | bookRepository<span class="token punctuation">.</span><span class="token function">findBy</span><span class="token punctuation">(</span><span class="token class-name">BookSlimWithVerId</span><span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">)</span><span class="token punctuation">;</span> |
Kết quả:
1 2 3 4 5 6 7 8 | <span class="token punctuation">[</span> <span class="token punctuation">{</span> <span class="token string">"id"</span><span class="token operator">:</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token string">"title"</span><span class="token operator">:</span> <span class="token string">"Book of auth0"</span><span class="token punctuation">,</span> <span class="token string">"verId"</span><span class="token operator">:</span> <span class="token string">"1 00123"</span> <span class="token punctuation">}</span> <span class="token punctuation">]</span> |
Theo Doc của Spring, biểu thức trong @Value
không nên phức tạp, cách khác để thay thế là dùng default method trong interface được giới thiệu ở java 8
1 2 3 4 | <span class="token keyword">default</span> <span class="token class-name">String</span> <span class="token function">getVerId</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token function">getId</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">concat</span><span class="token punctuation">(</span><span class="token string">" "</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">concat</span><span class="token punctuation">(</span><span class="token function">getVerboseCode</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> |
Class-based Projection
Khái niệm này gần với việc chúng ta sử dụng DTO (Data Transfer Object), về chức năng nó không khác Interface Projection ngoại trừ không dùng proxy (vì nó đã trả về object của class rồi) và cũng không dùng được nested projection (AuthorWithId trong BookSlim là 1 nested projection).
Thay vì dùng Interface mình sẽ convert BookSlim sang class:
1 2 3 4 5 6 | <span class="token annotation punctuation">@Value</span> <span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">BookSlimDTO</span> <span class="token punctuation">{</span> <span class="token class-name">Long</span> id<span class="token punctuation">;</span> <span class="token class-name">String</span> title<span class="token punctuation">;</span> <span class="token punctuation">}</span> |
Annotation
@Value
là của Lombok với tác dụng generate ra 1 số code tự động, cần thiết nhất là@AllArgsContructor
để đáp ứng Projection.
Game là dễ Nhưng rất tiếc với cách này chúng ta không thể trả về thêm Author bên trong được. Nếu gặp phải hạn chế từ interface projection nhưng class projection cũng không đáp ứng được, bạn biết rồi đấy, đến lúc phải dùng mapper rồi
Summary
Lặn cũng lâu rồi chúng ta ngoi lên thôi, vậy là bài viết này đã cung cấp kiến thức cần thiết về Projection, chúc bạn tích lũy thêm kiến thức mới.
Tham khảo
Spring Projection Documentation : https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#projections
Source code Github : https://github.com/phatnt99/spring-boot-tutorial/tree/main/projection