49

I have a number of simple object types that need to be persisted to a database. I am using Spring JPA to manage this persistence. For each object type I need to build the following:

import org.springframework.data.jpa.repository.JpaRepository;

public interface FacilityRepository extends JpaRepository<Facility, Long> {
}


public interface FacilityService {
    public Facility create(Facility facility);
}

@Service
public class FacilityServiceImpl implements FacilityService {

    @Resource
    private FacilityRepository countryRepository;

    @Transactional
    public Facility create(Facility facility) {
        Facility created = facility;
        return facilityRepository.save(created);
    }
}

It occurred to me that it may be possible to replace the multiple classes for each object type with three generics based classes, thus saving a lot of boilerplate coding. I am not exactly sure how to go about it and in fact if it is a good idea?

Oliver Drotbohm
  • 80,157
  • 18
  • 225
  • 211
skyman
  • 2,255
  • 4
  • 32
  • 55
  • 1
    https://thoughts-on-java.org/spring-data-jpa-query-annotation/ read the "Avoid Entity Name References" part in this article, It will useful. – Dusman Sep 24 '19 at 11:59

4 Answers4

104

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.

Bhesh Gurung
  • 50,430
  • 22
  • 93
  • 142
Oliver Drotbohm
  • 80,157
  • 18
  • 225
  • 211
  • 9
    This is really useful information. It would be great to include this kind of higher-level design guidance in the actual reference materials if it's not already there. Like "best practices" callouts or something. –  Oct 18 '13 at 06:43
  • 2
    Well, this is software architecture and design 1:1. If we included such stuff in the reference documentation, where should we stop then? ;) – Oliver Drotbohm Oct 18 '13 at 06:48
  • 3
    I'm facing this very scenario in the design of an application now... providing a 'dto' and a 'repo' for each model... It doesn't smell right. – Edward J Beckett Jan 12 '14 at 14:50
  • @OliverGierke When I am trying to expose my SD Neo4j repository using Spring Data REST where my neo4j repository is exactly the same as above, its not generating `_embedded` field and throwing 500 error... else repository with proper Domain class is working fine.. any workout there ? – Aman Gupta May 06 '14 at 11:59
  • That's a new question, not a comment, isn't it? – Oliver Drotbohm May 07 '14 at 11:57
  • I'm doing the monkey 1:1 abstraction of services over repositories as my services take Long id parameters and repositories take the corresponding objects in the JPQL queries. The controllers expecting ids and repositories expecting objects is sensible ? – Stephane Oct 17 '14 at 10:42
34

This is very possible! I am probably very late to the party. But this will certainly help someone in the future. Here is a complete solution that works like a charm!

Create BaseEntity class for your entities as follows:

@MappedSuperclass
public class AbstractBaseEntity implements Serializable{

    @Id @GeneratedValue
    private Long id;
    @Version
    private int version;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;

    public AbstractBaseEntity() {
        this.createdAt = LocalDateTime.now();
        this.updatedAt = LocalDateTime.now();
    }

    // getters and setters      
}

Create a generic JPA Repository interface for your DAO persistence as follows: NB. Remember to put the @NoRepositoryBean so that JPA will not try to find an implementation for the repository!

@NoRepositoryBean
public interface AbstractBaseRepository<T extends AbstractBaseEntity, ID extends Serializable>
extends JpaRepository<T, ID>{
    
}

Create a Base Service class that uses the above base JPA repository. This is the one that other service interfaces in your domain will simply extend as follows:

public interface AbstractBaseService<T extends AbstractBaseEntity, ID extends Serializable>{
    public abstract T save(T entity);
    public abstract List<T> findAll(); // you might want a generic Collection if u prefer

    public abstract Optional<T> findById(ID entityId);
    public abstract T update(T entity);
    public abstract T updateById(T entity, ID entityId);   
    public abstract void delete(T entity);
    public abstract void deleteById(ID entityId);
    // other methods u might need to be generic
    
}

Then create an abstract implementation for the base JPA repository & the basic CRUD methods will also be provided their implementations as in the following:

@Service
@Transactional
public abstract class AbstractBaseRepositoryImpl<T extends AbstractBaseEntity, ID extends Serializable>
        implements AbstractBaseService<T, ID>{
    
    private AbstractBaseRepository<T, ID> abstractBaseRepository;
    
    @Autowired
    public AbstractBaseRepositoryImpl(AbstractBaseRepository<T, ID> abstractBaseRepository) {
        this.abstractBaseRepository = abstractBaseRepository;
    }
    
    @Override
    public T save(T entity) {
        return (T) abstractBaseRepository.save(entity);
    }

    @Override
    public List<T> findAll() {
        return abstractBaseRepository.findAll();
    }

    @Override
    public Optional<T> findById(ID entityId) {
        return abstractBaseRepository.findById(entityId);
    }

    @Override
    public T update(T entity) {
        return (T) abstractBaseRepository.save(entity);
    }

    @Override
    public T updateById(T entity, ID entityId) {
        Optional<T> optional = abstractBaseRepository.findById(entityId);
        if(optional.isPresent()){
            return (T) abstractBaseRepository.save(entity);
        }else{
            return null;
        }
    }

    @Override
    public void delete(T entity) {
        abstractBaseRepository.delete(entity);
    }

    @Override
    public void deleteById(ID entityId) {
        abstractBaseRepository.deleteById(entityId);
    }

}

