Abstract CRUD from the repository to the controller: what else can you do with Spring + Generics

    Most recently, an article of a colleague flashed on Habré , which described a rather interesting approach to combining Generics and Spring capabilities. She reminded me of one approach that I use for writing microservices, and that’s what I decided to share with readers.



    At the output, we get a transport system, to add a new entity to which we will need to restrict ourselves to initializing one bin in each element of the repository-service-controller bundle.

    Immediately resources .
    Vetka as I do not: standart_version .
    The approach described in the article in the abstract_version branch .

    I built the project through Spring Initializr , adding JPA, Web and H2 frameworks. Gradle, Spring Boot 2.0.5. That will be quite enough.



    To begin with, consider the classic version of transport from the controller to the repository and back, devoid of any additional logic. If you want to go to the essence of the approach, scroll down to an abstract version. But, nevertheless, I recommend reading the entire article.

    Classic option.


    In the example resources there are several entities and methods for them, but in the article let us have only one User entity and only one save () method, which we drag from the repository through the service to the controller. In the resources of their 7, but in general the Spring CRUD / JPA Repository allow you to use about a dozen methods of saving / receiving / deleting, plus you can use, for example, some of their universal . Also, we will not be distracted by such necessary things as validation, dto mapping and so on. You can add it yourself or study it in other Habr articles .

    Domain:


    @EntitypublicclassUserimplementsSerializable{
        private Long id;
        private String name;
        private String phone;
        @Id@GeneratedValuepublic Long getId(){
            return id;
        }
        publicvoidsetId(Long id){
            this.id = id;
        }
        @Column(nullable = false)
        public String getName(){
            return name;
        }
        publicvoidsetName(String name){
            this.name = name;
        }
        @Columnpublic String getPhone(){
            return phone;
        }
        publicvoidsetPhone(String phone){
            this.phone = phone;
        }
    //equals, hashcode, toString
    }

    Repository:


    @RepositorypublicinterfaceUserRepositoryextendsCrudRepository<User, Long> {
    }

    Service:


    publicinterfaceUserService{
        Optional<User> save(User user);
    }

    Service (implementation):


    @ServicepublicclassUserServiceImplimplementsUserService{
        privatefinal UserRepository userRepository;
        @AutowiredpublicUserServiceImpl(UserRepository userRepository){
            this.userRepository = userRepository;
        }
        @Overridepublic Optional<User> save(User user){
            return Optional.of(userRepository.save(user));
        }
    }

    Controller:


    @RestController@RequestMapping("/user")
    publicclassUserController{
        privatefinal UserService service;
        @AutowiredpublicUserController(UserService service){
            this.service = service;
        }
        @PostMappingpublic ResponseEntity<User> save(@RequestBody User user){
            return service.save(user).map(u -> new ResponseEntity<>(u, HttpStatus.OK))
                    .orElseThrow(() -> new UserException(
                            String.format(ErrorType.USER_NOT_SAVED.getDescription(), user.toString())
                    ));
        }
    }

    We have a certain set of dependent classes that will help us operate with the User entity at the CRUD level. This is in our example by one method, there are more of them in resources. This is not at all an abstract variant of writing layers presented in the standart_version branch .

    Suppose we need to add another entity, say, Car. We will not map them at the entity level to each other (if there is a desire, you can zamapit).

    First, create an entity.

    @EntitypublicclassCarimplementsSerializable{
        private Long id;
        private String brand;
        private String model;
        @Id@GeneratedValuepublic Long getId(){
            return id;
        }
        publicvoidsetId(Long id){
            this.id = id;
        }
    //геттеры, сеттеры, equals, hashcode, toString
    }

    Then we create a repository.

    publicinterfaceCarRepositoryextendsCrudRepository<Car, Long> {
    }

    Then the service ...

    publicinterfaceCarService{
        Optional<Car> save(Car car);
        List<Car> saveAll(List<Car> cars);
        Optional<Car> update(Car car);
        Optional<Car> get(Long id);
        List<Car> getAll();
        Boolean deleteById(Long id);
        Boolean deleteAll();
    }

    Then service implementation ....... Controller ... ... ...

    Yes, you can just copy the same methods (they are universal) from the User class, then change User to Car, then do the same with the implementation, with the controller, next in turn the next entity , and there are already looking again and again ... Usually you get tired for the second, the creation of a service architecture for a couple of dozen entities (copy-and-paste, replacing the name of the entity, somewhere was mistaken, somewhere was sealed ...) leads to tortures that any monotonous work. Try to register twenty entities at your leisure and you will understand what I mean.

    And so, at one moment, when I was just keen on generics and type parameters, it dawned on me that the process could be done much less routine.

    So, abstractions based on typical parameters.


    The meaning of this approach is to bring all the logic into abstraction, abstraction to bind to the standard parameters of the interface, and inject bins into other bins. And that's all. No logic in the bins. Inject only other bins. This approach involves writing the architecture and logic once and not duplicating it when adding new entities.

    Let's start with the cornerstone of our abstraction - an abstract entity. The chain of abstract dependencies that will serve as the frame of the service will begin with it.

    All entities have at least one common field (usually more). This is an ID. We'll take this field into a separate abstract entity and inherit from it User and Car.

    AbstractEntity:


    @MappedSuperclasspublicabstractclassAbstractEntityimplementsSerializable{
        private Long id;
        @Id@GeneratedValuepublic Long getId(){
            return id;
        }
        publicvoidsetId(Long id){
            this.id = id;
        }
    }

    Do not forget to mark the abstraction with the annotation @MappedSuperclass - Hibernate should also know that this is an abstraction.

    User:


    @EntitypublicclassUserextendsAbstractEntity{
        private String name;
        private String phone;
        //...
    }

    With Car, respectively, the same.

    In each layer, in addition to bins, we will have one interface with typical parameters and one abstract class with logic. In addition to the repository - thanks to the specifics of Spring Data JPA, everything will be much easier here.

    The first thing we need in the repository is a common repository.

    CommonRepository:


    @NoRepositoryBeanpublicinterfaceCommonRepository<EextendsAbstractEntity> extendsCrudRepository<E, Long> {
    }

    In this repository, we set general rules for the whole chain: all entities participating in it will inherit from the abstract. Further, for each entity we need to write our own repository-interface, in which we will designate with which entity this repository-service-controller chain will work.

    UserRepository:


    @RepositorypublicinterfaceUserRepositoryextendsCommonRepository<User> {
    }

    On this, thanks to the features of Spring Data JPA, setting up the repository ends - everything will work and so. Next comes the service. We need to create a common interface, abstraction and bin.

    CommonService:


    publicinterfaceCommonService<EextendsAbstractEntity> { {
        Optional<E> save(E entity);
    //какое-то количество нужных нам методов
    }

    AbstractService:


    publicabstractclassAbstractService<EextendsAbstractEntity, RextendsCommonRepository<E>>
            implementsCommonService<E> {
        protectedfinal R repository;
        @AutowiredpublicAbstractService(R repository){
            this.repository = repository;
        }
    //другие методы, переопределённые из интерфейса
    }

    Here we override all the methods, and also create a parameterized constructor for the future repository, which we will override in the bean. Thus, we are already using a repository that we have not yet defined. We do not yet know which entity will be processed in this abstraction, and which repository we need.

    UserService:


    @ServicepublicclassUserServiceextendsAbstractService<User, UserRepository> {
        publicUserService(UserRepository repository){
            super(repository);
        }
    }

    In Bina, we do the final thing - we clearly define the repository we need, which will then be called up in the abstraction constructor. And that's all.

    With the help of interface and abstraction, we created a highway through which we will drive all entities. In Bina, however, we bring up a junction to the highway, along which we will bring the entity we need to the highway.

    The controller is built on the same principle: interface, abstraction, bin.

    CommonController:


    publicinterfaceCommonController<EextendsAbstractEntity> {
        @PostMappingResponseEntity<E> save(@RequestBody E entity);
    //остальные методы
    }

    AbstractController:


    publicabstractclassAbstractController<EextendsAbstractEntity, SextendsCommonService<E>> 
            implementsCommonController<E> {
        privatefinal S service;
        @AutowiredprotectedAbstractController(S service){
            this.service = service;
        }
        @Overridepublic ResponseEntity<E> save(@RequestBody E entity){
            return service.save(entity).map(ResponseEntity::ok)
                    .orElseThrow(() -> new SampleException(
                            String.format(ErrorType.ENTITY_NOT_SAVED.getDescription(), entity.toString())
                    ));
        }
    //другие методы
    }

    UserController:


    @RestController@RequestMapping("/user")
    publicclassUserControllerextendsAbstractController<User, UserService> {
        publicUserController(UserService service){
            super(service);
        }
    }

    This is the whole structure. It is written once.

    What's next?


    And now let's imagine that we have a new entity, which we have already inherited from Abstract Entity, and we need to prescribe the same chain for it. This will take us a minute. And no copy-paste and fixes.

    Take the already inherited from AbstractEntity Car.

    CarRepository:


    @RepositorypublicinterfaceCarRepositoryextendsCommonRepository<Car> {
    }

    CarService:


    @ServicepublicclassCarServiceextendsAbstractService<Car, CarRepository> {
        publicCarService(CarRepository repository){
            super(repository);
        }
    }

    CarController:


    @RestController@RequestMapping("/car")
    publicclassCarControllerextendsAbstractController<Car, CarService> {
        publicCarController(CarService service){
            super(service);
        }
    }

    As we can see, copying the same logic is simply adding a bean. No need to re-write the logic in each bin with changing parameters and signatures. They are written once and work in each subsequent case.

    Conclusion


    Of course, the example describes such a spherical situation in which the CRUD for each entity has the same logic. This does not happen - you still have to override some methods in the bin or add new ones. But this will come from the specific needs of the processing entity. Well, if 60 percent of the total number of CRUD methods will remain in the abstraction. And this will be a good result, because the more we generate extra code manually, the more time we spend on monotonous work and the higher the risk of error or typos.

    I hope the article was helpful, thank you for your attention.

    UPD.

    Thanks to the aleksandy proposal, it was possible to bring the initialization of the bin to the constructor and thereby significantly improve the approach. If you see how else you can improve the example, write in the comments, and perhaps your suggestions will be made.

    Also popular now: