1. Overview
When working with databases and Spring Data, it’s common to encounter scenarios where not all fields in an entity are necessary for every operation. Therefore, we might want to make some field optional in our result set.
In this tutorial, we’ll explore different techniques for fetching only the columns we need from a database query using Spring Data and native queries.
2. Why Optional Fields?
The need to make fields optional in result sets arises from the necessity to balance data integrity and performance. In many applications, especially those with complex data models, fetching entire entities can lead to unnecessary overhead, mainly when certain fields are irrelevant to a specific context or operation. By excluding non-essential fields from the result set, we can minimize the amount of data processed and transferred, leading to faster query execution and lower memory usage.
We can see this at the SQL level. For example, we want data from a book table:
select * from book where id = 1;
Imagine the book table has ten columns. If some of those columns aren’t required, we can extract a subset:
select id, title, author from book where id = 1;
3. Example Setup
To demonstrate, let’s create a Spring Boot application. Let’s say we have a list of books we want to get from a database. We can define a Book entity:
@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
}
We can use an H2 in-memory database. Let’s create the application-h2.properties file to set the database 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
We’ll start a Spring Boot application, and we also want a specific profile loading the properties file:
@Profile("h2")
@SpringBootApplication
public class OptionalFieldsApplication {
public static void main(String[] args) {
SpringApplication.run(OptionalFieldsApplication.class);
}
}
We’ll create a @Repository for every solution to access the data. However, we can already set up a test class with an active h2 profile:
@ActiveProfiles("h2")
@SpringBootTest(classes = OptionalFieldsApplication.class)
@Transactional
public class OptionalFieldsUnitTest {
// @Autowired of repositories and tests
}
Notably, we’ll use the @Transactional annotation, and every test will roll back to have a clean start for every test instance.
Let’s look at examples of making some fields optional for a Book. For example, we don’t need all the details; we’re fine with retrieving only the id, title, and author.
4. Use a Projection
In SQL, projection refers to selecting specific columns or fields from a table or query rather than retrieving all the data, thus allowing us to limit the data we fetch.
We can define our projection as a class or an interface:
public interface BookProjection {
Integer getId();
String getTitle();
String getAuthor();
}
Let’s define our repository to extract the projection:
@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();
}
We can then create a simple test for the 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());
}
Once we fetch the BookProjection objects, we can assert that the title and author are the ones we persisted.
5. Use a DTO
A DTO (Data Transfer Object) is a simple object used to transfer data between different layers or components of an application, typically between the database layer and the business logic or service layer.
In this case, we’ll use it to create a dataset object with the fields we need. Therefore, we’ll only populate the DTO’s fields when fetching from the database. Let’s define our BookDto:
public record BookDto(Integer id, String title, String author) {}
Let’s define our repository to extract the DTO objects:
@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();
}
In this case, we use a JPQL syntax to create an instance of BookDto for every book record.
Finally, we can add a test to verify:
@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. Use @SqlResultSetMapping
We can look at the @SqlResultSetMapping annotation as an alternative to DTO or projection. To use it, we need to apply the annotation to the entity’s class:
@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
}
To identify the result set, we need @ConstructorResult and @ColumnResult. Notably, the result set in these examples has the same class definition. Therefore, we can reuse the BookDto class for convenience, as it matches the same constructor.
We can’t use the @SqlResultSetMapping with @Query. Therefore, our repository needs extra work because it will use the EntityManager. First, we need to create a custom repository:
public interface BookCustomRepository {
List<BookDto> fetchBooks();
}
This interface includes the signature for the method we want and extends the actual @Repository:
@Repository
public interface BookSqlMappingRepository extends JpaRepository<Book, Integer>, BookCustomRepository {}
Finally, we can create the implementation:
@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();
}
}
In the fetchBooks() method, we create a native query using the EntityManager and the createNativeQuery() method.
Let’s also add a test for the repository:
@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 is a more complex solution. Nonetheless, if we have a repository with multiple queries using the EntityManager and native or named queries, it could be worth using.
7. Use Object or Tuple
We can do native queries and restrict fields using Object or Tuple. Although these methods are less readable as we don’t directly access the class properties, they’re still valid options.
7.1. Object
We don’t need to add any transfer object as we’ll use directly the Object class:
@Repository
public interface BookObjectsRepository extends JpaRepository<Book, Integer> {
@Query("SELECT b.id, b.title, b.author FROM Book b")
List<Object[]> fetchBooks();
}
Let’s look at how we can access the object values in a test:
@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());
}
As we can see, this isn’t a dynamic solution because we must know the columns’ position in the array.
7.2. Tuple
We can also use the Tuple class. It’s a wrapper to an array of Object, and it’s helpful because we can iterate over a list and access the property by the alias instead of positionally, as we have seen in the previous example. Let’s create a 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();
}
Let’s look at how we can access the Tuple values in a test:
@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"));
}
Still, this isn’t a dynamic solution either. However, we can access the column values using the alias or the column name.
8. Conclusion
In this article, we saw how to delimit the number of columns of a database result set using Spring Data. We saw how to use projections, DTOs, and @SqlResultSetMapping and got similar results. We also saw how to use Object or Tuple and access the generic result sets as array positionally.
As always, all source code is available over on GitHub.