[JAVA] Implémentation d'une API forte pour "Je veux afficher ~~ à l'écran" avec un simple CQRS

introduction

Par exemple, vous souhaiterez peut-être JOIN x, y et z et afficher la valeur COUNT à l'écran. Vous souhaiterez peut-être regrouper les valeurs demandées par l'écran à partir de la base de données et les renvoyer dans l'API. [^ 1]

[^ 1]: Cet article utilise l'API comme exemple, mais la même méthode peut être appliquée même s'il ne s'agit pas d'API.

Dans un tel cas, si vous mappez le modèle de base de données sur le modèle de domaine et mappez le modèle de domaine à l'interface API et le renvoyez, vous rencontrerez les problèmes suivants.

Dans cet article, je vais essayer de résoudre le problème ci-dessus avec un simple CQRS.

Qu'est-ce que le CQRS

En termes simples, CQRS est une méthode qui sépare les processus d'écriture (virgule) et de lecture (requête). Pour plus de détails, reportez-vous à "Traduction japonaise de CQRS".

On parle souvent de CQRS avec le sourcing événementiel, mais il n'est pas obligatoire de l'introduire avec le sourcing événementiel.

Dans cet article, la première étape de CQRS consiste à séparer les commandes et les requêtes au sein de votre application. Au contraire, il ne traite pas des éléments suivants qui apparaissent dans le CQRS plus avancé.

Thème de mise en œuvre

Imaginez un service comme Qiita.

CQRS_ドメインモデル (3).png

Pensez à enregistrer Like comme exemple de commande et à récupérer une liste d'articles comme exemple de requête.

Lors de l'enregistrement d'un Like, implémentez la logique métier pour vérifier qu'il ne s'agit pas d'un Like par l'affiche lui-même. Pour obtenir la liste des articles, le titre, le nom de l'affiche et le nombre de mentions J'aime sont renvoyés de la même manière que Qiita Top Page.

Langue à utiliser, FW, etc.

L'exemple de cet article est implémenté dans Spring Boot (Java). J'utilise MyBatis comme ORM parce que je veux écrire du SQL librement du côté de la requête.

Constitution

En référence à ce qui précède, la configuration est la suivante.

CQRS (2).png

En regardant la figure ci-dessus dans la structure des répertoires, c'est comme suit.

.
src/main/java/
└── com
    └── example
        └── minimumcqrssample
            ├── MinimumCqrsSampleApplication.java
            ├── application
            │   ├── exception
            │   └── service
            ├── domain
            │   └── model
            ├── infrastructure
            │   ├── mysqlquery
            │   └── mysqlrepository
            └── interfaces
                └── api

la mise en oeuvre

Il sera mis en œuvre à partir d'ici. Le code a également été téléchargé sur GitHub.

Côté commande

Du côté de la commande, l'implémentation sera la même que si CQRS n'était pas installé.

Il est implémenté en 4 couches.

interfaces.api

Une implémentation de Controller.

LikeCommandController.java


@RestController
@RequestMapping("/articles/{articleId}/likes")
@AllArgsConstructor
public class LikeCommandController {

  private LikeApplicationService service;

  @PostMapping
  public ResponseEntity<Void> post(@AuthenticationPrincipal SampleUserDetails sampleUserDetails,
                                   @PathVariable long articleId) {

    service.register(new ArticleId(articleId), sampleUserDetails.getUserId());

    return ResponseEntity.status(HttpStatus.CREATED).build();
  }

}

Les paramètres requis sont extraits du chemin de la requête, etc., et ApplicationService est appelée. Si le corps de la requête existe, créez un type comme LikePostCommandRequest et liez-le avec @RequestBody.

Lorsque le processus est terminé, il renvoie une réponse HTTP 201 Created.

application.service

La couche d'application. Cette couche est responsable de la réalisation des cas d'utilisation et du contrôle des transactions.

LikeApplicationService.java


@Service
@Transactional
@AllArgsConstructor
public class LikeApplicationService {

  private LikeRepository likeRepository;
  private ArticleRepository articleRepository;