How to use the above abstract entity, service, repository, and implementation:

Example here will be a MyDomain entity. Create a domain entity that extends the AbstractBaseEntity as follows: NB. ID, createdAt, updatedAt, version, etc will be automatically be included in the MyDomain entity from the AbstractBaseEntity

@Entity
public class MyDomain extends AbstractBaseEntity{

    private String attribute1;
    private String attribute2;
    // getters and setters
}

Then create a repository for the MyDomain entity that extends the AbstractBaseRepository as follows:

@Repository
public interface MyDomainRepository extends AbstractBaseRepository<MyDomain, Long>{

}

Also, Create a service interface for the MyDomain entity as follows:

public interface MyDomainService extends AbstractBaseService<MyDomain, Long>{

}

Then provide an implementation for the MyDomain entity that extends the AbstractBaseRepositoryImpl implementation as follows:

@Service
@Transactional
public class MyDomainServiceImpl extends AbstractBaseRepositoryImpl<MyDomain, Long> 
        implements MyDomainService{
    private MyDomainRepository myDomainRepository;

    public MyDomainServiceImpl(MyDomainRepository myDomainRepository) {
        super(myDomainRepository);
    }
    // other specialized methods from the MyDomainService interface

}
Now use your `MyDomainService` service in your controller as follows: 

@RestController // or @Controller
@CrossOrigin
@RequestMapping(value = "/")
public class MyDomainController {
    
    private final MyDomainService myDomainService;

    @Autowired
    public MyDomainController(MyDomainService myDomainService) {
        this.myDomainService = myDomainService;
    }
   
    @GetMapping
    public List<MyDomain> getMyDomains(){
        return myDomainService.findAll();
    }   
    // other controller methods

}

NB. Make sure that the AbstractBaseRepository is annotated with @NoRepositoryBean so that JPA does not try to find an implementation for the bean. Also the AbstractBaseServiceImpl must be marked abstract, otherwise JPA will try to autowire all the children daos of the AbstractBaseRepository in the constructor of the class leading to a NoUniqueBeanDefinitionException since more than 1 daos (repository) will be injected when the bean is created! Now your service, repository, and implementations are more reusable. We all hate boilerplate!

Hope this helps someone.

Leviand
  • 2,745
  • 4
  • 29
  • 43
Jose Mhlanga
  • 805
  • 12
  • 14
  • Thank you for going to so much trouble in your answer. Ironically, I am about to start a project to rebuild the application that this question arose from - thank you! – skyman Apr 28 '20 at 00:03
  • Happy to help. Faced with the same boilerplate in prev projects. All the best in yr projects the Spring way. We all like Clean code! Cheers – Jose Mhlanga Apr 28 '20 at 10:42
  • @JoseMhlanga I've just found this post and it seems interesting, but I wonder why you created AbstractBaseRepository and don't directly make MyDomainRepository extends JpaRepository ? Is it only in case where you move from Jpa to another library ? (But in this case you may need implementation since it may not be generated by Spring) – Julien O Nov 18 '20 at 03:31
  • The reasons I defined the `AbstractBaseRepository` is so that all our repositories wont have to repeat the extending `JpaRepository` all the time in every repository we create, especially for general crud operations, we just want to encourage reusability, but u are free to do it another way – Jose Mhlanga Nov 18 '20 at 06:29
  • Thank you very much. I have two questions: first why do we need 'AbstractBaseService'? and second is '@Autowired' at 'AbstractBaseRepositoryImpl' constructor is useless? It won't get autowired because it is abstract and is created when 'MyDomainServiceImpl' gets created, right? – anmi Mar 22 '21 at 21:04
  • Okay, in proper software development, we need to expose data through interfaces to clients (including other devs), so the `AbstractBaseService` exposes only necessary generic methods for each `entities`, rather than expose the `repository` itself, and then `AbstractBaseRepositoryImpl`, its a marker service for the generics that can be used with it along. Cheers – Jose Mhlanga Mar 24 '21 at 08:14
  • On a side note, use of `Serializable` is frowned upon in production code due to security vulnerabilities during deserialisation. I'm not sure why you would need it anyways though since spring maps directly to DTOs and back already ? – Lorenzo Jun 17 '21 at 00:49
  • Hello Good solution thanks for it. But my question how to make it more than for 2 and more daos (repositories)? – Jahongir Sabirov Nov 22 '21 at 10:20
  • I think you meant by AbstractBaseRepositoryImpl = AbstractBaseServiceImpl, the name Repository in the base service confused me, is this the case??, and do we really need to mark the AbstractService with @Service annotation? – Hamza Assada Oct 22 '22 at 16:25
3

I found an one more way to do this using one less class, inspired by @Jose Mhlanga's answer

