1. 概述
在使用Spring Data操作数据库时,我们经常遇到某些场景:并非实体中的所有字段在每次操作中都是必需的。因此,我们可能希望让结果集中的某些字段变为可选。
本文将探讨使用Spring Data和原生查询时,从数据库查询中仅获取所需列的不同技术方案。
2. 为什么需要可选字段?
在结果集中实现字段可选的需求,源于平衡数据完整性与性能的必要性。在许多应用中(尤其是具有复杂数据模型的应用),获取整个实体会导致不必要的开销,特别是当某些字段与特定上下文或操作无关时。通过从结果集中排除非必要字段,我们可以最小化处理和传输的数据量,从而加快查询速度并降低内存消耗。
在SQL层面可以直观看到这一点。例如,从book
表获取数据:
select * from book where id = 1;
假设book
表有十列。如果其中某些列不需要,我们可以提取子集:
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;
// 其他字段、getter和setter
}
使用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和测试方法
}
注意:我们使用@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 {
// 同初始设置
}
需要@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
类。它是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"));
}
这仍不是动态方案,但可通过别名或列名访问列值。
8. 总结
本文介绍了使用Spring Data限制数据库结果集列数的方法。我们学习了如何使用投影、DTO和@SqlResultSetMapping
得到相似结果,还探讨了如何使用Object
或Tuple
按数组位置访问通用结果集。
✅ 优先推荐方案:
- 投影:简洁高效,适合简单字段选择
- DTO:灵活可控,适合复杂转换逻辑
⚠️ 谨慎使用的方案:
@SqlResultSetMapping
:配置复杂,适合多查询复用场景Object
/Tuple
:可读性差,仅建议在特殊场景使用
选择方案时需权衡开发效率与运行时性能,避免过度优化导致代码维护困难。