  public void register(ArticleId articleId, UserId userId) {
    Article article = articleRepository.findById(articleId)
            .orElseThrow(BadRequestException::new);

    Like like = Like.of(article, userId);

    likeRepository.save(like);
  }

}

Pour la sécurité de type, acceptez articleId et userId comme type dédié plutôt que long. Puisqu'il est implémenté par le modèle de modèle de domaine, le travail d'ApplicationService est petit et le cas d'utilisation est réalisé uniquement en utilisant l'interface du modèle de domaine. [^ 2]

[^ 2]: Dans cet exemple, il est implémenté dans le modèle de domaine au lieu du script de transaction, mais il peut être remplacé par le script de transaction. Cliquez ici pour connaître la différence entre le modèle de domaine et le script de transaction [https://qiita.com/os1ma/items/7a229585ebdd8b7d86c2#%E3%83%93%E3%82%B8%E3%83%8D%E3%82% B9% E3% 83% AD% E3% 82% B8% E3% 83% 83% E3% 82% AF% E5% B1% A4).

Dans cet exemple, la valeur de retour d'ApplicationService est void, mais si vous souhaitez renvoyer Location dans la réponse HTTP, vous pouvez également renvoyer l'ID d'ApplicationService.

domain.model

Implémentez la logique métier dans le modèle de domaine. Cet exemple met en œuvre la logique selon laquelle vous ne pouvez pas aimer un article que vous publiez.

L'ApplicationService ci-dessus traite de deux agrégats, Like et Article, alors jetons un coup d'œil à ces deux.

domain.model.like

Like.java


@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Setter(AccessLevel.PRIVATE)
@EqualsAndHashCode
public class Like {

  /**
   * Factory.
   */
  public static Like of(Article article, UserId userId) {
    if (article.writtenBy(userId)) {
      throw new IllegalArgumentException();
    }
    return new Like(article.id(), userId);
  }

  private ArticleId articleId;
  private UserId userId;

}

Nous avons créé une fabrique qui reflète la logique métier comme une méthode statique de Like, et le constructeur est privé. [^ 3] De plus, pour les agrégats d'articles et d'utilisateurs, nous nous référons uniquement à l'ID de la racine agrégée afin de ne pas référencer directement d'autres agrégats.

[^ 3]: Vous pouvez le découper dans une autre classe au lieu de l'utiliser comme méthode statique.

La classe Like est un objet racine (racine d'agrégat) d'un agrégat qui est une unité de persistance des données. Un référentiel sera créé pour chaque agrégation, et une méthode de sauvegarde avec la racine d'agrégation comme argument sera préparée.

LikeRepository.java


public interface LikeRepository {
  void save(Like like);
}

domain.model.article

Article.java


@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Setter(AccessLevel.PRIVATE)
@EqualsAndHashCode
public class Article {
  private ArticleId id;
  private UserId userId;
  private String title;

  public ArticleId id() {
    return this.id;
  }

  public boolean writtenBy(UserId userId) {
    return this.userId.equals(userId);
  }
}

La classe Article a une méthode writeBy au lieu de Getter pour userId pour empêcher userId d'être manipulé de l'extérieur.

ArticleRepository.java


public interface ArticleRepository {
  Optional<Article> findById(ArticleId articleId);
}

infrastructure.repositoryimpl

Une implémentation d'accès à la base de données.

LikeMySQLRepository.java


@Repository
@AllArgsConstructor
public class LikeMySQLRepository implements LikeRepository {

  private LikeMapper likeMapper;

  @Override
  public void save(Like like) {
    likeMapper.save(like);
  }
}

LikeMapper.java


@Mapper
public interface LikeMapper {
  void save(Like like);
}

LikeMapper.xml


<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.example.minimumcqrssample.infrastructure.mysqlrepository.like.LikeMapper">

    <insert id="save" parameterType="com.example.minimumcqrssample.domain.model.like.Like">
        INSERT INTO `likes` (`article_id`, `user_id`) VALUES
        (#{articleId.value}, #{userId.value})
    </insert>

</mapper>

Puisque le référentiel est créé dans des unités agrégées et que le mappeur est créé dans des unités de table, le référentiel MySQL et le mappeur ont une relation un-à-plusieurs.

Côté requête

Il s'agit de l'implémentation principale du côté Requête de cet article.

interfaces.api

Les types Controller et Response sont simplement implémentés normalement.

ArticleQueryController.java


@RestController
@RequestMapping("/articles")
@AllArgsConstructor
public class ArticleQueryController {

  private ArticleQueryService service;

  @GetMapping
  public ResponseEntity<ArticleListQueryResponse> list() {
    return ResponseEntity.ok(service.list());
  }
}

ArticleListQueryResponse.java


@Data
@AllArgsConstructor
public class ArticleListQueryResponse {
  private List<Article> articles;

  @Data
  @AllArgsConstructor
  public static class Article {
    private String title;
    private String authorName;
    private long likeCount;
  }
}

En incorporant CQRS, nous créons une interface appelée QueryService.

ArticleQueryService.java


public interface ArticleQueryService {
  ArticleListQueryResponse list();
}

L'interface QueryService semble être mieux placée dans la couche application, mais dans cet exemple, elle est placée dans la couche d'interface. La raison en est la suivante.

Si vous souhaitez obtenir un traitement plus complexe, vous pouvez le placer dans la couche d'application.

En outre, l'article "Revenir aux bases de la conception échouée et de la conception pilotée par domaine" Pour les applications où le côté requête est important, vous pouvez également avoir besoin d'une couche de domaine pour la requête.

infrastructure.queryimpl

Enfin, l'implémentation de Query.

LikeMySQLRepository.java


@Service
@AllArgsConstructor
public class ArticleMySQLQueryService implements ArticleQueryService {

  private ArticleMySQLQueryMapper mapper;

  @Override
  public ArticleListQueryResponse list() {
    return new ArticleListQueryResponse(mapper.list());
  }
}

ArticleMySQLQueryMapper.java


@Mapper
public interface ArticleMySQLQueryMapper {
  List<ArticleListQueryResponse.Article> list();
}

Dans cet exemple, Repository et Mapper sont séparés, mais vous pouvez les intégrer en toute sécurité.

ArticleMySQLQueryMapper.xml


<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.example.minimumcqrssample.infrastructure.mysqlquery.article.ArticleMySQLQueryMapper">

    <resultMap id="article"
               type="com.example.minimumcqrssample.interfaces.api.article.ArticleListQueryResponse$Article">
        <result property="title" column="title"/>
        <result property="authorName" column="author_name"/>
        <result property="likeCount" column="like_count"/>
    </resultMap>

    <select id="list" resultMap="article">
        SELECT
            MAX(a.title) AS title,
            MAX(u.name) AS author_name,
            COUNT(*) AS like_count
        FROM articles a
        INNER JOIN users u ON a.user_id = u.id
        INNER JOIN likes l ON a.id = l.article_id
        GROUP BY l.article_id
    </select>

</mapper>

En SQL, JOIN, COUNT, etc. sont librement décrits.

C'est ce que j'ai mentionné au début de cet article

Le problème a été résolu.

en conclusion

Je pense que le simple CQRS est une assez bonne solution à la motivation d'écrire librement SQL dans le système de référence. Il semble que vous n'aurez pas à vous inquiéter quand on vous dit "Je veux afficher ~~ à l'écran".

D'autre part, dans le système de mise à jour, écrire une instruction SELECT monotone ou une instruction INSERT n'est qu'un tracas. Si vous avez juste besoin de méthodes comme findById et save, JPA peut être une meilleure correspondance que MyBatis. "[DDD x CQRS-Une histoire qui a bien fonctionné avec différents ORM pour la mise à jour et la référence" (https://speakerdeck.com/littlehands/ddd-x-cqrs-geng-xin-xi-tocan-zhao- xi-teyi-naruormwobing-yong-siteshang-shou-kuitutahua) », il semble plutôt bien de changer l'ORM entre le système de mise à jour et le système de référence.

référence

Livres

Web

Recommended Posts

Implémentation d'une API forte pour "Je veux afficher ~~ à l'écran" avec un simple CQRS
Je veux accéder à l'API avec Rails sur plusieurs docker-composes configurés localement
Je souhaite ajouter une fonction de navigation avec ruby on rails
Je veux revenir à l'écran précédent avec kotlin et java!
Je souhaite afficher un PDF chinois (coréen) avec des rapports fins
[Pour les débutants] Je souhaite saisir automatiquement des données pré-enregistrées dans le formulaire de saisie avec une commande de sélection.
Je souhaite télécharger un fichier sur Internet en utilisant Ruby et l'enregistrer localement (avec prudence)
Je souhaite afficher le nom de l'affiche du commentaire
Je souhaite utiliser le mode sombre avec l'application SWT
Je souhaite surveiller un fichier spécifique avec WatchService
Je souhaite simplifier la sortie du journal sur Android
Je souhaite créer une annotation générique pour un type
Je souhaite ajouter une fonction de suppression à la fonction de commentaire
Je veux placer RadioButtons dans le même RadioGroup à n'importe quelle position de l'écran
[Rails] Je souhaite afficher la destination du lien de link_to dans un onglet séparé
Je veux faire une liste avec kotlin et java!
Je veux appeler une méthode et compter le nombre
Je veux créer une fonction avec kotlin et java!
Je souhaite utiliser l'API Java 8 DateTime lentement (maintenant)
Je souhaite créer un formulaire pour sélectionner la catégorie [Rails]
Ce à quoi j'étais accro avec l'API REST Redmine
Même en Java, je veux afficher true avec un == 1 && a == 2 && a == 3
Je veux mettre le JDK sur mon PC Mac
Je veux donner un nom de classe à l'attribut select
Je veux renvoyer un type différent de l'élément d'entrée avec Java8 StreamAPI Reduce ()
Je souhaite passer au même écran dans l'état enregistré
Je souhaite utiliser FireBase pour afficher une chronologie comme Twitter
Je souhaite renvoyer plusieurs valeurs de retour pour l'argument saisi
Je veux passer la commande de démarrage à postgres avec docker-compose.
Comment gérer le type auquel j'ai pensé en écrivant un programme Java pendant 2 ans
Je souhaite rechercher de manière récursive des fichiers dans un répertoire spécifique
Je veux créer un bouton avec un saut de ligne avec link_to [Note]
Je veux connecter un casque SONY WH-1000XM4 avec LDAC avec ubuntu 20.04! !!
L'histoire de Collectors.groupingBy que je veux garder pour la postérité
Logique pour dessiner un cercle sur la console avec l'art ASCII
J'ai créé un formulaire de recherche simple avec Spring Boot + GitHub Search API.
Je veux ajouter l'option désactivée à f.radio_button en fonction de la condition
[Java] Je veux effectuer distinctement avec la clé dans l'objet
[Introduction à JSP + Servlet] J'ai joué avec pendant un moment ♬
J'ai essayé d'afficher le calendrier sur la console Eclipse en utilisant Java.
Je veux extraire entre des chaînes de caractères avec une expression régulière
Je veux créer un fichier de guerre Servlet avec OpenJDK sur CentOS7. Sans mvn. Sans connexion Internet.
Une histoire à laquelle j'étais accro à deux reprises avec le paramètre de démarrage automatique de Tomcat 8 sur CentOS 8
Je voulais écrire un processus équivalent à une instruction while avec l'API Java 8 Stream
Une histoire à laquelle j'étais accro lors du test de l'API à l'aide de MockMVC
Je veux afficher des images avec REST Controller de Java et Spring!
Je souhaite sélectionner plusieurs éléments avec une disposition personnalisée dans la boîte de dialogue
Même en Java, je veux afficher true avec un == 1 && a == 2 && a == 3 (édition PowerMockito)
Pratique des bases de la programmation Java - Je veux afficher un triangle avec une instruction for ①
J'ai essayé de créer une API Web qui se connecte à DB avec Quarkus