8 ways to write queries in Spring Data
Intro
Spring Data is a great way to simplify your code accessing the database. It also gives a lot of flexibility in terms of available abstractions. Programmers can choose from high-level constructs requiring minimum amount of code or dive deep into nitty-gritty implementation details, while accepting a more verbose solution.
Kickstarting a project with Spring Data is as simple as adding this dependency:
Assuming we have such entity:
…stored in the following table:
Let’s add the following test data:
1) Filtering based on method names
By carefully crafting a repository method name we can avoid writing even a single line of SQL code.
For example this method signature:
…will cause Hibernate to spit out something along the lines of:
We can easily select employees whose name contains a given string of characters and whose salary is above a threshold. No SQL, no JPQL, no HQL, just Java code.
Now, with the test data inserted earlier, when we call this:
…we should get a one-element list containing only Alice:
2) Using @Query annotation
The whole process of experimentation and trying to come up with the right method name is fun and exciting, but sometimes the end result is not that satisfying.
findByNameContainingIgnoreCaseAndSalaryGreaterThanOrderBySalaryDesc is quite long and looks clunky. Try to say it in one breath. It feels like we have traded readability for a quicker result.
It may be worth to replace this long method name with a shorter equivalent, where we can decide how to call the method, but the implementation in JPQL has to be provided in return (there is no free lunch I am afraid):
Now the method name looks friendlier:
3) Native @Query
We have sorted out the problem of having too long method names, but in the process we have introduced rather unpleasant implementation of case-insensitive LIKE condition:
In some databases there is a nicer ILIKE keyword that can be used to make case-insensitive matching with less effort.
The code could then be refactored to this (please note nativeQuery attribute set to true):
The caveat here is that we expose ourselves to a possible vendor lock-in so usually it is better to stay on the safe side and use nativeQuery sparingly.
Here nativeQuery just makes the code nicer to look at and it might be up for debate whether it is the best solution in this scenario. There are, however, situations when you absolutely need it. It is up to you to make the call when it should be used and when not. The decision is not always easy, but programming is the art of trade-offs after all.
4) Query by example
Another interesting alternative is querying by providing an example record:
Unfortunately, this is mostly limited to strings as stated in the docs which boils down to number expressions (e.g. greater/lower than) not being supported:
5) Specifications
A more advanced option are specifications, which were built on top of criteria API in JPA.
Here is how the previous conditions can be implemented as specifications:
After adding JpaSpecificationExecutor to the list of interfaces implemented by the repository:
…you can select elements based on specifications by calling an overloaded findAll method:
The best part is that specifications can be mixed together allowing for better customizability and reusability:
6) Querydsl
One downside of specifications is that they are not strictly typed, meaning we can run into issues when misstyping a property name or if we assume incorrect property type.
To enforce a stricter name and type checking Querydsl can be used:
It works by generating special classes prepended with “Q” and based on the entity classes. In our case Querydsl will generate QEmployee class from the corresponding Employee entity.
Below you can find Maven configuration for generating Q* classes during compilation:
After setting it up you can write code like this:
What’s great about Querydsl is we get autocompletion:
Custom repositories
It is very likely, that at one point you will reach a situation where all the above solutions do not work. For such scenarios Spring Data provides a way to strip all the layers of abstractions and get down to writing a more specific implementation.
The assumption is that you will rarely fallback to writing low-level custom code and most of the time you will stick to Spring Data repositories cherishing the features described so far.
In order to extend an existing Spring Data repository you need to create a dedicated repository interface:
…that is then extended by your original repository:
Finally, you should put your code inside an implementation of the custom interface:
Spring Data will do all the plumbing for you so that when findByNameLikeAndSalaryCustom is called on the interface the call is then correctly propagated to the implementation inside EmployeeRepositoryCustomImpl:
7) Custom repository with Criteria API
Going back to our initial problem of having to overcome difficulties with previous solutions here is how JPA criteria API can play together with custom repositories to implement dynamic queries:
What immediately stands out is the verbosity of this solution, but it works quite well when you need to implement a search capability where criteria changes frequently.
8) Old school approach using StringBuilder
You should be able to solve 99% of problems using one of the previous 7 different alternatives.
However, so far most of the solutions relied on JPA (and JPQL) which, like any other abstraction, has its own shortcomings. You may want to have in your toolkit something highly-customizable that can circumvent JPA.
If you mix the ol’ school approach of building the SQL query yourself together with custom repositories you might end up with something like this:
Such code has many disadvantages: it is incredibly prone to typos and it is also quite verbose. But this is the price to be paid for having the flexibility of accessing the database in a very direct way.
Still a good rule of thumb is to use it as a last resort. If it is unavoidable then you have to do what you have to do, but considering the alternatives is advisable.