Using Generics in Spring Data JPA Repositories

Using generics in Spring Data JPA repositories

First of all, I know we're raising the bar here quite a bit but this is already tremendously less code than you had to write without the help of Spring Data JPA.

Second, I think you don't need the service class in the first place, if all you do is forward a call to the repository. We recommend using services in front of the repositories if you have business logic that needs orchestration of different repositories within a transaction or has other business logic to encapsulate.

Generally speaking, you can of course do something like this:

interface ProductRepository<T extends Product> extends CrudRepository<T, Long> {

@Query("select p from #{#entityName} p where ?1 member of p.categories")
Iterable<T> findByCategory(String category);

Iterable<T> findByName(String name);
}

This will allow you to use the repository on the client side like this:

class MyClient {

@Autowired
public MyClient(ProductRepository<Car> carRepository,
ProductRepository<Wine> wineRepository) { … }
}

and it will work as expected. However there are a few things to notice:

This only works if the domain classes use single table inheritance. The only information about the domain class we can get at bootstrap time is that it will be Product objects. So for methods like findAll() and even findByName(…) the relevant queries will start with select p from Product p where…. This is due to the fact that the reflection lookup will never ever be able to produce Wine or Car unless you create a dedicated repository interface for it to capture the concrete type information.

Generally speaking, we recommend creating repository interfaces per aggregate root. This means you don't have a repo for every domain class per se. Even more important, a 1:1 abstraction of a service over a repository is completely missing the point as well. If you build services, you don't build one for every repository (a monkey could do that, and we're no monkeys, are we? ;). A service is exposing a higher level API, is much more use-case drive and usually orchestrates calls to multiple repositories.

Also, if you build services on top of repositories, you usually want to enforce the clients to use the service instead of the repository (a classical example here is that a service for user management also triggers password generation and encryption, so that by no means it would be a good idea to let developers use the repository directly as they'd effectively work around the encryption). So you usually want to be selective about who can persist which domain objects to not create dependencies all over the place.

Summary

Yes, you can build generic repositories and use them with multiple domain types but there are quite strict technical limitations. Still, from an architectural point of view, the scenario you describe above shouldn't even pop up as this means you're facing a design smell anyway.

Spring JPA Data Repository: How to create a generic repository?

I see a lot of issues with what you're trying to do here. Remember, when Spring initializes it's going to look for a bean of the type you've declared to inject. To do this, it needs to be a resolvable type, see https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/ResolvableType.html.

The spring data jpa repositories are already generic, you can extend this

public interface MyRepository<T> extends CrudRepository<T, Long>{}

public interface TheirRepository<T> extends MyRepository<T>{}

public interface AnotherRepository<T> extends TheirRepository<T>{}

as far as you want, but ultimately if you want to use it you'll have to declare one that conforms to your entity. So, no matter how you dice this, you're going to end up with having to declare something like

public interface MyGenericRepository<MyActualEntity>{}

To get away from this, I encourage you to go ahead and create all your repositories as you normally would, then make a service class that generically saves, updates, deletes the entities by looking up the repository for the type that your operating on. Something like this:

import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.support.Repositories;
import org.springframework.stereotype.Service;
import org.springframework.web.context.WebApplicationContext;

@Service
public class GenericPersistenceService {

private final WebApplicationContext applicationContext;
private Repositories repositories;

public GenericPersistenceService(WebApplicationContext applicationContext) {
repositories = new Repositories(applicationContext);
this.applicationContext = applicationContext;
}

public <T> T save(T object) {
Object repository = repositories.getRepositoryFor(object.getClass()).orElseThrow(
() -> new IllegalStateException(
"Can't find repository for entity of type " + object.getClass()));
CrudRepository<T, Long> crudRepository = (CrudRepository<T, Long>) repository;

return crudRepository.save(object);
}
}

The repositories are populated when the service is created, then when you pass an entity to the save method, it looks up the repository for the given entity and performs the save operation.

How do we create a generic repository in Spring Data JPA?

I am afraid not. If you already tried you would have noticed that your second repository extending CrudRepository<T, Long> fails with something like:

Invocation of init method failed; nested exception is java.lang.IllegalArgumentException: Not a managed type: class java.lang.Object.

because with generic T java can tell only that it is an Object which is not an entity. And because of that you need to add annotation @NoRepositoryBean which then only allows you to extend it with a real entity type instead of generic T.

It might be possible programmatically but most probably would be a lot more painful than just to create repo interfaces.

If you have entity inheritance tree like A->B->C you are able to create a repo for A that then handles also B & C what comes to properties inherited from A. So you could issue findAll() and it would return you all As, Bs & Cs but as a list of As and you would have to check the actual type of each item separately.

Usually when there is no need to any special treatment for entities and you do not want to write those repository "stubs" you can just use EntityManager directly. You can implement your own - generic repository like - @Service class that has @Autowired entity manager and invoke its find(..), persist(..) and other methods.



Related Topics



Leave a reply



Submit