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.
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é.
Imaginez un service comme Qiita.
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.
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.
En référence à ce qui précède, la configuration est la suivante.
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
Il sera mis en œuvre à partir d'ici. Le code a également été téléchargé sur GitHub.
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.
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.
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.
Web
Recommended Posts