Create a BaseEntity

@Getter
@Setter
@MappedSuperclass
public class BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id;
}

Create a BaseRepository

@NoRepositoryBean
public interface BaseRepository<T extends BaseEntity> extends JpaRepository<T, Long> { }

Finally create a BaseService to complete the setup

public interface BaseService<T extends BaseEntity> {

    BaseRepository<T> getRepository();

    default T create(T t) {
        return getRepository().save(t);
    }
    
    default Optional<T> update(T t) {
        if (getRepository().existsById(t.getId())) {
            return Optional.of(getRepository().save(t));
        }
        return Optional.empty();
    }
    
    default Optional<T> get(Long id) {
        return getRepository().findById(id);
    }
    
    default List<T> getAll() {
        return getRepository().findAll();
    }
    
    default void delete(Long id) {
        getRepository().deleteById(id);
    }
    
}

Now we can start creating our entities. Let's say we have an entity called Category. We will create a model, repository, service and controller.

The entity and repository will look something like:

@Getter
@Setter
@Entity
public class Category extends BaseEntity { String name; }

public interface CategoryRepository extends BaseRepository<Category> { }

For service, we need to override only one method: getRepository()

@ApplicationScoped
public class CategoryService implements BaseService<Category> {

    @Inject
    CategoryRepository categoryRepository;

    @Override
    public BaseRepository<Category> getRepository() {
        return categoryRepository;
    }

}

Finally, we create the controller. I haven't quite reached where I could create an abstraction for controller as well. Will edit this if I do.

@Path("categories")
@ApplicationScoped
public class CategoryController {

    // You can (and should!) use BaseService here.
    // Omitting that part as that would need qualifier.
    @Inject
    CategoryService categoryService;

    @POST
    public Response create(Category category) {
        return Response.status(Status.CREATED).entity(categoryService.create(category)).build();
    }

    @PUT
    @Path("{id}")
    public Response update(Category category, @PathParam("id") Long id) {
        if (Objects.isNull(category.getId()))
            category.setId(id);
        return categoryService.update(category).map(i -> Response.status(Status.ACCEPTED).entity(i).build())
                .orElseGet(() -> Response.status(Status.NOT_FOUND).build());
    }
    
    @DELETE
    @Path("{id}")
    public Response delete(@PathParam("id") Long id) {
        categoryService.delete(id);
        return Response.status(Status.ACCEPTED).build();
    }
    
    @GET
    @Path("{id}")
    public Response get(@PathParam("id") Long id) {
        return categoryService.get(id).map(i -> Response.status(Status.OK).entity(i).build())
                .orElseGet(() -> Response.status(Status.NO_CONTENT).build());
    }
    
    @GET
    public Response get() {
        return Response.status(Status.OK).entity(categoryService.getAll()).build();
    }
}

Hope this helps. Cheers!

ScottSummers
  • 310
  • 1
  • 13
0

I am working a project to create the generic repository for cassandra with spring data.

Firstly create a repository interface with code.

StringBuilder sourceCode = new StringBuilder();
sourceCode.append("import org.springframework.boot.autoconfigure.security.SecurityProperties.User;\n");
sourceCode.append("import org.springframework.data.cassandra.repository.AllowFiltering;\n");
sourceCode.append("import org.springframework.data.cassandra.repository.Query;\n");
sourceCode.append("import org.springframework.data.repository.CrudRepository;\n");
sourceCode.append("\n");
sourceCode.append("public interface TestRepository extends CrudRepository<Entity, Long> {\n");
sourceCode.append("}");

Compile the code and get the class, I use org.mdkt.compiler.InMemoryJavaCompiler

ClassLoader classLoader = org.springframework.util.ClassUtils.getDefaultClassLoader();
compiler = InMemoryJavaCompiler.newInstance();
compiler.useParentClassLoader(classLoader);
Class<?> testRepository = compiler.compile("TestRepository", sourceCode.toString());

And initialize the repository in spring data runtime. This is a little tricky as I debug the SpringData code to find how it initialize a repository interface in spring.

CassandraSessionFactoryBean bean = context.getBean(CassandraSessionFactoryBean.class);
RepositoryFragments repositoryFragmentsToUse = (RepositoryFragments) Optional.empty().orElseGet(RepositoryFragments::empty); 
CassandraRepositoryFactory factory = new CassandraRepositoryFactory(
    new CassandraAdminTemplate(bean.getObject(), bean.getConverter()));
factory.setBeanClassLoader(compiler.getClassloader());
Object repository = factory.getRepository(testRepository, repositoryFragmentsToUse);

Now you can try the save method of the repository and you can try other methods such as findById.

Method method = repository.getClass().getMethod("save", paramTypes);
T obj = (T) method.invoke(repository, params.toArray());

A full sample code and implementation I have put in this repo https://github.com/maye-msft/generic-repository-springdata.

You can extend it to JPA with the similar logic.

double-beep
  • 5,031
  • 17
  • 33
  • 41
Ye Ma
  • 9
  • 1