Beispielsweise möchten Sie möglicherweise x, y und z VERBINDEN und den COUNT-Wert auf dem Bildschirm anzeigen. Möglicherweise möchten Sie die vom Bildschirm aus der Datenbank angeforderten Werte aggregieren und in der API zurückgeben. [^ 1]
[^ 1]: In diesem Artikel wird die API als Beispiel verwendet, aber dieselbe Methode kann angewendet werden, auch wenn es sich nicht um eine API handelt.
Wenn Sie in einem solchen Fall das DB-Modell dem Domänenmodell zuordnen und das Domänenmodell der API-Schnittstelle zuordnen und zurückgeben, treten die folgenden Probleme auf.
In diesem Artikel werde ich versuchen, das obige Problem mit einem einfachen CQRS zu lösen.
Einfach ausgedrückt ist CQRS eine Methode, die Schreib- (Kommaond) und Lese- (Abfrage) Prozesse trennt. Weitere Informationen finden Sie unter "Japanische Übersetzung von CQRS".
CQRS wird häufig zusammen mit Event Sourcing erwähnt, es ist jedoch nicht zwingend erforderlich, es mit Event Sourcing einzuführen.
In diesem Artikel besteht der erste Schritt in CQRS darin, Befehle und Abfragen in Ihrer Anwendung zu trennen. Im Gegenteil, es werden nicht die folgenden Elemente behandelt, die im fortgeschritteneren CQRS erscheinen.
Stellen Sie sich einen Service wie Qiita vor.
Erwägen Sie, Like als Beispiel für Command zu registrieren und eine Liste mit Artikeln als Beispiel für Query abzurufen.
Implementieren Sie beim Registrieren eines "Gefällt mir" die Geschäftslogik, um zu überprüfen, ob es sich beim Poster selbst um ein "Gefällt mir" handelt. Um die Artikelliste zu erhalten, werden Titel, Postername und Anzahl der Likes auf dieselbe Weise wie Qiita Top Page zurückgegeben.
Das Beispiel in diesem Artikel ist in Spring Boot (Java) implementiert. Ich verwende MyBatis als ORM, weil ich SQL auf der Abfrageseite frei schreiben möchte.
In Bezug auf das Obige ist die Konfiguration wie folgt.
Die obige Abbildung in der Verzeichnisstruktur sieht wie folgt aus.
.
src/main/java/
└── com
└── example
└── minimumcqrssample
├── MinimumCqrsSampleApplication.java
├── application
│ ├── exception
│ └── service
├── domain
│ └── model
├── infrastructure
│ ├── mysqlquery
│ └── mysqlrepository
└── interfaces
└── api
Es wird von hier aus implementiert. Der Code wurde auch auf [GitHub] hochgeladen (https://github.com/os1ma/spring-minimum-cqrs-sample).
Auf der Befehlsseite ist die Implementierung dieselbe, als ob CQRS nicht installiert wäre.
Es ist in 4 Schichten implementiert.
interfaces.api
Eine Implementierung von 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();
}
}
Die erforderlichen Parameter werden aus dem Anforderungspfad usw. extrahiert und ApplicationService wird aufgerufen. Wenn der Anforderungshauptteil vorhanden ist, erstellen Sie einen Typ wie LikePostCommandRequest und binden Sie ihn an @RequestBody.
Wenn der Vorgang abgeschlossen ist, wird eine 201 Created HTTP-Antwort zurückgegeben.
application.service
Die Anwendungsschicht. Diese Schicht ist für die Implementierung von Anwendungsfällen und die Steuerung von Transaktionen verantwortlich.
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);
}
}
Aus Gründen der Typensicherheit erhalten wir articleId und userId in ihrem eigenen Typ und nicht lange. Da es durch das Domänenmodellmuster implementiert wird, ist die Arbeit von ApplicationService gering, und der Anwendungsfall wird nur mithilfe der Schnittstelle des Domänenmodells realisiert. [^ 2]
[^ 2]: In diesem Beispiel wird es im Domänenmodell anstelle des Transaktionsskripts implementiert, kann jedoch durch das Transaktionsskript ersetzt werden. Klicken Sie hier, um den Unterschied zwischen dem Domain-Modell und dem Transaktionsskript anzuzeigen. B9% E3% 83% AD% E3% 82% B8% E3% 83% 83% E3% 82% AF% E5% B1% A4).
In diesem Beispiel ist der Rückgabewert von ApplicationService ungültig. Wenn Sie jedoch Location in der HTTP-Antwort zurückgeben möchten, können Sie die ID auch von ApplicationService zurückgeben.
domain.model
Implementieren Sie die Geschäftslogik im Domänenmodell. In diesem Beispiel wird die Logik implementiert, dass Sie einen Artikel, den Sie veröffentlichen, nicht mögen können.
Der obige ApplicationService befasst sich mit zwei Aggregaten, Like und Article. Schauen wir uns also diese beiden an.
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;
}
Wir haben eine Factory erstellt, die die Geschäftslogik als statische Methode von Like widerspiegelt, und der Konstruktor ist privat. [^ 3] Außerdem beziehen wir uns bei Artikel- und Benutzeraggregaten nur auf die ID des Aggregatstamms, damit wir nicht direkt auf andere Aggregate verweisen.
[^ 3]: Sie können es in eine andere Klasse ausschneiden, anstatt es als statische Methode zu verwenden.
Die Like-Klasse ist ein Stammobjekt (Aggregatstamm) eines Aggregats, das eine Einheit der Datenpersistenz darstellt. Für jede Aggregation wird ein Repository erstellt und eine Speichermethode mit dem Aggregationsstamm als Argument erstellt.
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);
}
}
Die Article-Klasse hat anstelle von Getter eine userBd-Methode für userId, um zu verhindern, dass userId von außen behandelt wird.
ArticleRepository.java
public interface ArticleRepository {
Optional<Article> findById(ArticleId articleId);
}
infrastructure.repositoryimpl
Eine Implementierung des DB-Zugriffs.
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>
Da Repository in aggregierten Einheiten und Mapper in Tabelleneinheiten erstellt wird, haben MySQL Repository und Mapper eine Eins-zu-Viele-Beziehung.
Dies ist die Hauptimplementierung auf der Abfrageseite dieses Artikels.
interfaces.api
Die Controller- und Antworttypen werden nur normal implementiert.
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;
}
}
Durch die Integration von CQRS erstellen wir eine Schnittstelle namens QueryService.
ArticleQueryService.java
public interface ArticleQueryService {
ArticleListQueryResponse list();
}
Die QueryService-Schnittstelle scheint besser in der Anwendungsschicht platziert zu sein, in diesem Beispiel jedoch in der Schnittstellenschicht. Der Grund ist wie folgt.
Wenn Sie eine kompliziertere Verarbeitung erzielen möchten, können Sie diese in der Anwendungsschicht platzieren.
Darüber hinaus der Artikel "Zurück zu den Grundlagen des fehlgeschlagenen Designs und des domänengesteuerten Designs" Für Anwendungen, bei denen die Abfrageseite wichtig ist, benötigen Sie möglicherweise auch eine Domänenschicht für die Abfrage.
infrastructure.queryimpl
Schließlich die Implementierung von 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();
}
In diesem Beispiel sind Repository und Mapper getrennt, es ist jedoch sicher, sie zu integrieren.
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>
In SQL werden JOIN, COUNT usw. frei beschrieben.
Dies habe ich am Anfang dieses Artikels erwähnt
Das Problem ist gelöst worden.
Ich denke, einfaches CQRS ist eine ziemlich gute Lösung für die Motivation, SQL frei im Referenzsystem zu schreiben. Es scheint, dass Sie weniger besorgt sein werden, wenn Ihnen gesagt wird "Ich möchte ~~ auf dem Bildschirm anzeigen".
Andererseits ist das Schreiben einer monotonen SELECT- oder INSERT-Anweisung im Update-System nur ein Aufwand. Wenn Sie nur Methoden wie findById und save benötigen, passt JPA möglicherweise besser als MyBatis. "[DDD x CQRS-Eine Geschichte, die mit verschiedenen ORMs für die Update- und Referenzsysteme gut funktioniert hat](https://speakerdeck.com/littlehands/ddd-x-cqrs-geng-xin-xi-tocan-zhao- xi-teyi-naruormwobing-yong-siteshang-shou-kuitutahua) “scheint es recht gut zu sein, das ORM zwischen dem Update-System und dem Referenzsystem zu ändern.
Web