CRUD abstrait du référentiel au contrôleur: que pouvez-vous faire d'autre avec Spring + Generics

Plus récemment, un article d' un collègue a fait un flash sur Habré, qui a décrit une approche plutôt intéressante pour combiner les capacités Génériques et Spring. Elle m'a rappelé une approche que j'utilise pour écrire des microservices, et j'ai décidé de la partager avec les lecteurs.



En sortie, nous obtenons un système de transport, auquel ajouter une nouvelle entité dont nous aurons besoin pour nous limiter à initialiser un bean dans chaque élément du bundle repository-service-controller.

Ressources immédiatement.
Branch, comme je ne le fais pas: standart_version .
L'approche décrite dans l'article se trouve dans la branche abstract_version .

J'ai monté un projet via Spring Initializr , en ajoutant les frameworks JPA, Web et H2. Gradle, Spring Boot 2.0.5. Ce sera bien suffisant.



Pour commencer, considérez la version classique du transport du contrôleur au référentiel et vice versa, dépourvue de toute logique supplémentaire. Si vous voulez aller à l'essentiel de l'approche, faites défiler jusqu'à la version abstraite. Mais, néanmoins, je recommande de lire l'article complet.

La version classique.


Les ressources de l' exemple leur fournissent plusieurs entités et méthodes, mais dans l'article, nous n'avons qu'une seule entité User et une seule méthode save (), que nous ferons glisser du référentiel via le service vers le contrôleur. Dans les ressources, il y en a 7, mais en général Spring CRUD / JPA Repository vous permet d'utiliser environ une douzaine de méthodes d'enregistrement / réception / suppression, plus vous pouvez utiliser, par exemple, certaines universelles . De plus, nous ne serons pas distraits par des choses nécessaires telles que la validation, le mappage dto et ainsi de suite. Vous pouvez l'ajouter vous-même ou étudier dans d'autres articles de Habr .

Domaine:


@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 } 

Référentiel:


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

Service:


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

Service (mise en œuvre):


 @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)); } } 

ContrĂ´leur:


 @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()) )); } } 

Nous avons obtenu un certain ensemble de classes dépendantes qui nous aideront à opérer sur l'entité Utilisateur au niveau CRUD. Dans notre exemple, c'est une méthode, il y a plus de ressources. Cette version pas du tout abstraite des couches d'écriture est présentée dans la branche standart_version .

Supposons que nous devons ajouter une autre entité, disons, Car. Nous ne gagnerons pas d’argent au niveau des entités (si vous le souhaitez, vous pouvez le mapper).

Créez d'abord une entité.

 @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 } 

Créez ensuite un référentiel.

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

Alors le 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(); } 

Puis la mise en place du service ....... ContrĂ´leur ...........

Oui, vous pouvez simplement copier et coller les mêmes méthodes (elles sont universelles ici) à partir de la classe User, puis changer User en Car, puis faire de même avec l'implémentation, avec le contrôleur, puis l'entité suivante est la prochaine en ligne, et là, elles regardent déjà de plus en plus ... Habituellement, vous en avez assez de la seconde, la création d'une architecture de service pour quelques dizaines d'entités (copier-coller, remplacer le nom de l'entité, quelque part erroné, quelque part scellé ...) conduit au tourment que provoque tout travail monotone. Essayez de prescrire une vingtaine d'entités à votre guise et vous comprendrez ce que je veux dire.

Et donc, à un moment donné, alors que je n'étais intéressé que par les génériques et les paramètres typiques, il m'est apparu que le processus pouvait être rendu beaucoup moins routinier.

Donc, des abstractions basées sur des paramètres typiques.


Le sens de cette approche est de prendre toute la logique en abstraction, de lier l'abstraction aux paramètres typiques de l'interface et d'injecter d'autres bacs dans les bacs. Et c'est tout. Aucune logique dans les haricots. Seulement une injection d'autres haricots. Cette approche implique d'écrire une fois l'architecture et la logique et non de les dupliquer lors de l'ajout de nouvelles entités.

Commençons par la pierre angulaire de notre abstraction - une entité abstraite. C'est d'elle que commencera la chaîne des dépendances abstraites, qui servira de cadre au service.

Toutes les entités ont au moins un champ commun (généralement plus). Ceci est l'ID. Nous prenons ce champ dans une entité abstraite distincte et en héritons Utilisateur et Voiture.

Entité abstraite:


 @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; } } 

