[JAVA] Implementierung einer starken API für "Ich möchte ~~ auf dem Bildschirm anzeigen" mit einfachem CQRS

Einführung

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.

Was ist CQRS?

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.

Implementierungsthema

Stellen Sie sich einen Service wie Qiita vor.

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

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.

Zu verwendende Sprache, FW usw.

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.

Verfassung

In Bezug auf das Obige ist die Konfiguration wie folgt.

CQRS (2).png

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

Implementierung

Es wird von hier aus implementiert. Der Code wurde auch auf [GitHub] hochgeladen (https://github.com/os1ma/spring-minimum-cqrs-sample).

Befehlsseite

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.

Abfrageseite

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.

abschließend

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.

Referenz

Bücher

Web

Recommended Posts

Implementierung einer starken API für "Ich möchte ~~ auf dem Bildschirm anzeigen" mit einfachem CQRS
Ich möchte die API mit Rails auf mehreren lokal eingerichteten Docker-Composes treffen
Ich möchte eine Browsing-Funktion mit Ruby on Rails hinzufügen
Ich möchte mit Kotlin und Java zum vorherigen Bildschirm zurückkehren!
Ich möchte ein chinesisches (koreanisches) PDF mit dünnen Berichten anzeigen
[Für Anfänger] Ich möchte mit einem Auswahlbefehl automatisch vorregistrierte Daten in das Eingabeformular eingeben.
Ich möchte eine Datei mit Ruby im Internet herunterladen und lokal speichern (mit Vorsicht).
Ich möchte den Namen des Posters des Kommentars anzeigen
Ich möchte den Dunkelmodus mit der SWT-App verwenden
Ich möchte eine bestimmte Datei mit WatchService überwachen
Ich möchte die Protokollausgabe unter Android vereinfachen
Ich möchte eine generische Anmerkung für einen Typ erstellen
Ich möchte der Kommentarfunktion eine Löschfunktion hinzufügen
Ich möchte RadioButtons an derselben Stelle auf dem Bildschirm in derselben RadioGroup platzieren
[Rails] Ich möchte das Linkziel von link_to auf einer separaten Registerkarte anzeigen
Ich möchte eine Liste mit Kotlin und Java erstellen!
Ich möchte eine Methode aufrufen und die Nummer zählen
Ich möchte eine Funktion mit Kotlin und Java erstellen!
Ich möchte die Java 8 DateTime-API (jetzt) langsam verwenden.
Ich möchte ein Formular erstellen, um die Kategorie [Schienen] auszuwählen
Was ich mit der Redmine REST API süchtig gemacht habe
Selbst in Java möchte ich true mit == 1 && a == 2 && a == 3 ausgeben
Ich möchte das JDK auf meinem Mac-PC installieren
Ich möchte dem select-Attribut einen Klassennamen geben
Ich möchte mit Java8 StreamAPI redu () einen anderen Typ als das Eingabeelement zurückgeben.
Ich möchte im gespeicherten Zustand zum selben Bildschirm wechseln
Ich möchte FireBase verwenden, um eine Zeitleiste wie Twitter anzuzeigen
Ich möchte mehrere Rückgabewerte für das eingegebene Argument zurückgeben
Ich möchte den Startbefehl mit Docker-Compose an Postgres übergeben.
Wie gehe ich mit dem Typ um, den ich 2 Jahre lang über das Schreiben eines Java-Programms nachgedacht habe?
Ich möchte rekursiv nach Dateien in einem bestimmten Verzeichnis suchen
Ich möchte mit link_to [Hinweis] eine Schaltfläche mit einem Zeilenumbruch erstellen.
Ich möchte SONY Kopfhörer WH-1000XM4 mit LDAC mit Ubuntu 20.04 verbinden! !!
Die Geschichte von Collectors.groupingBy, die ich für die Nachwelt behalten möchte
Logik zum Zeichnen eines Kreises auf der Konsole mit ASCII-Grafik
Ich habe ein einfaches Suchformular mit Spring Boot + GitHub Search API erstellt.
Ich möchte die deaktivierte Option abhängig von der Bedingung zu f.radio_button hinzufügen
[Java] Ich möchte mit dem Schlüssel im Objekt eindeutig arbeiten
[Einführung in JSP + Servlet] Ich habe eine Weile damit gespielt ♬
Ich habe versucht, den Kalender mit Java auf der Eclipse-Konsole anzuzeigen.
Ich möchte mit einem regulären Ausdruck zwischen Zeichenketten extrahieren
Ich möchte eine Servlet-War-Datei mit OpenJDK unter CentOS7 erstellen. Ohne mvn. Ohne Internetverbindung.
Eine Geschichte, der ich mit der automatischen Starteinstellung von Tomcat 8 unter CentOS 8 zweimal verfallen war
Eine Geschichte, die ich mit der Stream-API von Java8 einem Prozess schreiben wollte, der einer while-Anweisung entspricht
Eine Geschichte, der ich beim Testen der API mit MockMVC verfallen war
Ich möchte Bilder mit REST Controller von Java und Spring anzeigen!
Ich möchte im Dialogfeld mehrere Elemente mit einem benutzerdefinierten Layout auswählen
Selbst in Java möchte ich true mit == 1 && a == 2 && a == 3 ausgeben (PowerMockito Edition)
Grundlagen der Java-Programmierung - Ich möchte ein Dreieck mit einer for-Anweisung ① anzeigen
Ich habe versucht, eine Web-API zu erstellen, die mit Quarkus eine Verbindung zur Datenbank herstellt