Spring Data is an umbrella project containing many projects specific to various databases. These projects are developed in partnership with the companies creating the database technologies themselves. Spring Data’s goal is to provide an abstraction for data access while retaining the underlying specifics of the various data stores.
It provides a layer of abstraction on top of a JPA provider (such as Hiber nate), in the spirit of the Spring framework, taking control of the configuration and transactions management.
Spring Data JPA provides support for interacting with Spring Data JPA JPA repositories. it is built on top of the functionality offered by the Spring Data Commons project and the JPA provider (Hibernate in our case).
1public interface UserRepository extends CrudRepository<User, Long> {
2}
The UserRepository interface extends CrudRepository<User, Long>. This means that it is a repository of User entities, which have a Long identifier. We can directly call methods such as save, findAll, and findById, inherited from CrudRepository, and we can use them without any additional information to execute the usual operations against the database. Spring Data JPA will create a proxy class implementing the UserRepository interface and implement its methods.
CrudRepository is a generic technology-agnostic persistence interface that we can use not only for JPA/relational databases but also for NoSQL databases.
JpaRepository extends PagingAndSortingRepository, which, in turn, extends CrudRepository. CrudRepository provides basic CRUD functionality, whereas PagingAndSortingRepository offers convenient methods that sort and paginate the records. JpaRepository offers JPA-related methods, such as flush ing the persistence context and deleting records in a batch. Additionally, JpaRepository overwrites a few methods from CrudRepository, such as findAll, findAllById, and saveAll to return List instead of Iterable.
1@Entity
2@Table(name = "USERS")
3public class User {
4 @Id
5 @GeneratedValue
6 private Long id;
7 private String username;
8 private LocalDate registrationDate;
9 private String email;
10 private int level;
11 private boolean active;
12
13 public User() {
14 }
15
16 public User(String username) {
17 this.username = username;
18 }
19
20 public User(String username, LocalDate registrationDate) {
21 this.username = username;
22 this.registrationDate = registrationDate;
23 }
24 //getters and setters
25}
26
1public interface UserRepository extends JpaRepository<User, Long> {
2
3 User findByUsername(String username);
4
5 List<User> findAllByOrderByUsernameAsc();
6
7 List<User> findByRegistrationDateBetween(LocalDate start, LocalDate end);
8
9 List<User> findByUsernameAndEmail(String username, String email);
10
11 List<User> findByUsernameOrEmail(String username, String email);
12
13 List<User> findByUsernameIgnoreCase(String username);
14
15 List<User> findByLevelOrderByUsernameDesc(int level);
16
17 List<User> findByLevelGreaterThanEqual(int level);
18
19 List<User> findByUsernameContaining(String text);
20
21 List<User> findByUsernameLike(String text);
22
23 List<User> findByUsernameStartingWith(String start);
24
25 List<User> findByUsernameEndingWith(String end);
26
27 List<User> findByActive(boolean active);
28
29 List<User> findByRegistrationDateIn(Collection<LocalDate> dates);
30
31 List<User> findByRegistrationDateNotIn(Collection<LocalDate> dates);
32
33}
This query mechanism removes prefixes and suffixes such as find...By, get...By, query...By, read...By, and count...By from the name of the method and parses the remainder of it.
You can declare methods containing expressions as Distinct to set a distinct clause; declare operators as LessThan, GreaterThan, Between, or Like; or declare compound conditions with And or Or. You can apply static ordering with the OrderBy clause in the name of the query method, referencing a property and providing a sort- ing direction (Asc or Desc). You can use IgnoreCase for properties that support such a clause. For deleting rows, you’d have to replace find with delete in the names of the methods. Also, Spring Data JPA will look at the return type of the method. If you want to find a User and return it in an Optional container, the method return type will be Optional<User>. A full list of possible return types, together with detailed explanations, can be found in appendix D of the Spring Data JPA reference documentation (Repository query return types ).
The names of the methods need to follow the rules. If the method naming is wrong (for example, the entity property does not match in the query method), you will get an error when the application context is loaded.
Keyword | Example | Generated JPQL |
---|---|---|
Is, Equals | findByUsername findByUsernameIs findByUsernameEquals | . . . where e.username = ?1 |
And | findByUsernameAndRegistrationDate | . . . where e.username = ?1 and e.registrationdate = ?2 |
Or | findByUsernameOrRegistrationDate | . . . where e.username = ?1 or e.registrationdate = ?2 |
LessThan | findByRegistrationDateLessThan | . . . where e.registrationdate < ?1 |
LessThanEqual | findByRegistrationDateLessThanEqual | . . . where e.registrationdate <= ?1 |
GreaterThan | findByRegistrationDateGreaterThan | . . . where e.registrationdate > ?1 |
GreaterThanEqual | findByRegistrationDateGreaterThanEqual | . . . where e.registrationdate >= ?1 |
Between | findByRegistrationDateBetween | . . . where e.registrationdate between ?1 and ?2 |
OrderBy | findByRegistrationDateOrderByUsernameDesc | . . . where e.registrationdate = ?1 order by e.username desc |
Like | findByUsernameLike | . . . where e.username like ?1 |
NotLike | findByUsernameNotLike | . . . where e.username not like ?1 |
Before | findByRegistrationDateBefore | . . . where e.registrationdate < ?1 |
After | findByRegistrationDateAfter | . . . where e.registrationdate > ?1 |
Null, IsNull | findByRegistrationDate(Is)Null | . . . where e.registrationdate is null |
NotNull, IsNotNull | findByRegistrationDate(Is)NotNull | . . . where e.registrationdate is not null |
Not | findByUsernameNot | . . . where e.username <> ?1 |
The first and top keywords (used equivalently) can limit the results of query methods. The top and first keywords may be followed by an optional numeric value to indicate the maximum result size to be returned. If this numeric value is missing, the result size will be 1.
Pageable is an interface for pagination information, but in practice we use the PageRequest class that implements it. This one can specify the page number, the page size, and the sorting criterion.
1public interface UserRepository extends JpaRepository<User, Long> {
2
3 User findFirstByOrderByUsernameAsc();
4
5 User findTopByOrderByRegistrationDateDesc();
6
7 Page<User> findAll(Pageable pageable);
8
9 List<User> findFirst2ByLevel(int level, Sort sort);
10
11 List<User> findByLevel(int level, Sort sort);
12
13 List<User> findByActive(boolean active, Pageable pageable);
14
15}
16
1public class FindUsersSortingAndPagingTest extends
2SpringDataJpaApplicationTests {
3
4 @Test
5 void testOrder() {
6 User user1 = userRepository.findFirstByOrderByUsernameAsc();
7 User user2 = userRepository.findTopByOrderByRegistrationDateDesc();
8 Page<User> userPage = userRepository.findAll(PageRequest.of(1, 3));
9 List<User> users = userRepository.findFirst2ByLevel(2,
10 Sort.by("registrationDate"));
11
12 assertAll(
13 () -> assertEquals("beth", user1.getUsername()),
14 () -> assertEquals("julius", user2.getUsername()),
15 () -> assertEquals(2, users.size()),
16 () -> assertEquals(3, userPage.getSize()),
17 () -> assertEquals("beth", users.get(0).getUsername()),
18 () -> assertEquals("marion", users.get(1).getUsername())
19 );
20 }
21
22 @Test
23 void testFindByLevel() {
24 Sort.TypedSort<User> user = Sort.sort(User.class);
25
26 List<User> users = userRepository.findByLevel(3,
27 user.by(User::getRegistrationDate).descending());
28
29 assertAll(
30 () -> assertEquals(2, users.size()),
31 () -> assertEquals("james", users.get(0).getUsername())
32 );
33 }
34
35 @Test
36 void testFindByActive() {
37 List<User> users = userRepository.findByActive(true,
38 PageRequest.of(1, 4, Sort.by("registrationDate")));
39
40 assertAll(
41 () -> assertEquals(4, users.size()),
42 () -> assertEquals("burk", users.get(0).getUsername())
43 );
44 }
45}
Query methods returning more than one result can use standard Java interfaces such as Iterable, List, Set. Additionally, Spring Data supports Streamable, which can be used as an alternative to Iterable or any collection type. You can concatenate Streamables and directly filter and map over the elements.
1public interface UserRepository extends JpaRepository<User, Long> {
2
3 Streamable<User> findByEmailContaining(String text);
4
5 Streamable<User> findByLevel(int level);
6
7}
1@Test
2void testStreamable() {
3 try(
4 // searching for emails containing “someother.”
5 Stream<User> result = userRepository.findByEmailContaining("someother")
6 // concatenate the resulting Streamable with the Streamable providing the users of level 2
7 .and(userRepository.findByLevel(2))
8 .stream().distinct()
9 ) {
10 assertEquals(6, result.count());
11 }
12 /**
13 It will transform this into a stream and will keep the distinct users. The stream is
14 given as a resource of the try block, so it will automatically be closed. An alternative
15 is to explicitly call the close() method. Otherwise, the stream would keep the underlying
16 connection to the database.
17 */
18}
With the @Query annotation, you can create a method and then write a custom query on it. When you use the @Query annotation, the method name does not need to follow any naming convention. The custom query can be parameterized, identifying the parameters by position or by name, and binding these names in the query with the @Param annotation. The @Query annotation can generate native queries with the nativeQuery flag set to true. You should be aware, however, that native queries can affect the portability of the application. To sort the results, you can use a Sort object. The properties you order by must resolve to a query property or a query alias.
Spring Data JPA supports Spring Expression Language (SpEL) expressions in queries defined using the @Query annotation, and Spring Data JPA supports the entityName variable. In a query such as select e from #{#entityName} e, entityName is resolved based on the @Entity annotation.
1@Query("select count(u) from User u where u.active = ?1")
2int findNumberOfUsersByActivity(boolean active);
3
4@Query("select u from User u where u.level = :level and u.active = :active")
5List<User> findByLevelAndActive(@Param("level") int level, @Param("active") boolean active);
6
7@Query(value = "SELECT COUNT(*) FROM USERS WHERE ACTIVE = ?1", nativeQuery = true)
8int findNumberOfUsersByActivityNative(boolean active);
9
10@Query("select u.username, LENGTH(u.email) as email_length from #{#entityName} u where u.username like %?1%")
11List<Object[]> findByAsArrayAndSort(String text, Sort sort);
12
1public class QueryResultsTest extends SpringDataJpaApplicationTests {
2 @Test
3 void testFindByAsArrayAndSort() {
4 List<Object[]> usersList1 =
5 userRepository.findByAsArrayAndSort("ar", Sort.by("username"));
6
7 List<Object[]> usersList2 = userRepository.findByAsArrayAndSort("ar", Sort.by("email_length").descending());
8
9 List<Object[]> usersList3 = userRepository.findByAsArrayAndSort("ar", JpaSort.unsafe("LENGTH(u.email)"));
10
11 assertAll(
12 () -> assertEquals(2, usersList1.size()),
13 () -> assertEquals("darren", usersList1.get(0)[0]),
14 () -> assertEquals(21, usersList1.get(0)[1]),
15 () -> assertEquals(2, usersList2.size()),
16 () -> assertEquals("marion", usersList2.get(0)[0]),
17 () -> assertEquals(26, usersList2.get(0)[1]),
18 () -> assertEquals(2, usersList3.size()),
19 () -> assertEquals("darren", usersList3.get(0)[0]),
20 () -> assertEquals(21, usersList3.get(0)[1])
21 );
22 }
23}
24
25
Not all attributes of an entity are always needed, so we may sometimes access only some of them. Consequently, instead of returning instances of the root entity managed by the repository, you may want to create projections based on certain attributes of those entities. Spring Data JPA can shape return types to selectively return attributes of entities.
An interface-based projection requires the creation of an interface that declares getter methods for the properties to be included in the projection. Such an interface can also compute specific values using the @Value annotation and SpEL expressions. By executing queries at runtime, the execution engine creates proxy instances of the interface for each returned element and forwards the calls to the exposed methods to the target object.
1public class Projection {
2 public interface UserSummary {
3 String getUsername();
4
5 @Value("#{target.username} #{target.email}")
6 String getInfo();
7 }
8}
If we include only methods such as getUsername() , we’ll create a closed projection — this is an interface whose getters all correspond to properties of the target entity. When you’re working with a closed projection, the query execution can be optimized by Spring Data JPA because all the properties needed by the projection proxy are known from the beginning.
If we include methods such as getInfo(), we create an closed projection, which is more flexible. However, Spring Data JPA will not be able to optimize the query execution, because the SpEL expression is evaluated at runtime and may include any properties or combination of properties of the entity root.
A class-based projection requires the creation of a data transfer object (DTO) class that declares the properties to be included in the projection and the getter methods. Using a class-based projection is similar to using interface-based projections. However, Spring Data JPA doesn’t need to create proxy classes for managing projections. Spring Data JPA will instantiate the class that declares the projection, and the properties to be included are determined by the parameter names of the constructor of the class.
1public class Projection {
2 // . . .
3 public static class UsernameOnly {
4 private String username;
5
6 public UsernameOnly(String username) {
7 this.username = username;
8 }
9
10 public String getUsername() {
11 return username;
12 }
13 }
14}
1List<Projection.UserSummary> findByRegistrationDateAfter(LocalDate date);
2List<Projection.UsernameOnly> findByEmail(String username);
1public class ProjectionTest extends SpringDataJpaApplicationTests {
2 @Test
3 void testProjectionUsername() {
4 List<Projection.UsernameOnly> users = userRepository.findByEmail("john@somedomain.com");
5 }
6
7 assertAll(
8 () -> assertEquals(1, users.size()),
9 () -> assertEquals("john", users.get(0).getUsername())
10 );
11
12 @Test
13 void testProjectionUserSummary() {
14 List<Projection.UserSummary> users = userRepository.findByRegistrationDateAfter(
15 LocalDate.of(2021, Month.FEBRUARY, 1)
16 );
17
18 assertAll(
19 () -> assertEquals(1, users.size()),
20 () -> assertEquals("julius", users.get(0).getUsername()),
21 () -> assertEquals("julius julius@someotherdomain.com", users.get(0).getInfo())
22 );
23 }
24
25 @Test
26 void testDynamicProjection() {
27 List<Projection.UsernameOnly> usernames =
28 userRepository.findByEmail("mike@somedomain.com", Projection.UsernameOnly.class);
29
30 List<User> users = userRepository.findByEmail("mike@somedomain.com", User.class);
31
32 assertAll(
33 () -> assertEquals(1, usernames.size()),
34 () -> assertEquals("mike", usernames.get(0).getUsername()),
35 () -> assertEquals(1, users.size()),
36 () -> assertEquals("mike", users.get(0).getUsername())
37 );
38 }
39}
You can define modifying methods with the @Modifying annotation. For example, INSERT, UPDATE, and DELETE queries, or DDL statements, modify the content of the database. The @Query annotation will have the modifying query as an argument, and it may need binding parameters. Such a method must also be annotated with @Transactional or be run from a programmatically managed transaction. Modifying queries have the advantage of clearly emphasizing which column they address, and they may include conditions, so they can make the code clearer, compared to persisting or deleting the whole object. Also, changing a limited number of columns in the database will execute more quickly.
1public interface UserRepository extends JpaRepository<User, Long> {
2 @Modifying
3 @Transactional
4 @Query("update User u set u.level = ?2 where u.level = ?1")
5 int updateLevel(int oldLevel, int newLevel);
6
7 @Transactional
8 //@Transactional. @Modifying isn’t necessary in this case, since the query is generated by the framework.
9 int deleteByLevel(int level);
10
11 @Transactional
12 @Modifying
13 @Query("delete from User u where u.level = ?1")
14 int deleteBulkByLevel(int level);
15}
16
What is the difference between the deleteByLevel and deleteBulkByLevel methods? The first one runs a query, and it will then remove the returned instances one by one. If there are callback methods that control the lifecycle of each instance (for example, a method to be run when a user is removed), they will be executed. The second method will remove the users in bulk, executing a single JPQL query. No User instance (not even the ones that are already loaded in memory) will execute lifecycle callback methods.
1@Test
2void testModifyLevel() {
3 int updated = userRepository.updateLevel(5, 4);
4 List<User> users = userRepository.findByLevel(4, Sort.by("username"));
5
6 assertAll(
7 () -> assertEquals(1, updated),
8 () -> assertEquals(3, users.size()),
9 () -> assertEquals("katie", users.get(1).getUsername())
10 );
11}
12
13@Test
14void testDeleteByLevel() {
15 int deleted = userRepository.deleteByLevel(2);
16 List<User> users = userRepository.findByLevel(2, Sort.by("username"));
17 assertEquals(0, users.size());
18}
19
20@Test
21void testDeleteBulkByLevel() {
22 int deleted = userRepository.deleteBulkByLevel(2);
23 List<User> users = userRepository.findByLevel(2, Sort.by("username"));
24 assertEquals(0, users.size());
25}
Query by Example (QBE) is a querying technique that does not require you to write classical queries to include entities and properties. It allows dynamic query creation and consists of three pieces: a probe, an ExampleMatcher, and an Example.
The probe is a domain object with already-set properties. The ExampleMatcher provides the rules for matching particular properties. An Example puts the probe and the ExampleMatcher together and generates the query. Multiple Examples may reuse a single ExampleMatcher.
These are the most appropriate use cases for QBE:
QBE has a couple of limitations:
We won’t add any more methods to the UserRepository interface. We’ll only write tests to build the probe, the ExampleMatchers, and the Examples.
1public class QueryByExampleTest extends SpringDataJpaApplicationTests {
2 @Test
3 void testEmailWithQueryByExample() {
4 User user = new User();
5 //Initialize a User instance and set up an email for it. This will represent the probe.
6 user.setEmail("@someotherdomain.com");
7
8 /**
9 * Create the ExampleMatcher with the help of the builder pattern. Any null reference
10 * property will be ignored by the matcher. However, we need to explicitly
11 * ignore the level and active properties, which are primitives. If they were not
12 * ignored, they would be included in the matcher with their default values (0 for
13 * level and false for active) and would change the generated query. We’ll configure
14 * the matcher condition so that the email property will end with a given string.
15 */
16 ExampleMatcher matcher = ExampleMatcher.matching()
17 .withIgnorePaths("level", "active")
18 .withMatcher("email", match -> match.endsWith());
19
20 /**
21 * Create an Example that puts the probe and ExampleMatcher together and generates the query.
22 * The query will search for users that have an email property ending with the string defining
23 * the email of the probe.
24 *
25 Example<User> example = Example.of(user, matcher);
26
27 // Execute the query to find all users matching the probe.
28 List<User> users = userRepository.findAll(example);
29
30 assertEquals(4, users.size());
31 }
32
33 @Test
34 void testUsernameWithQueryByExample() {
35 User user = new User();
36 user.setUsername("J");
37
38 /**
39 * Create the ExampleMatcher with the help of the builder pattern. Any null reference
40 * property will be ignored by the matcher. Again, we need to explicitly ignore
41 * the level and active properties, which are primitives. We configure the matcher
42 * condition so that the match will be made on starting strings for the configured
43 * properties (the username property from the probe, in our case).
44 */
45 ExampleMatcher matcher = ExampleMatcher.matching()
46 .withIgnorePaths("level", "active")
47 .withStringMatcher(ExampleMatcher.StringMatcher.STARTING)
48 .withIgnoreCase();
49
50 /**
51 * Create an Example that puts the probe and the ExampleMatcher together and generates
52 * the query. The query will search for users having a username property that
53 * starts with the string defining the username of the probe.
54 */
55 Example<User> example = Example.of(user, matcher);
56
57 List<User> users = userRepository.findAll(example);
58 assertEquals(3, users.size());
59 }
60}
To emphasize the importance of ignoring the default primitive properties, we’ll compare the generated queries with and without the calls to the withIgnorePaths("level", "active") methods. For the first test, this is the query generated with the call to the withIgnorePaths("level", "active") method:
select user0_.id as id1_0_, user0_.active as active2_0_, user0_.email as email3_0_, user0_.level as level4_0_, user0_.registration_date as registra5_0_, user0_.username as username6_0_ from users user0_ where user0_.email like ? escape ?
This is the query generated without the call to the withIgnorePaths("level", "active") method:
select user0_.id as id1_0_, user0_.active as active2_0_, user0_.email as email3_0_, user0_.level as level4_0_, user0_.registration_date as registra5_0_, user0_.username as username6_0_ from users user0_ where user0_.active=? and (user0_.email like ? escape ?) and user0_.level=0
For the second test, this is the query generated with the call to the withIgnore- Paths("level", "active") method:
select user0_.id as id1_0_, user0_.active as active2_0_, user0_.email as email3_0_, user0_.level as level4_0_, user0_.registration_date as registra5_0_, user0_.username as username6_0_ from users user0_ where lower(user0_.username) like ? escape ?
This is the query generated without the call to the withIgnorePaths("level", "active") method:
select user0_.id as id1_0_, user0_.active as active2_0_, user0_.email as email3_0_, user0_.level as level4_0_, user0_.registration_date as registra5_0_, user0_.username as username6_0_ from users user0_ where user0_.active=? and user0_.level=0 and (lower(user0_.username) like ? escape ?)
Note the conditions added on primitive properties when the withIgnore- Paths("level", "active") method was removed:
user0_.active=? and user0_.level=0
This will change the query result.