Week 4 · Les 2

Services en relaties tussen lagen

Agenda

  1. Richtlijnen voor Services (bespreken op de site)
  2. Relaties tussen klassen bekijken
  3. Complexe custom queries over meerdere repositories
  4. Werken aan weekopgave

Voorbereiding — Onderdeel 1: Service

Verplaats veranderPrijs uit de controller naar een service.

// Vóór — in ProductController
@PatchMapping("{id}/prijs")
public void veranderPrijs(@PathVariable("id") Product product,
                          @RequestBody Geld nieuwePrijs) {
    product.setPrijs(nieuwePrijs);
    productRepository.save(product);

    bestellingRepository.findAll().forEach(bestelling -> {
        bestelling.veranderStukPrijs(AggregateReference.to(product.getId()), nieuwePrijs);
        bestellingRepository.save(bestelling);
    });
}
// Ná — delegeer naar service
@PatchMapping("{id}/prijs")
public void veranderPrijs(@PathVariable("id") Product product,
                          @RequestBody Geld nieuwePrijs) {
    productService.veranderPrijs(product, nieuwePrijs);  // ✅
}

Wat valt je op aan de vóór-versie?

Voorbereiding — Onderdeel 1: ProductService uitwerking

ProductService krijgt de repositories via constructor-injectie (@Service + final-velden).

@Service
public class ProductService {

    private final ProductRepository productRepository;
    private final BestellingRepository bestellingRepository;

    public ProductService(ProductRepository productRepository,
                          BestellingRepository bestellingRepository) {
        this.productRepository = productRepository;
        this.bestellingRepository = bestellingRepository;
    }

    public void veranderPrijs(Product product, Geld nieuwePrijs) {
        product.setPrijs(nieuwePrijs);
        productRepository.save(product);

        bestellingRepository.findAll().forEach(bestelling -> {
            bestelling.veranderStukPrijs(
                AggregateReference.to(product.getId()), nieuwePrijs);
            bestellingRepository.save(bestelling);
        });
    }
}

ProductController verwijdert zijn repository-velden — die horen nu in de service.
Kleine compile-fouten (ontbrekende imports e.d.) mag je zelf oplossen.

Voorbereiding — Onderdeel 2a: Derived Query

ProductNaamDTO teruggeeft op basis van id — zonder @Query.

// ProductNaamDTO.java
public record ProductNaamDTO(String naam) {}
// ProductRepository.java
ProductNaamDTO findProductNaamById(Integer id);
// ProductController.java — endpoint uit commentaar halen
@GetMapping("{id}/productnaam")
public ProductNaamDTO findProductNaamById(@PathVariable("id") Integer id) {
    return productRepository.findProductNaamById(id);
}

Voorbereiding — Onderdeel 2b: @Query

Vervang findAll() door een gerichte query — filter in de database.

// Probleem: findAll + Java-filter
bestellingRepository.findAll().forEach(b -> {
    b.veranderStukPrijs(AggregateReference.to(product.getId()), nieuwePrijs);
    bestellingRepository.save(b);
});
// Oplossing: @Query in BestellingRepository
@Query("SELECT * FROM bestelling b JOIN bestelregel br ON b.id = br.bestelling WHERE br.product = :productId")
List<Bestelling> findAllMetProductId(Integer productId);

Discussievraag: AND b.status = 'CONCEPT' toevoegen — wat is het nadeel?

Je mist bestellingen in andere statussen die toch een prijswijziging moeten krijgen.

2. Relaties tussen klassen — context

De bestellingapplicatie heeft vier lagen:

Laag Klasse(n)
Controller ProductController
Service ProductService
Repository BestellingRepository, ProductRepository
Domain Bestelling, Bestelregel, Product

Per opgave: is de getekende relatie ✅ toegestaan, ❌ verboden, of ⚠️ ter discussie?

Associatie vs. dependency

Twee soorten relaties tussen klassen:

Associatie --> Dependency ..>
Betekenis klasse A heeft een veld van type B klasse A gebruikt B als parameter, return-type of lokale variabele
Koppeling sterk — object leeft mee met A zwak — B verschijnt alleen tijdens een aanroep
Voorbeeld private final ProductService service; public Product findById(Integer id)
Aanmaken vaak via constructor-injectie new Product(...) of via methode-parameter