N'oubliez pas de marquer l'abstraction avec l'annotation @MappedSuperclass - Hibernate doit également savoir qu'il s'agit d'une abstraction.

Utilisateur:


 @Entity public class User extends AbstractEntity { private String name; private String phone; //... } 

Avec Car, en conséquence, la même chose.

Dans chaque couche, en plus des bacs, nous aurons une interface avec des paramètres typiques et une classe abstraite avec logique. En plus du référentiel - grâce aux spécificités de Spring Data JPA, tout sera beaucoup plus simple ici.

La première chose dont nous avons besoin dans le référentiel est un référentiel partagé.

Dépôt commun:


 @NoRepositoryBean public interface CommonRepository<E extends AbstractEntity> extends CrudRepository<E, Long> { } 

Dans ce référentiel, nous définissons des règles générales pour toute la chaîne: toutes les entités qui y participent hériteront de l'abstrait. Ensuite, pour chaque entité, nous devons écrire notre propre interface de référentiel, dans laquelle nous indiquons avec quelle entité cette chaîne de référentiel-service-contrôleur fonctionnera.

UserRepository:


 @Repository public interface UserRepository extends CommonRepository<User> { } 

Grâce aux fonctionnalités de Spring Data JPA, la configuration du référentiel se termine ici - tout fonctionnera comme ça. Vient ensuite le service. Nous devons créer une interface, une abstraction et un bean communs.

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; } // ,    } 

Ici, nous redéfinissons toutes les méthodes et créons également un constructeur paramétré pour le futur référentiel, que nous redéfinissons dans le bean. Ainsi, nous utilisons déjà un référentiel que nous n'avons pas encore défini. Nous ne savons pas encore quelle entité sera traitée dans cette abstraction et de quel référentiel nous aurons besoin.

UserService:


 @Service public class UserService extends AbstractService<User, UserRepository> { public UserService(UserRepository repository) { super(repository); } } 

Dans le bac, nous faisons la dernière chose - nous définissons explicitement le référentiel dont nous avons besoin, qui est ensuite appelé dans le constructeur d'abstraction. Et c'est tout.

En utilisant l'interface et l'abstraction, nous avons créé une autoroute à travers laquelle nous conduirons toutes les entités. Dans la poubelle, nous apportons le dénouement à l'autoroute, à travers lequel nous afficherons l'entité dont nous avons besoin sur l'autoroute.

Le contrĂ´leur est construit sur le mĂŞme principe: 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); } } 

C'est toute la structure. Il est écrit une fois.

Et ensuite?


Et maintenant, imaginons que nous avons une nouvelle entité que nous avons déjà héritée de AbstractEntity, et que nous devons écrire la même chaîne pour elle. Cela nous prendra une minute. Et pas de copier-coller et de corrections.

Prenez déjà hérité de 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); } } 

Comme nous pouvons le voir, copier la même logique consiste à simplement ajouter un bean. Pas besoin de réécrire la logique dans chaque bac avec le changement des paramètres et des signatures. Ils sont écrits une seule fois et fonctionnent dans chaque cas ultérieur.

Conclusion


Bien sûr, l'exemple décrit une sorte de situation sphérique dans laquelle le CRUD pour chaque entité a la même logique. Cela ne se produit pas - vous devez encore redéfinir certaines méthodes dans le bac ou en ajouter de nouvelles. Mais cela proviendra des besoins spécifiques du traitement de l'entité. Eh bien, si 60% du nombre total de méthodes CRUD resteront en abstraction. Et ce sera un bon résultat, car plus nous générons manuellement de code redondant, plus nous passons de temps sur un travail monotone et plus le risque d'erreurs ou de fautes de frappe est élevé.

J'espère que l'article vous a été utile, merci de votre attention.

UPD

Grâce à la proposition, aleksandy a réussi à obtenir l'initialisation du bean dans le constructeur et à améliorer ainsi considérablement l'approche. Si vous voyez comment vous pouvez améliorer l'exemple, écrivez dans les commentaires, et peut-être que vos suggestions seront soumises.

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


All Articles