4 ways to query using Spring Data

Calin C
Yonder TechBlog
Published in
6 min readJul 30, 2023

--

The ability to efficiently access and manipulate data is crucial for building robust, scalable, and maintainable applications. Traditionally, interacting with databases and data repositories required developers to write intricate and boilerplate code, often diverting their attention from core business logic. However, with the advent of modern frameworks and libraries, developers can now focus on what truly matters — creating innovative solutions to real-world problems. One such powerful tool that has revolutionised data access in the Java ecosystem is Spring Data.

In this article, we will explore how Spring Data enables developers to tackle various data access scenarios elegantly. We will delve into common use cases, each presenting a unique data access challenge, and demonstrate how Spring Data provides intuitive solutions to overcome them.

1. You have to fetch entities by a custom condition

Let’s begin with a straightforward scenario: fetching an entity from your database based on a condition. For this case, I recommend opting for the simplest approach by using query methods. Not only is this method highly readable, but it also effectively accomplishes its task. To further enhance your development experience, I strongly suggest using an IDE that provides Spring Data support, as it offers additional (type-)safety measures and code completion. This becomes particularly beneficial when adhering to conventions in your code.

interface PersonRepository : JpaRepository<Person, UUID> {
fun findDistinctByAgeAndNameIgnoreCase(age: Int, name: String): List<Person>
}

Here’s a more detailed explanation of the method:

  1. findDistinctBy: This is a keyword prefix that indicates that we want to find distinct records that match the given criteria.
  2. AgeAndName: These are the properties (fields) of the Person entity that we want to filter by. The method will find Person entities that have both the specified age and name.
  3. IgnoreCase: This is a keyword suffix that indicates that the name comparison should be case-insensitive. It means that the method will consider names with different casing as equal (e.g., "John" and "john" will be treated as the same).

2. You have to fetch multiple related entities at once

I assume that you have employed FetchType.LAZY on every entity relationship. If not, I strongly advise you to investigate why this practice is considered a best practice. However, there are certain scenarios where you need to retrieve a hierarchy or a graph of entities, and using lazy loading for entity relationships can result in performance issues, such as the notorious N+1 problem.

Switching to FetchType.EAGER might seem like a solution, but it introduces another problem: JPA/Hibernate will load these collections regardless of whether they are necessary for the current use case, leading to potential inefficiencies.

To address this challenge, I recommend utilising the power of @NamedEntityGraph, which allows you to modify the fetch plan at runtime. This way, you can optimize the retrieval of related entities based on the specific requirements of each use case. By strategically defining entity graphs, you can strike a balance between fetching only the data needed and avoiding unnecessary data loading, ultimately enhancing the performance and efficiency of your application.

@Entity
@NamedEntityGraph(name = "Author.books",
attributeNodes = @NamedAttributeNode("books")
)
data class Author(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,

val name: String,

@OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
val books: List<Book> = emptyList()
)

And in your repository you can provide the entity graph that you wanted:

@Repository
interface AuthorRepository : JpaRepository<Author, Long> {

@EntityGraph(value = "Author.books", type = EntityGraphType.LOAD)
fun findByName(name: String): Author?
}

Or you can use an AD-HOC graph definition:

@Repository
interface AuthorRepository : JpaRepository<Author, Long> {

@EntityGraph(attributePaths = ["books"])
fun findByName(name: String): Author?
}

In this case, the query will fetch the Author with a collection of books. Everything is executed at once and the books are fetched eagerly. Another nice feature is that it allows you to declare subgraphs if you want go deep through your entity hierarchy/graph structure.

3. You have to aggregate data

Whether you need to calculate statistics, generate reports, or provide analytics, efficient data aggregation is essential for making informed decisions. Instead of fetching entities you want to combine and to aggregate using existing functions some data into an object:

interface SaleRepository : JpaRepository<Sale, Long> {
@Query("SELECT NEW com.example.dto.RevenuePerCategoryDTO(s.product.category, SUM(s.price)) FROM Sale s GROUP BY s.product.category")
fun getTotalRevenuePerCategory(): List<RevenuePerCategoryDTO>
}

In this case, I recommend to use JPQL to access built-in functions like sum or other aggregation functions. JPQL allows you create instances of your DTO directly from the query results (Also known as Projection technique). The thing I don’t like using JPQL is lack of full type safety since it uses string-based queries and if it is too big or complex, it becomes a little bit unmaintainable. Again, make sure that your IDE can give you a little bit of help in this directions using built-in linters features or 3rd party plugins.

4. You need just a few properties from your entity

In enterprise projects, it is not uncommon for entities to grow significantly in size, accumulating numerous properties over time. In my own experience, I have encountered projects where entities swelled to encompass almost 50–70 properties. While this may not be an ideal situation, it’s a reality we sometimes face; no software project is entirely flawless.

Now, consider the scenario where you need to query this massive entity to display just two or three specific properties on the UI. It’s akin to searching for a needle in a haystack! The inefficiency and performance overhead in retrieving a bulk of unnecessary data can be quite significant.

To address this issue, I highly recommend employing the technique I mentioned earlier: Projection. By using Projection, you can selectively retrieve only the required properties from the entity, rather than fetching the entire entity itself. This approach results in more focused and optimized data retrieval, leading to improved UI performance and reduced database load:

@Entity
class Person {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
var id: UUID? = null

@Column
var firstName: String? = null

@Column
var lastName: String? = null

@Column
var age: Int? = null

@OneToMany
var addresses: List<Address>? = null

// ... and 100 properties more
}
interface PersonRepository : JpaRepository<Person, UUID> {
@Query("SELECT new com.example.dto.PersonAgeDto(p.firstName, p.lastName, p.age) FROM Person p")
fun findPeopleNameAndAge(): List<Person>
}

This example is similar with the example above when I discussed about aggregation and I mentioned that complex JPQL are hard to maintain:

Take the example where you want to retrieve a root entity with a few properties along with its projected child associations on multiple levels. How do you solve that?

To achieve this, I strongly recommend utilising an additional helper library called Blaze Persistence. This powerful library complements your Spring Data repositories with a rich and type-safe API, seamlessly integrating with Spring and offering a bunch of features to enhance your queries. However, the module that specifically captures our interest in this case is the Entity View module.

This module empowers you to create custom, lightweight views of your entities, tailored to match the specific requirements of your use cases. These views, known as “entity views,” serve as a projection mechanism, enabling you to extract only the necessary data from your entities while retaining the type-safety and strong-typing benefits.

@EntityView(Person::class.java)
interface PersonView {
@IdMapping()
fun getId(): UUID?
fun getFirstName(): String?
fun getLastName(): String?
fun getAge(): Int?

@Mapping("addresses.country")
fun getAddresses(): List<String>
}
interface PersonViewRepository : EntityViewRepository<PersonView, UUID> {
fun findAll(): List<PersonView>
}

The problem at hand is addressed through a more elegant solution compared to the existing built-in alternatives like ResultTransformer or JPA Criteria API. This solution revolves around creating an “Entity View,” which shares a similar concept with Table View but within the JPA context. By exposing the Entity View through an EntityViewRepository, you can follow a pattern analogous to JpaRepository.

Conclusion

Selecting the optimal data retrieval method from your repository technology is a crucial task, as it directly impacts the performance and maintainability of your application. Beyond just the developer experience, factors like maintainability and simplicity should also be taken into consideration. My firm recommendation is to always use the right tool for the right job.

Throughout this article, I have presented four common use cases that Spring Data can efficiently address with its diverse array of features. It is important to note that these solutions can even be combined to tackle more complex scenarios beyond the presented use cases. Always check the documentation to find if there better alternatives than the solution you know.

While Spring Data and JPA are indeed powerful technologies, they do come with their own set of limitations. Therefore, don’t hesitate to explore and experiment with additional libraries like Blaze Persistence, which can seamlessly supplement your requirements and enhance your data access capabilities.

Furthermore, I cannot stress enough the importance of paying close attention to the raw SQL generated by JPA/Hibernate or any other database abstraction tools. Understanding the underlying queries can reveal potential performance bottlenecks and aid in fine-tuning your application’s data access layer.

In conclusion, choosing the right data retrieval methods can help you make the most of Spring Data and JPA. This approach will lead to strong, easy-to-maintain, and high-performance applications. Embrace these practices to ensure an elegant and efficient data access experience.

--

--