2a. Associaties omlaag — opgaven 01–04

Een associatie (-->) = klasse A heeft een veld van type B.

Bestanden: <jouw repo>/oefeningen/les-2/lesprogramma/onderdeel2/2a_associaties_omlaag/

Beoordeel per opgave: ✅ altijd toegestaan · ❌ altijd verboden · ⚠️ ter discussie

2a · Opgave 01 — Associaties omlaag

Is deze associatie ✅ toegestaan · ❌ verboden · ⚠️ ter discussie?

2a · Antwoord 01 — ✅ Associaties omlaag

ProductController --> ProductService ✅

Controller mag service als veld hebben. Afhankelijkheid gaat omlaag — gewenste richting.

2a · Opgave 02 — Associaties omlaag

Is deze associatie ✅ toegestaan · ❌ verboden · ⚠️ ter discussie?

2a · Antwoord 02 — ✅ ✅ Associaties omlaag

ProductService --> ProductRepository ✅
ProductService --> BestellingRepository ✅

Service coördineert meerdere repositories. Richting omlaag — correct.

2a · Opgave 03 — Associaties omlaag

Is deze associatie ✅ toegestaan · ❌ verboden · ⚠️ ter discussie?

2a · Antwoord 03 — ⚠️ ⚠️ Associaties omlaag

ProductController --> ProductRepository ⚠️
ProductController --> BestellingRepository ⚠️

Richting klopt, maar controller omzeilt de service. Service bestaat juist om repository-gebruik te centraliseren. Ter discussie.

2a · Opgave 04 — Associaties omlaag

Is deze associatie ✅ toegestaan · ❌ verboden · ⚠️ ter discussie?

2a · Antwoord 04 — ❌ Associaties omlaag

BestellingRepository --> Bestelling ❌

Een repository gebruikt domeinobjecten (dependency ✅), maar bezit ze niet als veld. Een associatie hier zou betekenen dat de repository één object cached — dat hoort thuis in een caching-laag, niet in het domein.

2b. Dependencies omlaag — opgaven 05–09

Een dependency (..>) = klasse A gebruikt B als parameter, return-type of lokale variabele — maar heeft géén veld van type B.

Bestanden: <jouw repo>/oefeningen/les-2/lesprogramma/onderdeel2/2b_dependencies_omlaag/

2b · Opgave 05 — Dependencies omlaag

Is deze dependency ✅ toegestaan · ❌ verboden · ⚠️ ter discussie?

2b · Antwoord 05 — ✅ Dependencies omlaag

BestellingRepository ..> Bestelling ✅

findAll() en findById() geven Bestelling-objecten terug. Dat is een dependency (return-type), géén associatie. Richting omlaag — correct.

2b · Opgave 06 — Dependencies omlaag

Is deze dependency ✅ toegestaan · ❌ verboden · ⚠️ ter discussie?

2b · Antwoord 06 — ❌ Dependencies omlaag

ProductRepository ..> Bestelling <<return>> ❌

ProductRepository hoort Product-objecten terug te geven.
Dit doorbreekt de aggregate-grenzen: de ene aggregate
lekt via de repository van de andere.

2b · Opgave 07 — Dependencies omlaag

Is deze dependency ✅ toegestaan · ❌ verboden · ⚠️ ter discussie?

2b · Antwoord 07 — Dependencies omlaag

ProductController ..> Product <<use>> ⚠️
ProductService ..> Product <<use>> ✅

Service accepteert Product als parameter — normaal.
Controller koppelt HTTP-laag direct aan domein (geen DTO).
Ter discussie voor de controller.

2b · Opgave 08 — Dependencies omlaag

Is deze dependency ✅ toegestaan · ❌ verboden · ⚠️ ter discussie?

2b · Antwoord 08 — ⚠️ ⚠️ Dependencies omlaag

ProductController ..> Product <<return>> ⚠️
ProductService ..> Product <<return>> ⚠️

Controller geeft idealiter een DTO terug zodat het
domein niet direct via de API blootgesteld wordt.
Ter discussie voor beide.

2b · Opgave 09 — Dependencies omlaag

Is deze dependency ✅ toegestaan · ❌ verboden · ⚠️ ter discussie?

2b · Antwoord 09 — Dependencies omlaag

ProductService ..> Bestelling <<use>> ✅
ProductService ..> Bestelregel <<use>> ⚠️

