Most developers create their own repositories by adding their queries to one of Spring Data JPA’s standard base repositories, like the JpaRepository or the CrudRepository. These repositories give you a set of standard operations, e.g., to read and write entity objects. It often seems like the obvious choice to define your own repositories based on one of these standard interfaces. But they are not adjusted to your project. They usually contain several methods you don’t want to use. Some of the provided methods can even cause performance problems.
You should, therefore, create your own base repository definition that you can then customize for each entity object. And don’t worry; this doesn’t mean you will not benefit from Spring Data JPA’s standard features or that you will implement all persistence operations yourself. Spring Data JPA provides an easy way to define your own standard repository and use Spring’s implementation.
There are 2 general groups of methods you should not include in your repository definition:
You might have various reasons why you don’t want to use specific methods in your project. Maybe you don’t want to use features like query by example or specification because you want to limit the number of different approaches used to define a query. Or maybe you consider the code that uses a specific feature hard to read.
All of these are valid reasons. Just make sure that you understand the feature and its performance implications before you decide to exclude it. I sometimes see teams exclude features like custom or native queries. But these are essential to creating an efficient persistence layer. If you find a method or feature you don’t know, I recommend watching my Spring Data JPA Certification course included in the Persistence Hub.
Unfortunately, Spring Data JPA also offers a few methods that can cause severe performance problems. The most obvious ones are the count(), deleteAll(), deleteAllInBatch(), and findAll() methods. These methods count, delete or fetch all records in the database table mapped by an entity class.
Other methods you should avoid in most situations are flush() and saveAndFlush(T entity). Both methods force your persistence provider, e.g., Hibernate, to ignore all internal optimizations, immediately flush the current persistence context, and write all pending changes to the database. There are only very few situations where this is necessary or helpful, like the execution of a bulk operation. In all other cases, it will only slow down your application. Due to that, you should only provide these methods if you need them, and they shouldn’t be part of your standard repository.
The deleteAllInBatch(Iterable<T> entities) method defined by the JpaRepository is another method you might want to exclude from your standard repository definition if you don’t need to delete a huge number of entities. This method handles those situations very efficiently, but your 1st level cache and database might get out of sync. If not everyone on your team knows how to handle or avoid that, this method should better not be available.
It only takes a few minutes to create your own base repository that you can then extend to define your entity-specific repositories. You only need to create an interface that extends Spring Data’s Repository interface and annotate it with @NoRepositoryBean. This annotation tells Spring Data JPA that this is a base repository definition for which it shall not instantiate a repository.
After that, you can copy method definitions from all repositories defined by Spring Data JPA. Spring will then automatically provide the required implementations.
@NoRepositoryBean
public interface CustomJpaRepository<T> extends Repository<T, Long> {
<S extends T> S save(S entity);
<S extends T> List<S> saveAll(Iterable<S> entities);
Optional<T> findById(Long id);
Iterable<T> findAllById(Iterable<Long> ids);
void deleteById(Long id);
void delete(T entity);
void deleteAllById(Iterable<? extends Long> ids);
void deleteAll(Iterable<? extends T> entities);
}
The code snippet shows the definition of a custom base repository that only provides a very basic set of methods. I copied these methods from the JpaRepository and CrudRepository interfaces.
I also set the type of the primary key attribute to Long. This isn’t necessary, but it’s a common approach that streamlines the definition of your repositories if you use a database sequence to generate the primary key value of all of your entities.
That’s all you need to do to define your own base repository. As mentioned earlier, Spring Data JPA will provide the implementations of all methods you copied from Spring Data’s repositories.
In the next step, you can use your new standard repository to define entity-specific repositories and add your queries to them. Here you can see an example of such a repository.
public interface ChessPlayerRepository extends CustomJpaRepository<ChessPlayer> {
List<ChessPlayer> findByFirstNameAndLastName(String firstName, String lastName);
}
The ChessPlayerRepository extends the CustomJpaRepository with all its methods and adds the findByFirstNameAndLastName method. That method uses Spring Data JPA’s derived query feature. At runtime, Spring Data generates and executes a query based on the method name.
As you can see, you can use the CustomJpaRepository in the same way as Spring Data JPA’s standard repositories, e.g., the JpaRepository or CrudRepository. But in contrast to using one of those standard repositories, you are now in full control of provided repository methods. That enables you to exclude methods you don’t want to use, reduces the risk of bugs, and makes it easier to follow your project-specific guidelines.
As I showed you in the previous section, you don’t need to provide any method implementations when you copy the method definitions from Spring Data JPA’s standard repositories. But you might want to add methods not defined by Spring Data. In those cases, you can use fragment repositories. They enable you to combine your own method implementations with Spring Data JPA’s standard methods.
Unfortunately, you can’t include fragment repositories in the base repository definition (see #2142). Due to that, you need to define your custom methods in one or more independent fragment repository definitions and extend them together with your base repository.
Let’s take a look at an example that gives you a better version of Spring Data JPA’s save method. As you might know, I’m not a huge fan of that method. The name sounds like you need to call that method to save any change in the database. But that’s not the case. Due to JPA’s lifecycle management, you only need to call the save method to persist a new entity or to merge a detached entity. All the changes you perform on managed entity objects get automatically stored in the database.
So, let’s create a BetterJpaRepository that doesn’t include the save and saveAll methods
@NoRepositoryBean
public interface BetterJpaRepository<T> extends Repository<T, Long> {
Optional<T> findById(Long id);
Iterable<T> findAllById(Iterable<Long> ids);
void deleteById(Long id);
void delete(T entity);
void deleteAllById(Iterable<? extends Long> ids);
void deleteAll(Iterable<? extends T> entities);
}
In the next step, you need to define and implement a fragment repository. As I explained in my guide to composite repositories and fragment interfaces, the fragment interface only defines the methods you want to add to a base repository. So, in this example, these are the persist, persistAll, merge, and mergeAll methods.
public interface PersistAndMergeRepository<T> {
<S extends T> S persist(S entity);
<S extends T> List<S> persistAll(Iterable<S> entities);
<S extends T> S merge(S entity);
<S extends T> List<S> mergeAll(Iterable<S> entities);
}
The implementation of these is straightforward. You only need to inject an EntityManager and call the persist and merge methods. Spring Data JPA will automatically find and use this implementation for all repositories that extend the PersistAndMergeRepository fragment interface.
public class PersistAndMergeRepositoryImpl<T> implements PersistAndMergeRepository<T> {
@PersistenceContext
private EntityManager em;
@Override
public <S extends T> S persist(S entity) {
em.persist(entity);
return entity;
}
@Override
public <S extends T> List<S> persistAll(Iterable<S> entities) {
List<S> result = new ArrayList<>();
entities.forEach(e -> result.add(persist(e)));
return result;
}
@Override
public <S extends T> S merge(S entity) {
return em.merge(entity);
}
@Override
public <S extends T> List<S> mergeAll(Iterable<S> entities) {
List<S> result = new ArrayList<>();
entities.forEach(e -> result.add(merge(e)));
return result;
}
}
After you have defined the BetterJpaRepository and the PersistAndMergeRepository, you can extend them to define your custom repository. In this example, I define a BetterChessPlayerRepository, which includes the methods defined by the 2 extended interfaces and a derived query.
public interface BetterChessPlayerRepository extends BetterJpaRepository<ChessPlayer>, PersistAndMergeRepository<ChessPlayer> {
List<ChessPlayer> findByFirstNameAndLastName(String firstName, String lastName);
}
When you use this repository in your business code, Spring Data JPA:
Spring Data JPA provides different standard repositories which provide a huge set of methods that you can use to implement your persistence layer. But a few of these methods can cause problems when used on a production database, and others might not fit your coding guidelines.
In those cases, you can define a repository that you can then use as the base of your entity-specific repository definitions. You don’t need to provide any method implementation if you copy all methods used in that repository from Spring Data JPA’s standard repositories. Spring Data will handle that for you.
If you want to provide custom implementations for any of these methods or add methods that are unknown to Spring Data JPA, you need to use a fragment repository. When doing that, please remember that Spring Data JPA doesn’t support fragment interfaces on super interfaces. So, you can’t reference them in your base repository definition.