📜 ⬆️ ⬇️

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

More 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.

The classic option.


In the example resources , several entities and methods are presented 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 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 articles of Habr .

Domain:


@Entity public class User implements Serializable { private Long id; private String name; private String phone; @Id @GeneratedValue public Long getId() { return id; } public void setId(Long id) { this.id = id; } @Column(nullable = false) public String getName() { return name; } public void setName(String name) { this.name = name; } @Column public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } //equals, hashcode, toString } 

Repository:


 @Repository public interface UserRepository extends CrudRepository<User, Long> { } 

Service:


 public interface UserService { Optional<User> save(User user); } 

Service (implementation):


 @Service public class UserServiceImpl implements UserService { private final UserRepository userRepository; @Autowired public UserServiceImpl(UserRepository userRepository) { this.userRepository = userRepository; } @Override public Optional<User> save(User user) { return Optional.of(userRepository.save(user)); } } 

Controller:


 @RestController @RequestMapping("/user") public class UserController { private final UserService service; @Autowired public UserController(UserService service) { this.service = service; } @PostMapping public 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 resources in them. 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.

 @Entity public class Car implements Serializable { private Long id; private String brand; private String model; @Id @GeneratedValue public Long getId() { return id; } public void setId(Long id) { this.id = id; } //, , equals, hashcode, toString } 

Then we create a repository.

 public interface CarRepository extends CrudRepository<Car, Long> { } 

Then the service ...

 public interface CarService { 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 the implementation of the service ... ... Controller ... ... ...

Yes, you can just copy the same methods (they are universal) from the User class, then change the User to Car, then do the same with the implementation, with the controller, then another entity next in turn, and there already look again and again ... Usually you get tired already for the second, but the creation of a service architecture for a couple of dozen entities (copy-pasteing, replacing the name of the entity, was mistaken somewhere, was sealed somewhere ...) leads to agony that any monotonous work causes. 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, bind abstraction 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. Let's take out this field into a separate abstract entity and inherit from it User and Car.

AbstractEntity:


 @MappedSuperclass public abstract class AbstractEntity implements Serializable { private Long id; @Id @GeneratedValue public Long getId() { return id; } public void setId(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:


 @Entity public class User extends AbstractEntity { 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:


 @NoRepositoryBean public interface CommonRepository<E extends AbstractEntity> extends CrudRepository<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:


 @Repository public interface UserRepository extends CommonRepository<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:


 public interface CommonService<E extends AbstractEntity> { { Optional<E> save(E entity); //-     } 

AbstractService:


 public abstract class AbstractService<E extends AbstractEntity, R extends CommonRepository<E>> implements CommonService<E> { protected final R repository; @Autowired public AbstractService(R repository) { this.repository = repository; } // ,    } 

Here we redefine all the methods, and also create a parameterized constructor for the future repository, which we will redefine 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:


 @Service public class UserService extends AbstractService<User, UserRepository> { public UserService(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:


 public interface CommonController<E extends AbstractEntity> { @PostMapping ResponseEntity<E> save(@RequestBody E entity); //  } 

AbstractController:


 public abstract class AbstractController<E extends AbstractEntity, S extends CommonService<E>> implements CommonController<E> { private final S service; @Autowired protected AbstractController(S service) { this.service = service; } @Override public 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") public class UserController extends AbstractController<User, UserService> { public UserController(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:


 @Repository public interface CarRepository extends CommonRepository<Car> { } 

CarService:


 @Service public class CarService extends AbstractService<Car, CarRepository> { public CarService(CarRepository repository) { super(repository); } } 

CarController:


 @RestController @RequestMapping("/car") public class CarController extends AbstractController<Car, CarService> { public CarController(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 redefine 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 manually generate extra code, 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.

Source: https://habr.com/ru/post/423741/


All Articles