Werken met aggregate root Bestelling — correct.
Direct verwijzen naar Bestelregel (entity binnen de
aggregate) omzeilt de root. Ter discussie.

2c · Opgave 10 — Associaties omhoog

Is deze associatie ✅ toegestaan · ❌ verboden · ⚠️ ter discussie?

2c · Antwoord 10 — ❌ Associaties omhoog

Alle vijf zijn altijd verboden.

Lagere laag mag nooit een veld hebben van het type
van een hogere laag. Veroorzaakt circulaire
afhankelijkheden.

2d · Opgave 11 — Dependencies omhoog

Is deze dependency ✅ toegestaan · ❌ verboden · ⚠️ ter discussie?

2d · Antwoord 11 — ❌ Dependencies omhoog

Alle vijf zijn altijd verboden.

Ook tijdelijke afhankelijkheden (parameter, return-type)
van laag naar hoog doorbreken het lagenprincipe.
De hogere laag is onbekend vanuit de lagere laag.

3. Complexe custom queries

Wanneer volstaat Spring Data niet?

Aanpak Wanneer?
Afgeleide query (findByNaam) Eenvoudige zoekopdrachten op één aggregate
@Query op repository Custom SQL, joins binnen één aggregate
JdbcTemplate + ResultSetExtractor Joins over meerdere aggregates, groepering

3. Minions casus — domein

Twee aggregates: Minion en Person (evil master). (gebaseerd op blog Jens Schrauder)

center

Minion heeft een AggregateReference naar de Person-aggregate.

3a. Eenvoudige query — minions per evil master

Derived query (Spring Data leidt SQL af uit methodenaam):

Collection<Minion> findByEvilMaster(AggregateReference<Person, Integer> id);

Custom @Query (zelfde resultaat, expliciete SQL):

@Query("SELECT * FROM MINION WHERE EVIL_MASTER = :id")
Collection<Minion> findByEvilMaster(Integer id);

Met DTO — alleen naam teruggeven:

@Query("""
    SELECT m.name as minion_name, p.name as evil_master_name
    FROM minion m JOIN person p ON p.id = m.evil_master
    WHERE p.id = :id
    """)
Collection<MinionWithEvilMasterDTO> findByEvilMaster(Integer id);

3b. Probleem — één Person met alle Minions

Resultaat van de JOIN heeft meerdere rijen voor één Person:

name minion_name
Scarlet Bob
Scarlet Stuart
Scarlet Kevin

Spring Data kan dit niet automatisch samenvoegen tot één DTO.

3. Opdracht — Minions casus

Bestanden: lesmateriaal/3_complexere_custom_query/minions/

  1. Start de applicatie en bekijk de data via de endpoints in http/.
  2. Derived query: activeer findByEvilMaster(AggregateReference<Person, Integer> id) in MinionRepository en test het.
  3. @Query: activeer de versie met @Query + MinionWithEvilMasterDTO. Wat verandert er aan het resultaat?
  4. ResultSetExtractor: bekijk PersonWithMinionsExtractor in PersonRepository. Leg uit waarom hier niet een gewone @Query volstaat.
  5. Bonus: schrijf zelf een ResultSetExtractor die alle evil masters met hun minions teruggeeft (dus een List<PersonWithMinionsDTO>).
    Tip: voeg de uitwerking toe als extra methode in PersonRepository.java — gebruik de bestaande PersonWithMinionsExtractor als referentie.

3b. Oplossing — ResultSetExtractor

class PersonWithMinionsExtractor implements ResultSetExtractor<PersonWithMinionsDTO> {
    @Override
    public PersonWithMinionsDTO extractData(ResultSet rs)
            throws SQLException, DataAccessException {
        rs.next();
        String personName = rs.getString("name");
        List<String> minionNames = new ArrayList<>();
        minionNames.add(rs.getString("minion_name"));
        while (rs.next()) {
            minionNames.add(rs.getString("minion_name"));
        }
        return new PersonWithMinionsDTO(personName, minionNames);
    }
}

@Query(value = """
    select p.name, m.name as minion_name
    from person p join minion m on p.id = m.evil_master
    where p.id = :id
    """, resultSetExtractorClass = PersonWithMinionsExtractor.class)
PersonWithMinionsDTO findByIdWithMinions(Integer id);