CodeGym /Kurse /Spring Data JPA /Lese-/Schreibdienste und Transaktionen

Lese-/Schreibdienste und Transaktionen

Spring Data JPA
Level 19, Lektion 3
Verfügbar

1. Service als Inhaber der Absicht

Wenn es in der Anwendung Repositories gibt, beginnt man schnell, Services als „dünne Hülle um repository.save(...)“ zu betrachten. Das ist ungefähr so, als würde man eine Mikrowelle kaufen und darin nur Tee zubereiten (technisch möglich, aber das Leben könnte offensichtlich reicher sein). Die Service-Schicht in unserem Mini-Shop existiert nicht um der architektonischen Schönheit willen, sondern um die Absicht des Use Cases auszudrücken: „Katalogseite abrufen“, „Preis ändern“, „Bestellung aufgeben“, „Restbestand reservieren“.

Das Repository hat tatsächlich seine eigenen transactional defaults, und das ist eine nützliche Absicherung. Aber es beantwortet nicht die Hauptfrage: Liest diese Operation gerade, schreibt sie oder fasst sie mehrere Schritte in einem Use Case zusammen? Das beantwortet bereits der Service, weil er der Inhaber der Absicht und der Grenze der Operation ist.

Die Transaktion in Spring ist fast immer an die Service-Operation gebunden. Das ist logisch: Die Transaktion sollte die gesamte Geschäftsoperation umfassen, nicht ein „zufälliges Stück“ innerhalb des Repositories oder eine „Hilfsmethode mit drei Zeilen“. Bevor wir also über Lese-/Schreibdienste sprechen, müssen wir den grundlegenden Gedanken festhalten: Der Service ist der Ort, an dem wir entscheiden, ob eine Operation ein Lese- oder ein Schreibvorgang ist und wie ihr transaktionaler Rahmen aussehen sollte.

Eine einfache Regel hilft uns, die anfangs zu „einfach“ erscheint, aber vor Chaos bewahrt: Wenn eine Methode nur liest – sollte sie als Read-Use-Case gestaltet sein; wenn eine Methode Daten ändert – sollte sie als Write-Use-Case gestaltet sein. Und wenn eine Methode überhaupt nicht auf die DB zugreift und lediglich lineTotal berechnet, dann ist sie einfach eine Methode (und sollte nicht in Transaktionen spielen).

Um dies nicht als Philosophie im Kopf zu behalten, halten wir den Unterschied gleich in der Form fest. In unserem Projekt shop-data-jpa ist es letztendlich praktisch, zwei Arten von Diensten zu haben:

Frage Query-Dienst (Lesen) Command-Dienst (Ändern)
Was tut er Holt Daten, erstellt View/Antwort Ändert den Systemzustand (erzeugt/aktualisiert/storniert)
Typische Annotation @Transactional(readOnly = true) @Transactional
Inhalt Repository-Lesezugriffe, manchmal mehrere Lesen → Prüfen → Ändern → Schreiben
Wie Methoden benennen find..., get..., load... create..., change..., activate..., cancel...
Hauptrisiko versehentlich Schreiben „einmischen“ all-or-nothing zerstören, Transaktion aufteilen

Und ja, das klingt banal. Aber in realen Projekten führt genau das Fehlen dieser Banalität später zu einem „Service mit 2000 Zeilen, bei dem die Hälfte der Methoden schreibt, die andere Hälfte liest und die Transaktionen wie Sterne am Himmel verteilt sind – schön, aber unklar wofür“.

2. Transaktion als Rahmen des Use Case

Wenn wir sagen „wo die Transaktion beginnt und endet“, stellen sich viele etwas auf SQL-Ebene vor: BEGIN, COMMIT, ROLLBACK. In einer Spring-Anwendung schreiben wir das normalerweise nicht von Hand, aber der Sinn ist derselbe: Wir haben die Transaktion am Anfang des Use Case geöffnet, am Ende geschlossen. Und hier kommt der entscheidende technische Punkt: Die Transaktion sollte dort beginnen, wo der Use Case begann, nicht „irgendwo innen“.

Damit sich das nicht wie ein Slogan anfühlt, stellen wir uns einen typischen Aufrufablauf in unserem Projekt vor:

flowchart TD
    A["Eintritt in den Use Case
(öffentliche Service-Methode)"] --> B["Transaktion gestartet"] B --> C["Repository reads / checks"] C --> D["Business changes"] D --> E["Repository writes"] E --> F["Transaktion abgeschlossen
(commit/rollback)"]

Der typischste Startpunkt einer Transaktion ist die öffentliche Methode eines Spring-Services, weil sie normalerweise der „Eintrittspunkt“ in ein Szenario ist. Das stimmt auch mit dem überein, was wir früher über proxy boundary besprochen haben: Spring öffnet die Transaktion „um den Aufruf“ der Service-Methode herum.

Der Service muss dabei nicht groß sein. Im Gegenteil: Je besser Sie den Use Case formulieren, desto kürzer ist in der Regel die Service-Methode. Zum Beispiel ist „Preis eines Produkts ändern“ eine normale eigenständige Operation, aber „Preis und Status und Kategorie ändern und nebenbei noch einen Bericht lesen“ – das ist bereits ein verdächtiger Mischmasch.

Schauen wir uns einen charakteristischen Write-Use-Case an und markieren die Grenze. Wichtig ist nicht die Menge des Codes, sondern dass alle Schritte innerhalb einer Transaktion leben:

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class PriceChangeService {

    @Transactional
    public void changePrice(Long productId) {
        // Öffentliche Methode des Services – typischer „Eintrittspunkt“ des Use Case.
        // Deshalb sollte hier normalerweise die Transaktion beginnen.
        // Innerhalb werden Lesen, Prüfen, Ändern, Schreiben stattfinden.
    }
}

Das sieht noch nach einer „leeren Methode“ aus, aber schon in diesem Stadium ist sie nützlich: Wir haben klar gesagt, dass es eine Änderungsoperation ist. Später füllen wir sie ruhig mit dem Lesen der Entität, Prüfung und Schreiben, ohne die Transaktion über die gesamte Codebasis zu verteilen.

Und hier ein wichtiger Gedanke zum Lesen: Ein Read-Use-Case kann ebenfalls eine Service-Operation sein und kann ebenfalls einen transaktionalen Rahmen haben. Nicht weil „es so üblich ist“, sondern weil ein Read-Szenario aus mehreren Lesevorgängen bestehen kann, die Konsistenz benötigen, oder einfach weil wir die Absicht im Code explizit zeigen wollen.

3. Lesedienste: Read-Only-Transaktionen

Das Thema Read-Dienste wird oft unterschätzt: Es scheint, dass Lesen einfach „das Repository aufrufen und das Ergebnis zurückgeben“ ist. Aber in einem realen Projekt wird das Lesen schnell zu einer „komplexen Datenverknüpfung“, besonders wenn Sie nicht die gesamte Entität, sondern ein sauberes Read-Modell erhalten möchten (z.B. ProductCatalogRow oder OrderSummaryRow, die wir früher über Projections erstellt haben). Daher ist ein Read-Dienst keine „überflüssige Schicht“, sondern der Ort, an dem wir Repository-Abfragen in ein verständliches Anwendungsszenario verwandeln.

Die einfachste Regel für Read-Dienste: Wenn eine Methode Lesen ausdrückt, sollte sie @Transactional(readOnly = true) sein. Das macht den Code lesbarer und unterstreicht die Absicht. Zudem vermeiden Sie, versehentlich Schreiboperationen „einzumischen“, die dann ein Eigenleben entwickeln.

Beispiel für den Katalog: Wir möchten die Seite der aktiven Produkte als Projektion ProductCatalogRow erhalten. Selbst wenn das Repository intern bereits read-only sein kann, fixiert der Service den Use Case und gibt ihm einen Namen, der angenehm zu lesen ist.

import com.example.shopdatajpa.catalog.query.ProductCatalogRow;
import com.example.shopdatajpa.catalog.repository.ProductRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional(readOnly = true) // Der gesamte Service – nur lesen, daher readOnly praktisch auf Klassenebene.
public class CatalogQueryService {

    private final ProductRepository productRepository;

    public CatalogQueryService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    public Page<ProductCatalogRow> findActiveCatalog(Pageable pageable) {
        // Hier geben wir keine Entity zurück: Wir liefern ein Read-Modell/Projektion, bequem für UI/Antwort.
        return productRepository.findCatalogRowsByActiveTrue(pageable);
    }
}

Beachten Sie den Stil: Hier haben wir @Transactional(readOnly = true) auf die Klasse gesetzt. Das ist praktisch für einen Query-Dienst, da fast alle seine Methoden Leseoperationen sind. Sollte eines Tages eine Methode auftauchen, die „plötzlich Daten ändert“, ist das kein Zufall, sondern eine rote Warnleuchte: Entweder ist sie nicht an ihrem Platz, oder sie muss separat als Write annotiert werden (und wahrscheinlich in einen Command-Dienst ausgelagert werden).

Jetzt ein etwas „lebendigeres“ Beispiel: Ein Read-Use-Case, der mehrere zusammenhängende Lesevorgänge durchführt. Angenommen, wir benötigen eine kleine „Widget“-Ansicht für ein Produkt: Name, Preis und verfügbare Menge. Wir sind nicht verpflichtet, dies mit einer einzigen Super-Projektion zu tun (obwohl wir es oft können), manchmal verknüpft der Service die Daten ehrlich aus zwei Repositories.

import com.example.shopdatajpa.catalog.entity.Product;
import com.example.shopdatajpa.catalog.repository.ProductRepository;
import com.example.shopdatajpa.inventory.repository.StockItemRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class ProductViewService {

    private final ProductRepository productRepository;
    private final StockItemRepository stockItemRepository;

    public ProductViewService(ProductRepository productRepository,
                              StockItemRepository stockItemRepository) {
        this.productRepository = productRepository;
        this.stockItemRepository = stockItemRepository;
    }

    @Transactional(readOnly = true)
    public int availableFor(Long productId) {
        // Wichtig: Dies ist ein Lese-Use-Case, auch wenn er mehrere Repository-Aufrufe tätigt.
        Product p = productRepository.findById(productId).orElseThrow(); // Wenn das Produkt nicht existiert, ist das ein Szenario-Fehler.
        // Daten verknüpfen: Entität aus catalog, Bestand aus inventory.
        return stockItemRepository.findAvailableQuantityByProductId(p.getId());
    }
}

Dieser Code ist bewusst einfach gehalten, um den Gedanken zu verdeutlichen: Auch Lesen kann eine „Operation“ sein, und sie hat eine Grenze. Wir machen das Repository nicht zum Orchestrator, sondern tun dies auf Service-Ebene.

Und noch eine Nuance, die wichtig zu verstehen ist. readOnly = true ist ein Hinweis. Es garantiert nicht, dass „es niemals einen Schreibvorgang geben wird“. Es hilft der Infrastruktur, zeigt dem Team die Absicht und reduziert die Wahrscheinlichkeit von „versehentlichem Schreiben“, aber wenn Sie innerhalb einer Read-Methode eine Entität ändern und save aufrufen, sind Sie Ihr eigener Feind (Spring hält Sie nicht an der Hand, es ist kein Kindermädchen). Daher ist ein Read-Dienst eine Disziplin, kein magisches Verbot.

4. Änderungsdienste: Lesen → Prüfen → Schreiben

Ein Write-Szenario sieht normalerweise wie eine kleine Geschichte aus. Zuerst lesen wir die benötigten Daten aus der DB, dann prüfen wir Geschäftsbedingungen, dann ändern wir den Zustand und schreiben das Ergebnis. Deshalb wird fast immer eine Transaktion benötigt: Wenn Sie mitten in der Geschichte abstürzen, möchten Sie das System nicht in einem „halb geänderten“ Zustand hinterlassen. Für einen Mini-Shop ist das besonders offensichtlich: Man kann keine Bestellung erstellen, ohne den Bestand zu reduzieren, und man kann den Bestand nicht reduzieren, ohne die Bestellung zu erstellen.

Beginnen wir mit einem supertypischen Fall: Preisänderung. Wichtig ist, dass der Service linear lesbar ist: Man sieht das Lesen, sieht die Änderung, sieht den Write-Schritt. Deshalb halten wir hier explizit ein save() als sichtbaren Schreibpunkt. Das ist nicht die gesamte JPA-Mechanik: Eine managed-Entität kann auch ohne explizites save() mit der DB synchronisiert werden, aber für dieses Gespräch ist es wichtiger, zuerst die Grenze des Use Cases zu sehen, nicht die Interna des Persistenzkontexts.

import com.example.shopdatajpa.catalog.entity.Product;
import com.example.shopdatajpa.catalog.repository.ProductRepository;
import java.math.BigDecimal;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class CatalogCommandService {

    private final ProductRepository productRepository;

    public CatalogCommandService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @Transactional
    public void changePrice(Long productId, BigDecimal newPrice) {
        // Schritt 1: Die benötigte Entität innerhalb der Transaktion laden.
        Product product = productRepository.findById(productId).orElseThrow();

        // Schritt 2: Domänenänderung anwenden (Geschäftsmethode in der Entität).
        product.changePrice(newPrice);

        // Schritt 3: Expliziter Schreibpunkt – damit Anfänger das Szenario linear lesen können.
        productRepository.save(product);
    }
}

Hier ist nicht die Annotation wichtig, sondern die Form: Es ist ein Write-Use-Case, er ist vollständig in einer Transaktion und nicht über verschiedene Klassen verteilt.

Jetzt ein etwas domänenbezogeneres Beispiel: Ein Produkt aktivieren. Das ist im Grunde auch eine „Zustandsänderung“, aber oft wird es irgendwie dem Lesen zugeordnet („es ist ja nur ein Flag“). Nein: Jede Datenänderung ist ein Write-Use-Case.

import com.example.shopdatajpa.catalog.entity.Product;
import com.example.shopdatajpa.catalog.repository.ProductRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class ProductLifecycleService {

    private final ProductRepository productRepository;

    public ProductLifecycleService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @Transactional
    public void activate(Long productId) {
        // Zustand lesen...
        Product product = productRepository.findById(productId).orElseThrow();

        // ...Zustand im Domänenmodell ändern...
        product.activate();

        // ... und das Ergebnis in derselben Transaktion in der DB speichern.
        productRepository.save(product);
    }
}

Und noch ein Beispiel, das gut „Lesen + Prüfen + Schreiben“ zeigt. Die Reservierung von Beständen ist nicht einfach ein „update“, sondern eine Geschäftsoperation: Wir müssen prüfen, ob genügend Ware vorhanden ist, und erst dann die Zahlen ändern.

import com.example.shopdatajpa.inventory.entity.StockItem;
import com.example.shopdatajpa.inventory.repository.StockItemRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class InventoryCommandService {

    private final StockItemRepository stockItemRepository;

    public InventoryCommandService(StockItemRepository stockItemRepository) {
        this.stockItemRepository = stockItemRepository;
    }

    @Transactional
    public void reserve(Long productId, int qty) {
        // 1) Bestandsdatensatz lesen.
        StockItem stock = stockItemRepository.findByProductId(productId).orElseThrow();

        // 2) Zustand ändern: intern Prüfungen auf qty und Verfügbarkeit.
        stock.reserve(qty);

        // 3) Änderungen speichern: Die gesamte Operation muss atomar sein.
        stockItemRepository.save(stock);
    }
}

Wenn Sie dies betrachten und denken „warum sieht alles so ähnlich aus?“, dann beginnen Sie gerade, ein gesundes Muster zu erkennen. Write-Operationen sind tatsächlich oft ähnlich: finden, prüfen, ändern, speichern. Das ist normal.

Und hier beginnt die „Reife“ der Service-Schicht (ohne unnötigen Schmerz): Eine Write-Operation sollte keine „Verknüpfung zufälliger Transaktionen“ sein. Sie sollte einen einzigen transaktionalen Rahmen haben, der am Eingang beginnt. Daher steht @Transactional normalerweise auf der Methode, die den Use Case ausdrückt, nicht auf kleinen Teilen.

5. Methoden ohne Datenbankzugriff

Mit @Transactional gibt es eine lustige Falle: Wenn man einmal verstanden hat, dass es wichtig ist, entsteht der Wunsch, „alles, was sich bewegt, mit Transaktionen zu überziehen“. Das ist wie beim ersten Kennenlernen von final, wenn man es sogar auf Variablen in for-Schleifen setzt (wir waren alle dort, keine Schande). Aber eine Transaktion wird nur dort benötigt, wo Datenzugriff stattfindet und wo atomicity/consistency gewahrt werden müssen.

Wenn eine Methode reine Mathematik ist, Zeichenkettenformatierung oder Zusammenstellung eines Werts aus Parametern, dann bringt eine Transaktion nichts als Rauschen. Mehr noch: Wenn Sie anfangen, Transaktionen um „kleine Helfer“ zu öffnen, riskieren Sie seltsame Grenzen, Self-Invocation-Effekte und verlieren einfach das Verständnis dafür, wo tatsächlich mit der DB gearbeitet wird.

Ein einfaches Beispiel ist die Berechnung der Zeilensumme einer Bestellung. Hier gibt es keinen Sinn für eine Transaktion, weil keine Interaktion mit der DB stattfindet.

import java.math.BigDecimal;

public class MoneyCalculator {

    public BigDecimal lineTotal(BigDecimal unitPrice, int quantity) {
        // Reine Funktion: keine Repositories, keine Entitäten, keine Transaktion.
        return unitPrice.multiply(BigDecimal.valueOf(quantity));
    }
}

Oder zum Beispiel die Generierung einer Bestellnummer. Das kann Geschäftslogik sein, aber nicht unbedingt transaktional. Wenn Ihr Generator nicht in die DB geht, braucht er keine Transaktion.

import java.time.Instant;

public class OrderNumberGenerator {

    public String next() {
        // Beispielgenerierung „aus dem Stegreif“: Sie greift nicht auf die DB zu, daher keine Transaktion nötig.
        long ts = Instant.now().toEpochMilli();
        return "ORD-" + ts; // Beispiel: ORD-1710000000000
    }
}

Wichtiger Punkt: Helper-Methoden werden oft innerhalb eines @Transactional Use Case aufgerufen, und das ist normal. Die Transaktion „deckt“ das gesamte Szenario von oben ab, und die Helper-Methoden arbeiten darin wie normale Methoden. Aber die Annotation auf einen Helper zu setzen, verwischt die Grenze.

Selbst wenn der Helper im selben Service ist und „logisch zusammengehörig“ erscheint, macht ihn das nicht zur Grenze des Use Case. Die Grenze des Use Case ist der Einstiegspunkt der Operation, nicht jede einzelne Zeile darin.

6. Struktur von shop-data-jpa

Wenn ein Projekt klein ist, kann man mit einem einzigen CatalogService leben, der sowohl liest als auch schreibt. Aber unser Kurs ist bewusst so aufgebaut, dass Sie sich an eine Struktur gewöhnen, die beim Wachsen nicht auseinanderfällt. Daher ist es nützlich, bereits jetzt die Gewohnheit zu entwickeln: Lesen und Ändern auf Service-Ebene zu trennen, nicht nur auf Annotationsebene.

In den Begriffen unseres package-by-feature ist das sehr natürlich. Im catalog können wir zwei Dienste haben: CatalogQueryService und CatalogCommandService. Im inventory – InventoryQueryService und InventoryCommandService. Im ordering – ähnlich, besonders wenn Sie sowohl „Bestellliste abrufen“ als auch „Bestellung aufgeben“ haben.

Hier sind minimale „Skelette“ – ohne unnötige Details, nur um die Struktur und die Grenzen zu sehen:

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional(readOnly = true)
public class InventoryQueryService {
    // nur Leseoperationen: aktueller Bestand, Low-Stock-Ansicht, Zusammenfassung
}
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class InventoryCommandService {

    @Transactional
    public void reserve(Long productId, int qty) {
        // Lesen -> Prüfen -> Ändern -> Schreiben
    }
}

In einer solchen Struktur erhalten Sie einen angenehmen Bonus: Bereits am Klassennamen ist zu erkennen, um welche „Art“ von Dienst es sich handelt. Das reduziert die kognitive Belastung, insbesondere bei Neulingen, die noch nicht gelernt haben, Annotationen schnell mit den Augen zu lesen.

Und hier knüpfen wir vorsichtig an das Thema der transactional defaults von Repositories aus der vorherigen Lektion an. Ja, Repositories können standardmäßig „ein bisschen transaktional“ sein. Aber in einem guten Projekt ist die Transaktionspolitik am besten auf Service-Ebene lesbar, weil der Service den Use Case widerspiegelt. Das Repository sollte beim Datenzugriff bleiben, nicht bei Szenarien.

Wenn Sie sich ein „ideales Bild“ der Grenzen vorstellen möchten, halten Sie die folgende mentale Folie im Kopf:

flowchart LR
    A["CatalogQueryService
readOnly"] --> B["ProductRepository
read methods"] C["CatalogCommandService
write tx"] --> B C --> D["StockItemRepository
write methods"]

Dieses Schema ist bewusst einfach: Es zeigt, dass der Service der Inhaber der Absicht und der Grenze ist und die Repositories die Werkzeuge sind.

7. Typische Fehler bei der Arbeit mit Services und Transaktionen

Fehler Nr. 1: @Transactional auf „alles Mögliche“ setzen, damit es ja funktioniert.
So entsteht normalerweise eine Service-Schicht, bei der jede Methode in einer Transaktion ist, selbst wenn sie nur eine Zeichenkette formatiert oder eine Summe berechnet. Das Problem liegt nicht so sehr in der Performance, sondern darin, dass man die Grenzen des Use Case nicht mehr versteht. Am Ende wird jedes Lesen „plötzlich“ teuer und seltsam, und die Annotation wird zum Hintergrund, den niemand mehr liest.

Fehler Nr. 2: Lesen und Ändern in einer Methode mischen, ohne explizite Absicht.
Klassisches Beispiel: Die Methode heißt getProduct(), aber „nebenbei“ aktualisiert sie updatedAt, bereinigt einige Felder oder korrigiert den Status. Das sieht nach einer harmlosen Optimierung aus, zerstört aber die Erwartungen: Lesen wird plötzlich zu Schreiben. Einen Monat später setzt jemand auf diese Methode readOnly = true, und es treten „mystische“ Fehler auf (und mystische Fehler sind die teuersten, weil man auf sie nicht wütend sein kann, sondern nur traurig).

Fehler Nr. 3: Versuchen, das Repository zum Inhaber der Geschäftsoperation zu machen.
Wenn Sie anfangen, im Repository Methoden zu schreiben, die „sowohl lesen als auch prüfen als auch summieren als auch Bestände ändern“, verlieren Sie den Hauptsinn der Layered Architecture: Das Repository wird nicht mehr zum Datenzugriff, und der Service wird zu einer dekorativen Klasse. Die Transaktionsgrenze ist dann sehr schwer an einem Ort zu halten, weil sich die „Geschäftslogik“ über die dafür nicht vorgesehene Ebene ausbreitet.

Fehler Nr. 4: Die Transaktion in einer Helper-Methode beginnen, weil „sie ja wichtig ist“.
Die Wichtigkeit einer Methode ist nicht gleichbedeutend damit, dass sie die Grenze des Use Case ist. Die Grenze des Use Case sollte am Einstiegspunkt des Szenarios liegen, wo Sie sagen können: „Wir haben die Operation gestartet“, und am Ende „wir haben sie entweder erfolgreich abgeschlossen oder zurückgerollt“. Wenn die Annotation auf einer kleinen internen Methode liegt und die äußere Methode ohne Transaktion noch weitere Aufrufe tätigt, erhalten Sie Grenzen, die selbst Ihnen schwer zu erklären sind.

Fehler Nr. 5: Einen Read-Dienst ohne readOnly erstellen und auf die Defaults des Repositories hoffen.
Technisch mag es funktionieren, insbesondere wenn der Read-Dienst nur einen Repository-Aufruf tätigt. Aber der erzieherische und teambezogene Wert von readOnly = true liegt darin, dass Sie die Absicht festhalten. Es ist wie eine Variable totalAmount statt x zu nennen: nicht weil der Compiler es sonst nicht versteht, sondern weil Menschen keine Compiler sind und Hilfe brauchen.

1
Aufgabe
Spring Data JPA, Level 19, Lektion 3
Gesperrt
Separater query-Service zum Lesen aktiver Produkte
Separater query-Service zum Lesen aktiver Produkte
1
Aufgabe
Spring Data JPA, Level 19, Lektion 3
Gesperrt
Command-Service für Preisänderung eines Produkts
Command-Service für Preisänderung eines Produkts
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION