如何在 JPA 實體中使字段可選?
1. 概述
在使用資料庫和 Spring Data 時,通常會遇到並非每個操作都需要實體中的所有欄位的情況。因此,我們可能希望在結果集中使某些欄位可選。
在本教程中,我們將探索使用 Spring Data 和本機查詢從資料庫查詢中僅取得所需列的不同技術。
2. 為什麼是可選字段?
由於需要平衡資料完整性和效能,因此需要在結果集中設定欄位可選。在許多應用程式中,尤其是那些具有複雜資料模型的應用程序,獲取整個實體可能會導致不必要的開銷,主要是當某些欄位與特定上下文或操作無關時。透過從結果集中排除非必要字段,我們可以最大限度地減少處理和傳輸的資料量,從而加快查詢執行速度並降低記憶體使用量。
我們可以在 SQL 層級看到這一點。例如,我們需要來自book
表的數據:
select * from book where id = 1;
想像一下書表有十列。如果不需要其中一些列,我們可以提取一個子集:
select id, title, author from book where id = 1;
3. 設定範例
為了進行演示,讓我們創建一個 Spring Boot 應用程式。假設我們有一個想要從資料庫中取得的書籍清單。我們可以定義一個Book
實體:
@Entity
@Table
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column
private Integer id;
@Column
private String title;
@Column
private String author;
@Column
private String synopsis;
@Column
private String language;
// other fields, getters and setters
}
我們可以使用 H2 記憶體資料庫。讓我們建立application-h2.properties
檔案來設定資料庫屬性:
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update
我們將啟動一個 Spring Boot 應用程序,我們還需要一個特定的設定檔來載入屬性檔:
@Profile("h2")
@SpringBootApplication
public class OptionalFieldsApplication {
public static void main(String[] args) {
SpringApplication.run(OptionalFieldsApplication.class);
}
}
我們將為每個解決方案建立一個@Repository
來存取資料。但是,我們已經可以設定一個具有活動h2
設定檔的測試類別:
@ActiveProfiles("h2")
@SpringBootTest(classes = OptionalFieldsApplication.class)
@Transactional
public class OptionalFieldsUnitTest {
// @Autowired of repositories and tests
}
值得注意的是,我們將使用@Transactional
註釋,並且每個測試都會回滾以便每個測試實例都有一個乾淨的開始。
讓我們來看看使Book
的某些欄位成為可選欄位的範例。例如,我們不需要所有的細節;我們可以只檢索id
、 title
和author
。
4.使用投影
在 SQL 中,投影是指從表或查詢中選擇特定的列或字段,而不是檢索所有數據,從而允許我們限制獲取的數據。
我們可以將投影定義為類別或介面:
public interface BookProjection {
Integer getId();
String getTitle();
String getAuthor();
}
讓我們定義我們的儲存庫來提取投影:
@Repository
public interface BookProjectionRepository extends JpaRepository<Book, Integer> {
@Query(value = "SELECT b.id as id, b.title, b.author FROM Book b", nativeQuery = true)
List<BookProjection> fetchBooks();
}
然後我們可以為BookProjectionRepository
建立一個簡單的測試:
@Test
public void whenUseProjection_thenFetchOnlyProjectionAttributes() {
String title = "Title Projection";
String author = "Author Projection";
Book book = new Book();
book.setTitle(title);
book.setAuthor(author);
bookProjectionRepository.save(book);
List<BookProjection> result = bookProjectionRepository.fetchBooks();
assertEquals(1, result.size());
assertEquals(title, result.get(0).getTitle());
assertEquals(author, result.get(0).getAuthor());
}
一旦我們取得了BookProjection
對象,我們就可以斷言title
和author
是我們保留的。
5.使用DTO
DTO(資料傳輸對象)是一個簡單的對象,用於在應用程式的不同層或元件之間傳輸數據,通常是在資料庫層和業務邏輯或服務層之間。
在本例中,我們將使用它來建立包含我們需要的欄位的資料集物件。因此,我們只會在從資料庫取得資料時填入 DTO 的欄位。讓我們定義BookDto
:
public record BookDto(Integer id, String title, String author) {}
讓我們定義儲存庫來提取 DTO 物件:
@Repository
public interface BookDtoRepository extends JpaRepository<Book, Integer> {
@Query(value = "SELECT new com.baeldung.spring.data.jpa.optionalfields.BookDto(b.id, b.title, b.author) FROM Book b")
List<BookDto> fetchBooks();
}
在本例中,我們使用 JPQL 語法為每個圖書記錄建立一個BookDto
實例。
最後,我們可以新增一個測試來驗證:
@Test
public void whenUseDto_thenFetchOnlyDtoAttributes() {
String title = "Title Dto";
String author = "Author Dto";
Book book = new Book();
book.setTitle(title);
book.setAuthor(author);
bookDtoRepository.save(book);
List<BookDto> result = bookDtoRepository.fetchBooks();
assertEquals(1, result.size());
assertEquals(title, result.get(0).title());
assertEquals(author, result.get(0).author());
}
6.使用@SqlResultSetMapping
我們可以將@SqlResultSetMapping
註釋視為 DTO 或投影的替代方案。要使用它,我們需要將註解應用於實體的類別:
@Entity
@Table
@SqlResultSetMapping(name = "BookMappingResultSet",
classes = @ConstructorResult(targetClass = BookDto.class, columns = {
@ColumnResult(name = "id", type = Integer.class),
@ColumnResult(name = "title", type = String.class),
@ColumnResult(name = "author", type = String.class) }))
public class Book {
// same as intial setup
}
為了識別結果集,我們需要@ConstructorResult
和@ColumnResult
。值得注意的是,這些範例中的結果集具有相同的類別定義。因此,為了方便起見,我們可以重複使用BookDto
類,因為它會匹配相同的建構子。
我們不能將@SqlResultSetMapping
與@Query
一起使用。因此,我們的儲存庫需要額外的工作,因為它將使用EntityManager
。首先,我們需要建立一個自訂儲存庫:
public interface BookCustomRepository {
List<BookDto> fetchBooks();
}
此介麵包含我們想要的方法的簽名並擴展了實際的@Repository
:
@Repository
public interface BookSqlMappingRepository extends JpaRepository<Book, Integer>, BookCustomRepository {}
最後,我們可以創建實作:
@Repository
public class BookSqlMappingRepositoryImpl implements BookCustomRepository {
@PersistenceContext
private EntityManager entityManager;
@Override
public List<BookDto> fetchBooks() {
return entityManager.createNativeQuery("SELECT b.id, b.title, b.author FROM Book b", "BookMappingResultSet")
.getResultList();
}
}
在fetchBooks()
方法中,我們使用EntityManager
和createNativeQuery()
方法來建立本機查詢。
我們也為儲存庫新增一個測試:
@Test
public void whenUseSqlMapping_thenFetchOnlyColumnResults() {
String title = "Title Sql Mapping";
String author = "Author Sql Mapping";
Book book = new Book();
book.setTitle(title);
book.setAuthor(author);
bookSqlMappingRepository.save(book);
List<BookDto> result = bookSqlMappingRepository.fetchBooks();
assertEquals(1, result.size());
assertEquals(title, result.get(0).title());
assertEquals(author, result.get(0).author());
}
@SqlResultSetMapping
是一個更複雜的解決方案。儘管如此,如果我們有一個包含使用EntityManager
和本機或命名查詢的多個查詢的儲存庫,那麼它可能值得使用。
7. 使用Object
或Tuple
我們可以使用Object
或Tuple
進行本機查詢並限製欄位。儘管這些方法的可讀性較差,因為我們不直接存取類別屬性,但它們仍然是有效的選項。
7.1. Object
我們不需要新增任何傳輸對象,因為我們將直接使用Object
類別:
@Repository
public interface BookObjectsRepository extends JpaRepository<Book, Integer> {
@Query("SELECT b.id, b.title, b.author FROM Book b")
List<Object[]> fetchBooks();
}
讓我們看看如何在測試中存取物件值:
@Test
public void whenUseObjectArray_thenFetchOnlyQueryFields() {
String title = "Title Object";
String author = "Author Object";
Book book = new Book();
book.setTitle(title);
book.setAuthor(author);
bookObjectsRepository.save(book);
List<Object[]> result = bookObjectsRepository.fetchBooks();
assertEquals(1, result.size());
assertEquals(3, result.get(0).length);
assertEquals(title, result.get(0)[1].toString());
assertEquals(author, result.get(0)[2].toString());
}
正如我們所看到的,這不是一個動態解決方案,因為我們必須知道列在陣列中的位置。
7.2. Tuple
我們也可以使用[Tuple](https://jakarta.ee/specifications/platform/10/apidocs/jakarta/persistence/tuple)
類別。它是Object,
它很有用,因為我們可以迭代列表並透過別名而不是位置存取屬性,正如我們在前面的範例中看到的那樣。讓我們建立一個BookTupleRepository
:
@Repository
public interface BookTupleRepository extends JpaRepository<Book, Integer> {
@Query(value = "SELECT b.id, b.title, b.author FROM Book b", nativeQuery = true)
List<Tuple> fetchBooks();
}
讓我們看看如何在測試中存取Tuple
值:
@Test
public void whenUseTuple_thenFetchOnlyQueryFields() {
String title = "Title Tuple";
String author = "Author Tuple";
Book book = new Book();
book.setTitle(title);
book.setAuthor(author);
bookTupleRepository.save(book);
List<Tuple> result = bookTupleRepository.fetchBooks();
assertEquals(1, result.size());
assertEquals(3, result.get(0).toArray().length);
assertEquals(title, result.get(0).get("title"));
assertEquals(author, result.get(0).get("author"));
}
不過,這也不是一個動態的解決方案。但是,我們可以使用別名或列名存取列值。
八、結論
在本文中,我們了解如何使用 Spring Data 來分隔資料庫結果集的列數。我們了解如何使用投影、DTO和@SqlResultSetMapping
並獲得了類似的結果。我們也了解如何使用Object
或Tuple
以及如何按位置數組存取通用結果集。
與往常一樣,所有原始程式碼都可以在 GitHub 上取得。