From 520d62a6d7aa3923ada74c64a667084d8d86ed35 Mon Sep 17 00:00:00 2001 From: Ralf Ueberfuhr Date: Thu, 20 Jun 2024 15:36:22 +0200 Subject: [PATCH 01/10] Introduce Persistence Layer. --- customer-api-provider/pom.xml | 8 +++ .../quarkus/persistence/CustomerEntity.java | 30 +++++++++ .../persistence/CustomerEntityMapper.java | 16 +++++ .../persistence/CustomerEntityRepository.java | 8 +++ .../persistence/CustomersSinkPanacheImpl.java | 66 +++++++++++++++++++ 5 files changed, 128 insertions(+) create mode 100644 customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomerEntity.java create mode 100644 customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomerEntityMapper.java create mode 100644 customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomerEntityRepository.java create mode 100644 customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomersSinkPanacheImpl.java diff --git a/customer-api-provider/pom.xml b/customer-api-provider/pom.xml index 1646e2f..5946dde 100644 --- a/customer-api-provider/pom.xml +++ b/customer-api-provider/pom.xml @@ -84,6 +84,14 @@ io.quarkus quarkus-hibernate-validator + + io.quarkus + quarkus-hibernate-orm-panache + + + io.quarkus + quarkus-jdbc-h2 + diff --git a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomerEntity.java b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomerEntity.java new file mode 100644 index 0000000..b612b45 --- /dev/null +++ b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomerEntity.java @@ -0,0 +1,30 @@ +package de.schulung.sample.quarkus.persistence; + +import de.schulung.sample.quarkus.domain.Customer; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDate; +import java.util.UUID; + +@Getter +@Setter +@Entity(name = "Customer") +@Table(name = "CUSTOMERS") +public class CustomerEntity { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID uuid; + @Size(min = 3, max = 100) + @NotNull + private String name; + @Column(name = "DATE_OF_BIRTH") + private LocalDate birthdate; + @NotNull + private Customer.CustomerState state = Customer.CustomerState.ACTIVE; + +} diff --git a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomerEntityMapper.java b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomerEntityMapper.java new file mode 100644 index 0000000..cbe19d0 --- /dev/null +++ b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomerEntityMapper.java @@ -0,0 +1,16 @@ +package de.schulung.sample.quarkus.persistence; + +import de.schulung.sample.quarkus.domain.Customer; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; + +@Mapper(componentModel = "cdi") +public interface CustomerEntityMapper { + + CustomerEntity map(Customer source); + + Customer map(CustomerEntity source); + + void copy(CustomerEntity source, @MappingTarget Customer target); + +} diff --git a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomerEntityRepository.java b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomerEntityRepository.java new file mode 100644 index 0000000..a7e9ae4 --- /dev/null +++ b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomerEntityRepository.java @@ -0,0 +1,8 @@ +package de.schulung.sample.quarkus.persistence; + +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class CustomerEntityRepository implements PanacheRepository { +} diff --git a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomersSinkPanacheImpl.java b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomersSinkPanacheImpl.java new file mode 100644 index 0000000..551d21e --- /dev/null +++ b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomersSinkPanacheImpl.java @@ -0,0 +1,66 @@ +package de.schulung.sample.quarkus.persistence; + +import de.schulung.sample.quarkus.domain.Customer; +import de.schulung.sample.quarkus.domain.CustomersSink; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Typed; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; + +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Stream; + +@ApplicationScoped +@Typed(CustomersSink.class) +@RequiredArgsConstructor +public class CustomersSinkPanacheImpl implements CustomersSink { + + private final CustomerEntityRepository repo; + private final CustomerEntityMapper mapper; + + @Override + public Stream findAll() { + return repo.listAll() + .stream() + .map(mapper::map); + } + + @Override + public Stream findByState(Customer.CustomerState state) { + // TODO + return CustomersSink.super.findByState(state); + } + + @Override + public Optional findByUuid(UUID uuid) { + // TODO findById mit UUID? + return CustomersSink.super.findByUuid(uuid); + } + + @Override + @Transactional + public void save(Customer customer) { + var entity = this.mapper.map(customer); + repo.persist(entity); + //customer.setUuid(entity.getUuid()); + mapper.copy(entity, customer); + } + + @Override + public boolean delete(UUID uuid) { + // TODO delete by UUID + return false; + } + + @Override + public boolean exists(UUID uuid) { + // TODO exists by UUID + return CustomersSink.super.exists(uuid); + } + + @Override + public long count() { + return repo.count(); + } +} From 0595bbd9b1a120d1d5b9589bba27dc23b50591ae Mon Sep 17 00:00:00 2001 From: Ralf Ueberfuhr Date: Thu, 20 Jun 2024 19:14:13 +0200 Subject: [PATCH 02/10] Add support for UUID. --- .../persistence/CustomerEntityRepository.java | 6 +++-- .../persistence/CustomersSinkPanacheImpl.java | 16 ++++++------ .../src/main/resources/application.properties | 9 ++++++- .../PersistenceCustomersSinkTests.java | 25 +++++++++++++++++++ 4 files changed, 45 insertions(+), 11 deletions(-) create mode 100644 customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/PersistenceCustomersSinkTests.java diff --git a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomerEntityRepository.java b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomerEntityRepository.java index a7e9ae4..63a3d33 100644 --- a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomerEntityRepository.java +++ b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomerEntityRepository.java @@ -1,8 +1,10 @@ package de.schulung.sample.quarkus.persistence; -import io.quarkus.hibernate.orm.panache.PanacheRepository; +import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; import jakarta.enterprise.context.ApplicationScoped; +import java.util.UUID; + @ApplicationScoped -public class CustomerEntityRepository implements PanacheRepository { +public class CustomerEntityRepository implements PanacheRepositoryBase { } diff --git a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomersSinkPanacheImpl.java b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomersSinkPanacheImpl.java index 551d21e..5441ce6 100644 --- a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomersSinkPanacheImpl.java +++ b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomersSinkPanacheImpl.java @@ -28,14 +28,15 @@ public Stream findAll() { @Override public Stream findByState(Customer.CustomerState state) { - // TODO - return CustomersSink.super.findByState(state); + return repo.list("state", state) + .stream() + .map(mapper::map); } @Override public Optional findByUuid(UUID uuid) { - // TODO findById mit UUID? - return CustomersSink.super.findByUuid(uuid); + return repo.findByIdOptional(uuid) + .map(mapper::map); } @Override @@ -48,15 +49,14 @@ public void save(Customer customer) { } @Override + @Transactional public boolean delete(UUID uuid) { - // TODO delete by UUID - return false; + return this.repo.deleteById(uuid); } @Override public boolean exists(UUID uuid) { - // TODO exists by UUID - return CustomersSink.super.exists(uuid); + return repo.findByIdOptional(uuid).isPresent(); } @Override diff --git a/customer-api-provider/src/main/resources/application.properties b/customer-api-provider/src/main/resources/application.properties index c601ba1..c7ca55d 100644 --- a/customer-api-provider/src/main/resources/application.properties +++ b/customer-api-provider/src/main/resources/application.properties @@ -8,4 +8,11 @@ customers.initialization.enabled=false %dev.customers.initialization.enabled=true customers.initialization.sample.name=Max -%dev.customers.initialization.sample.name=Julia \ No newline at end of file +%dev.customers.initialization.sample.name=Julia + +# for %test -> use in-memory +%dev.quarkus.datasource.db-kind=h2 +%dev.quarkus.datasource.jdbc.url=jdbc:h2:./.local-db/customers +%dev.quarkus.hibernate-orm.database.generation=update + + diff --git a/customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/PersistenceCustomersSinkTests.java b/customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/PersistenceCustomersSinkTests.java new file mode 100644 index 0000000..4d2c1e2 --- /dev/null +++ b/customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/PersistenceCustomersSinkTests.java @@ -0,0 +1,25 @@ +package de.schulung.sample.quarkus.persistence; + +import de.schulung.sample.quarkus.domain.Customer; +import de.schulung.sample.quarkus.domain.CustomersSink; +import io.quarkus.test.TestTransaction; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@QuarkusTest +@TestTransaction +public class PersistenceCustomersSinkTests { + + @Inject + CustomersSink sink; + + @Test + void shouldFindByState() { + var result = sink.findByState(Customer.CustomerState.ACTIVE); + assertThat(result).isNotNull(); + } + +} From 5a301fa5252364848d69c81701b2b3a6bb7ce1ed Mon Sep 17 00:00:00 2001 From: Ralf Ueberfuhr Date: Thu, 20 Jun 2024 19:26:35 +0200 Subject: [PATCH 03/10] Optimize tests --- .../persistence/CustomersSinkPanacheImpl.java | 5 +++++ .../src/main/resources/application.properties | 4 ++++ .../PersistenceCustomersSinkTests.java | 2 ++ .../persistence/UsePanacheImplementation.java | 15 +++++++++++++++ 4 files changed, 26 insertions(+) create mode 100644 customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/UsePanacheImplementation.java diff --git a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomersSinkPanacheImpl.java b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomersSinkPanacheImpl.java index 5441ce6..d81c28e 100644 --- a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomersSinkPanacheImpl.java +++ b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomersSinkPanacheImpl.java @@ -2,6 +2,7 @@ import de.schulung.sample.quarkus.domain.Customer; import de.schulung.sample.quarkus.domain.CustomersSink; +import io.quarkus.arc.properties.IfBuildProperty; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Typed; import jakarta.transaction.Transactional; @@ -14,6 +15,10 @@ @ApplicationScoped @Typed(CustomersSink.class) @RequiredArgsConstructor +@IfBuildProperty( + name = "persistence.sink.implementation", + stringValue = "panache" +) public class CustomersSinkPanacheImpl implements CustomersSink { private final CustomerEntityRepository repo; diff --git a/customer-api-provider/src/main/resources/application.properties b/customer-api-provider/src/main/resources/application.properties index c7ca55d..b200a40 100644 --- a/customer-api-provider/src/main/resources/application.properties +++ b/customer-api-provider/src/main/resources/application.properties @@ -10,9 +10,13 @@ customers.initialization.enabled=false customers.initialization.sample.name=Max %dev.customers.initialization.sample.name=Julia +persistence.sink.implementation=panache +%test.persistence.sink.implementation=in-memory + # for %test -> use in-memory %dev.quarkus.datasource.db-kind=h2 %dev.quarkus.datasource.jdbc.url=jdbc:h2:./.local-db/customers %dev.quarkus.hibernate-orm.database.generation=update + diff --git a/customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/PersistenceCustomersSinkTests.java b/customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/PersistenceCustomersSinkTests.java index 4d2c1e2..623c4d7 100644 --- a/customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/PersistenceCustomersSinkTests.java +++ b/customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/PersistenceCustomersSinkTests.java @@ -4,6 +4,7 @@ import de.schulung.sample.quarkus.domain.CustomersSink; import io.quarkus.test.TestTransaction; import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; import jakarta.inject.Inject; import org.junit.jupiter.api.Test; @@ -11,6 +12,7 @@ @QuarkusTest @TestTransaction +@TestProfile(UsePanacheImplementation.class) public class PersistenceCustomersSinkTests { @Inject diff --git a/customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/UsePanacheImplementation.java b/customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/UsePanacheImplementation.java new file mode 100644 index 0000000..5f3489d --- /dev/null +++ b/customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/UsePanacheImplementation.java @@ -0,0 +1,15 @@ +package de.schulung.sample.quarkus.persistence; + +import io.quarkus.test.junit.QuarkusTestProfile; + +import java.util.Map; + +public class UsePanacheImplementation implements QuarkusTestProfile { + + @Override + public Map getConfigOverrides() { + return Map.of( + "persistence.sink.implementation", "panache" + ); + } +} From a548534609f38596debc5a651245326d0403986b Mon Sep 17 00:00:00 2001 From: Ralf Ueberfuhr Date: Thu, 20 Jun 2024 20:16:08 +0200 Subject: [PATCH 04/10] Add liveness and readyness --- customer-api-provider/pom.xml | 4 ++ .../infrastructure/LongRunningStartup.java | 70 +++++++++++++++++++ .../src/main/resources/application.properties | 11 +++ 3 files changed, 85 insertions(+) create mode 100644 customer-api-provider/src/main/java/de/schulung/sample/quarkus/infrastructure/LongRunningStartup.java diff --git a/customer-api-provider/pom.xml b/customer-api-provider/pom.xml index 5946dde..0cb2938 100644 --- a/customer-api-provider/pom.xml +++ b/customer-api-provider/pom.xml @@ -92,6 +92,10 @@ io.quarkus quarkus-jdbc-h2 + + io.quarkus + quarkus-smallrye-health + diff --git a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/infrastructure/LongRunningStartup.java b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/infrastructure/LongRunningStartup.java new file mode 100644 index 0000000..973e964 --- /dev/null +++ b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/infrastructure/LongRunningStartup.java @@ -0,0 +1,70 @@ +package de.schulung.sample.quarkus.infrastructure; + +import io.quarkus.runtime.Startup; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.infrastructure.Infrastructure; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import org.eclipse.microprofile.health.HealthCheck; +import org.eclipse.microprofile.health.HealthCheckResponse; +import org.eclipse.microprofile.health.Liveness; +import org.eclipse.microprofile.health.Readiness; + +@ApplicationScoped +public class LongRunningStartup { + + private volatile boolean initialized = false; + private volatile boolean initializingFailed = false; + + // check during startup: http://localhost:9090/q/health/ready + + private Uni doLongRunningInitialization() { + try { + Thread.sleep(5000); + initialized = true; + return Uni.createFrom().voidItem(); + } catch (InterruptedException e) { + initializingFailed = true; + return Uni.createFrom().failure(e); + } + } + + @Startup + public void init() { + Uni + .createFrom() + .voidItem() + .emitOn(Infrastructure.getDefaultWorkerPool()) + .subscribe() + .with(v -> this.doLongRunningInitialization()); + } + + @Produces + @ApplicationScoped + @Liveness + public HealthCheck myStartupLiveness() { + return new HealthCheck() { + @Override + public HealthCheckResponse call() { + final var result = HealthCheckResponse + .named("My long-running startup"); + return (initializingFailed ? result.down() : result.up()).build(); + } + }; + } + + @Produces + @ApplicationScoped + @Readiness + public HealthCheck myStartupReadyness() { + return new HealthCheck() { + @Override + public HealthCheckResponse call() { + final var result = HealthCheckResponse + .named("My long-running startup"); + return (initialized ? result.up() : result.down()).build(); + } + }; + } + +} diff --git a/customer-api-provider/src/main/resources/application.properties b/customer-api-provider/src/main/resources/application.properties index b200a40..714055f 100644 --- a/customer-api-provider/src/main/resources/application.properties +++ b/customer-api-provider/src/main/resources/application.properties @@ -19,4 +19,15 @@ persistence.sink.implementation=panache %dev.quarkus.hibernate-orm.database.generation=update +# Observability - serve on another port +# https://quarkus.io/guides/management-interface-reference +# https://quarkus.io/guides/smallrye-health +# https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ +# https://developers.redhat.com/blog/2020/11/10/you-probably-need-liveness-and-readiness-probes +quarkus.management.enabled=true +quarkus.management.port=9090 +%dev.quarkus.management.host=localhost +quarkus.datasource.health.enabled=true +# Liveness: if fails -> container must be restarted +# Readyness: if fails -> container is just initializing \ No newline at end of file From 5c46e51daaedef9eda6152080ed8099725300594 Mon Sep 17 00:00:00 2001 From: Ralf Ueberfuhr Date: Fri, 21 Jun 2024 09:53:22 +0200 Subject: [PATCH 05/10] Update docs --- docs/06-mandantenfaehigkeit.png | Bin 0 -> 19893 bytes docs/07-jdbc-jpa-panache.png | Bin 0 -> 20672 bytes docs/README.md | 7 ++++++- 3 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 docs/06-mandantenfaehigkeit.png create mode 100644 docs/07-jdbc-jpa-panache.png diff --git a/docs/06-mandantenfaehigkeit.png b/docs/06-mandantenfaehigkeit.png new file mode 100644 index 0000000000000000000000000000000000000000..049f444657876164628abca0730c6b3951afe861 GIT binary patch literal 19893 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 KpUXO@geCw&%SP+~ literal 0 HcmV?d00001 diff --git a/docs/07-jdbc-jpa-panache.png b/docs/07-jdbc-jpa-panache.png new file mode 100644 index 0000000000000000000000000000000000000000..590ff3ed6052371034f2f7fa6774d986e26856a5 GIT binary patch literal 20672 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

ePF>CTD3 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 Date: Fri, 21 Jun 2024 10:37:05 +0200 Subject: [PATCH 06/10] Add JPA and JDBC implementations --- .../persistence/CustomersSinkJdbcImpl.java | 92 +++++++++++++++++++ .../persistence/CustomersSinkJpaImpl.java | 85 +++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomersSinkJdbcImpl.java create mode 100644 customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomersSinkJpaImpl.java diff --git a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomersSinkJdbcImpl.java b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomersSinkJdbcImpl.java new file mode 100644 index 0000000..21d5f7f --- /dev/null +++ b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomersSinkJdbcImpl.java @@ -0,0 +1,92 @@ +package de.schulung.sample.quarkus.persistence; + +import de.schulung.sample.quarkus.domain.Customer; +import de.schulung.sample.quarkus.domain.CustomersSink; +import io.quarkus.arc.properties.IfBuildProperty; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Typed; +import lombok.RequiredArgsConstructor; + +import javax.sql.DataSource; +import java.sql.*; +import java.time.LocalDate; +import java.util.Collection; +import java.util.LinkedList; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Stream; + +@ApplicationScoped +@Typed(CustomersSink.class) +@RequiredArgsConstructor +@IfBuildProperty( + name = "persistence.sink.implementation", + stringValue = "jdbc" +) +public class CustomersSinkJdbcImpl implements CustomersSink { + + private final DataSource ds; + + private static LocalDate convert(Date date) { + return Optional.ofNullable(date) + .map(Date::toLocalDate) + .orElse(null); + } + + @Override + public Stream findAll() { + try(Connection con = ds.getConnection(); + Statement stmt = con.createStatement(); + ResultSet rs = stmt.executeQuery("select * from CUSTOMERS")) { + + Collection result = new LinkedList<>(); + + while(rs.next()) { + + var customer = Customer.builder() + .uuid(UUID.fromString(rs.getString("UUID"))) + .birthdate(convert(rs.getDate("DATE_OF_BIRTH"))) + .name(rs.getString("NAME")) + //.state(rs.?) + .build(); + result.add(customer); + + } + + return result.stream(); + + } catch (SQLException e) { + throw new RuntimeException(e); // eigene Exception? + } + } + + @Override + public Stream findByState(Customer.CustomerState state) { + return CustomersSink.super.findByState(state); + } + + @Override + public Optional findByUuid(UUID uuid) { + return CustomersSink.super.findByUuid(uuid); + } + + @Override + public void save(Customer customer) { + + } + + @Override + public boolean delete(UUID uuid) { + return false; + } + + @Override + public boolean exists(UUID uuid) { + return CustomersSink.super.exists(uuid); + } + + @Override + public long count() { + return 0; + } +} diff --git a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomersSinkJpaImpl.java b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomersSinkJpaImpl.java new file mode 100644 index 0000000..cccdfbf --- /dev/null +++ b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomersSinkJpaImpl.java @@ -0,0 +1,85 @@ +package de.schulung.sample.quarkus.persistence; + +import de.schulung.sample.quarkus.domain.Customer; +import de.schulung.sample.quarkus.domain.CustomersSink; +import io.quarkus.arc.properties.IfBuildProperty; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Typed; +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; + +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Stream; + +@ApplicationScoped +@Typed(CustomersSink.class) +@RequiredArgsConstructor +@IfBuildProperty( + name = "persistence.sink.implementation", + stringValue = "jpa" +) +public class CustomersSinkJpaImpl implements CustomersSink { + + private final CustomerEntityMapper mapper; + private final EntityManager em; + + @Override + public Stream findAll() { + return em.createQuery( + "select c from Customer c", + CustomerEntity.class + ) + .getResultList() + .stream() + .map(mapper::map); + } + + @Override + public Stream findByState(Customer.CustomerState state) { + return em.createQuery( + "select c from Customer c where c.state = :state", + CustomerEntity.class + ) + .setParameter("state", state) + .getResultList() + .stream() + .map(mapper::map); + } + + @Override + public Optional findByUuid(UUID uuid) { + return Optional + .ofNullable(em.find(CustomerEntity.class, uuid)) + .map(mapper::map); + } + + @Override + public void save(Customer customer) { + var entity = this.mapper.map(customer); + em.persist(entity); + //customer.setUuid(entity.getUuid()); + mapper.copy(entity, customer); + } + + @Override + public boolean delete(UUID uuid) { + var found = em.find(CustomerEntity.class, uuid); + if(found == null) { + return false; + } + em.remove(found); + return true; + } + + @Override + public boolean exists(UUID uuid) { + var found = em.find(CustomerEntity.class, uuid); + return found != null; + } + + @Override + public long count() { + return 0; // TODO ?? + } +} From 2a933338c1e98d65e31780d60cb439791c4ecb01 Mon Sep 17 00:00:00 2001 From: Ralf Ueberfuhr Date: Fri, 21 Jun 2024 11:08:33 +0200 Subject: [PATCH 07/10] Add JPA and JDBC implementations --- .../persistence/CustomersSinkJdbcImpl.java | 170 +++++++++++++++--- .../PersistenceJdbcCustomersSinkTests.java | 27 +++ .../PersistenceJpaCustomersSinkTests.java | 27 +++ ...PersistencePanacheCustomersSinkTests.java} | 2 +- .../persistence/UseJdbcImplementation.java | 15 ++ .../persistence/UseJpaImplementation.java | 15 ++ 6 files changed, 228 insertions(+), 28 deletions(-) create mode 100644 customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/PersistenceJdbcCustomersSinkTests.java create mode 100644 customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/PersistenceJpaCustomersSinkTests.java rename customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/{PersistenceCustomersSinkTests.java => PersistencePanacheCustomersSinkTests.java} (92%) create mode 100644 customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/UseJdbcImplementation.java create mode 100644 customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/UseJpaImplementation.java diff --git a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomersSinkJdbcImpl.java b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomersSinkJdbcImpl.java index 21d5f7f..aa44708 100644 --- a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomersSinkJdbcImpl.java +++ b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomersSinkJdbcImpl.java @@ -1,6 +1,7 @@ package de.schulung.sample.quarkus.persistence; import de.schulung.sample.quarkus.domain.Customer; +import de.schulung.sample.quarkus.domain.Customer.CustomerState; import de.schulung.sample.quarkus.domain.CustomersSink; import io.quarkus.arc.properties.IfBuildProperty; import jakarta.enterprise.context.ApplicationScoped; @@ -27,66 +28,181 @@ public class CustomersSinkJdbcImpl implements CustomersSink { private final DataSource ds; - private static LocalDate convert(Date date) { + /* ******************************************************* * + * Converter methods - could be converter objects instead * + * ******************************************************* */ + + private static UUID convertUuid(String uuid) { + return Optional.ofNullable(uuid) + .map(UUID::fromString) + .orElse(null); + } + + private static String convertUuid(UUID uuid) { + return Optional.ofNullable(uuid) + .map(UUID::toString) + .orElse(null); + } + + private static LocalDate convertDate(Date date) { return Optional.ofNullable(date) .map(Date::toLocalDate) .orElse(null); } - @Override - public Stream findAll() { - try(Connection con = ds.getConnection(); - Statement stmt = con.createStatement(); - ResultSet rs = stmt.executeQuery("select * from CUSTOMERS")) { + private static Date convertDate(LocalDate date) { + return Optional.ofNullable(date) + .map(Date::valueOf) + .orElse(null); + } - Collection result = new LinkedList<>(); + private static CustomerState convertState(int value) { + return CustomerState.values()[value]; + } - while(rs.next()) { + private static int convertState(CustomerState value) { + return Optional.ofNullable(value) + .map(CustomerState::ordinal) + .orElse(0); - var customer = Customer.builder() - .uuid(UUID.fromString(rs.getString("UUID"))) - .birthdate(convert(rs.getDate("DATE_OF_BIRTH"))) - .name(rs.getString("NAME")) - //.state(rs.?) - .build(); - result.add(customer); + } - } + /* ******************************************************* * + * Row Mapping - could be a RowMapper object instead * + * ******************************************************* */ + + private static Customer readSingle(ResultSet rs) throws SQLException { + return Customer.builder() + .uuid(convertUuid(rs.getString("UUID"))) + .birthdate(convertDate(rs.getDate("DATE_OF_BIRTH"))) + .name(rs.getString("NAME")) + .state(convertState(rs.getInt("STATE"))) + .build(); + } + + private static Stream readAll(ResultSet rs) throws SQLException { + Collection result = new LinkedList<>(); + while (rs.next()) { + result.add(readSingle(rs)); + } + return result.stream(); + } + + /* ******************************************************* * + * CustomersSink implementation * + * ******************************************************* */ + + @Override + public Stream findAll() { + try (Connection con = ds.getConnection(); + Statement stmt = con.createStatement(); + ResultSet rs = stmt.executeQuery( + "select * from CUSTOMERS" + )) { - return result.stream(); + return readAll(rs); } catch (SQLException e) { - throw new RuntimeException(e); // eigene Exception? + throw new RuntimeException(e); // eigene Exception? } } @Override - public Stream findByState(Customer.CustomerState state) { - return CustomersSink.super.findByState(state); + public Stream findByState(CustomerState state) { + try (Connection con = ds.getConnection(); + PreparedStatement stmt = con.prepareStatement( + "select * from CUSTOMERS where STATE=?" + )) { + + stmt.setInt(1, convertState(state)); + + try (ResultSet rs = stmt.executeQuery()) { + return readAll(rs); + } + + } catch (SQLException e) { + throw new RuntimeException(e); // eigene Exception? + } } @Override public Optional findByUuid(UUID uuid) { - return CustomersSink.super.findByUuid(uuid); + try (Connection con = ds.getConnection(); + PreparedStatement stmt = con.prepareStatement( + "select * from CUSTOMERS where UUID=?" + )) { + + stmt.setString(1, convertUuid(uuid)); + + try (ResultSet rs = stmt.executeQuery()) { + if (!rs.next()) { + return Optional.empty(); + } + return Optional.of(readSingle(rs)); + } + + } catch (SQLException e) { + throw new RuntimeException(e); // eigene Exception? + } } @Override public void save(Customer customer) { + // TODO only insert, we need an update too, if the UUID is already set + try (Connection con = ds.getConnection(); + PreparedStatement stmt = con.prepareStatement( + "insert into CUSTOMERS(UUID,NAME,DATE_OF_BIRTH,STATE) values(?,?,?,?)", + Statement.RETURN_GENERATED_KEYS + )) { + + stmt.setString(1, convertUuid(customer.getUuid())); + stmt.setString(2, customer.getName()); + stmt.setDate(3, convertDate(customer.getBirthdate())); + stmt.setInt(4, convertState(customer.getState())); + stmt.executeUpdate(); + + try (ResultSet rs = stmt.getGeneratedKeys()) { + if (!rs.next()) { + throw new RuntimeException("not expected"); // bessere Exception + } + customer.setUuid(convertUuid(rs.getString(1))); + } + } catch (SQLException e) { + throw new RuntimeException(e); // eigene Exception? + } } @Override public boolean delete(UUID uuid) { - return false; - } + try (Connection con = ds.getConnection(); + PreparedStatement stmt = con.prepareStatement( + "delete from CUSTOMERS where UUID=?" + )) { - @Override - public boolean exists(UUID uuid) { - return CustomersSink.super.exists(uuid); + stmt.setString(1, convertUuid(uuid)); + return stmt.executeUpdate() > 0; + + } catch (SQLException e) { + throw new RuntimeException(e); // eigene Exception? + } } @Override public long count() { - return 0; + try (Connection con = ds.getConnection(); + Statement stmt = con.createStatement(); + ResultSet rs = stmt.executeQuery( + "select count(uuid) from CUSTOMERS" + )) { + + if (!rs.next()) { + return 0; + } + return rs.getLong(1); + + } catch (SQLException e) { + throw new RuntimeException(e); // eigene Exception? + } } } diff --git a/customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/PersistenceJdbcCustomersSinkTests.java b/customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/PersistenceJdbcCustomersSinkTests.java new file mode 100644 index 0000000..06ce24e --- /dev/null +++ b/customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/PersistenceJdbcCustomersSinkTests.java @@ -0,0 +1,27 @@ +package de.schulung.sample.quarkus.persistence; + +import de.schulung.sample.quarkus.domain.Customer; +import de.schulung.sample.quarkus.domain.CustomersSink; +import io.quarkus.test.TestTransaction; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@QuarkusTest +@TestTransaction +@TestProfile(UseJdbcImplementation.class) +public class PersistenceJdbcCustomersSinkTests { + + @Inject + CustomersSink sink; + + @Test + void shouldFindByState() { + var result = sink.findByState(Customer.CustomerState.ACTIVE); + assertThat(result).isNotNull(); + } + +} diff --git a/customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/PersistenceJpaCustomersSinkTests.java b/customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/PersistenceJpaCustomersSinkTests.java new file mode 100644 index 0000000..232f5a7 --- /dev/null +++ b/customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/PersistenceJpaCustomersSinkTests.java @@ -0,0 +1,27 @@ +package de.schulung.sample.quarkus.persistence; + +import de.schulung.sample.quarkus.domain.Customer; +import de.schulung.sample.quarkus.domain.CustomersSink; +import io.quarkus.test.TestTransaction; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@QuarkusTest +@TestTransaction +@TestProfile(UseJpaImplementation.class) +public class PersistenceJpaCustomersSinkTests { + + @Inject + CustomersSink sink; + + @Test + void shouldFindByState() { + var result = sink.findByState(Customer.CustomerState.ACTIVE); + assertThat(result).isNotNull(); + } + +} diff --git a/customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/PersistenceCustomersSinkTests.java b/customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/PersistencePanacheCustomersSinkTests.java similarity index 92% rename from customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/PersistenceCustomersSinkTests.java rename to customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/PersistencePanacheCustomersSinkTests.java index 623c4d7..780a549 100644 --- a/customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/PersistenceCustomersSinkTests.java +++ b/customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/PersistencePanacheCustomersSinkTests.java @@ -13,7 +13,7 @@ @QuarkusTest @TestTransaction @TestProfile(UsePanacheImplementation.class) -public class PersistenceCustomersSinkTests { +public class PersistencePanacheCustomersSinkTests { @Inject CustomersSink sink; diff --git a/customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/UseJdbcImplementation.java b/customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/UseJdbcImplementation.java new file mode 100644 index 0000000..786f2b6 --- /dev/null +++ b/customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/UseJdbcImplementation.java @@ -0,0 +1,15 @@ +package de.schulung.sample.quarkus.persistence; + +import io.quarkus.test.junit.QuarkusTestProfile; + +import java.util.Map; + +public class UseJdbcImplementation implements QuarkusTestProfile { + + @Override + public Map getConfigOverrides() { + return Map.of( + "persistence.sink.implementation", "jdbc" + ); + } +} diff --git a/customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/UseJpaImplementation.java b/customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/UseJpaImplementation.java new file mode 100644 index 0000000..e86c6cb --- /dev/null +++ b/customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/UseJpaImplementation.java @@ -0,0 +1,15 @@ +package de.schulung.sample.quarkus.persistence; + +import io.quarkus.test.junit.QuarkusTestProfile; + +import java.util.Map; + +public class UseJpaImplementation implements QuarkusTestProfile { + + @Override + public Map getConfigOverrides() { + return Map.of( + "persistence.sink.implementation", "jpa" + ); + } +} From 896d20105a4fa7e0e479fe739c847d3a15bf9912 Mon Sep 17 00:00:00 2001 From: Ralf Ueberfuhr Date: Fri, 21 Jun 2024 11:19:55 +0200 Subject: [PATCH 08/10] Fix JDBC implementation --- .../quarkus/persistence/CustomersSinkJdbcImpl.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomersSinkJdbcImpl.java b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomersSinkJdbcImpl.java index aa44708..f0ccf2a 100644 --- a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomersSinkJdbcImpl.java +++ b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomersSinkJdbcImpl.java @@ -151,14 +151,13 @@ public void save(Customer customer) { // TODO only insert, we need an update too, if the UUID is already set try (Connection con = ds.getConnection(); PreparedStatement stmt = con.prepareStatement( - "insert into CUSTOMERS(UUID,NAME,DATE_OF_BIRTH,STATE) values(?,?,?,?)", + "insert into CUSTOMERS(NAME,DATE_OF_BIRTH,STATE) values(?,?,?)", Statement.RETURN_GENERATED_KEYS )) { - stmt.setString(1, convertUuid(customer.getUuid())); - stmt.setString(2, customer.getName()); - stmt.setDate(3, convertDate(customer.getBirthdate())); - stmt.setInt(4, convertState(customer.getState())); + stmt.setString(1, customer.getName()); + stmt.setDate(2, convertDate(customer.getBirthdate())); + stmt.setInt(3, convertState(customer.getState())); stmt.executeUpdate(); try (ResultSet rs = stmt.getGeneratedKeys()) { From 7257cd89eb1d8ca1223739ad5fdb0ddf764286f6 Mon Sep 17 00:00:00 2001 From: Ralf Ueberfuhr Date: Fri, 21 Jun 2024 11:33:35 +0200 Subject: [PATCH 09/10] Introduce customer state converter --- .../quarkus/persistence/CustomerEntity.java | 1 + .../persistence/CustomerStateConverter.java | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomerStateConverter.java diff --git a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomerEntity.java b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomerEntity.java index b612b45..739065b 100644 --- a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomerEntity.java +++ b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomerEntity.java @@ -25,6 +25,7 @@ public class CustomerEntity { @Column(name = "DATE_OF_BIRTH") private LocalDate birthdate; @NotNull + //@Enumerated(EnumType.STRING) private Customer.CustomerState state = Customer.CustomerState.ACTIVE; } diff --git a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomerStateConverter.java b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomerStateConverter.java new file mode 100644 index 0000000..3c15185 --- /dev/null +++ b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomerStateConverter.java @@ -0,0 +1,31 @@ +package de.schulung.sample.quarkus.persistence; + +import de.schulung.sample.quarkus.domain.Customer; +import de.schulung.sample.quarkus.domain.Customer.CustomerState; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import jakarta.persistence.PersistenceException; + +@Converter(autoApply = true) +public class CustomerStateConverter implements AttributeConverter { + + + @Override + public String convertToDatabaseColumn(CustomerState source) { + return null == source ? null : switch (source) { + case ACTIVE -> "a"; + case LOCKED -> "l"; + case DISABLED -> "d"; + }; + } + + @Override + public CustomerState convertToEntityAttribute(String source) { + return null == source ? null : switch (source) { + case "a" -> Customer.CustomerState.ACTIVE; + case "l" -> Customer.CustomerState.LOCKED; + case "d" -> Customer.CustomerState.DISABLED; + default -> throw new PersistenceException(source + " is not allowed here."); + }; + } +} From d2d3daa53e89c155b3681873f0e5c4f60e18f0c3 Mon Sep 17 00:00:00 2001 From: Ralf Ueberfuhr Date: Fri, 21 Jun 2024 13:53:21 +0200 Subject: [PATCH 10/10] Use Lambdas --- .../infrastructure/LongRunningStartup.java | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/infrastructure/LongRunningStartup.java b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/infrastructure/LongRunningStartup.java index 973e964..85ebad7 100644 --- a/customer-api-provider/src/main/java/de/schulung/sample/quarkus/infrastructure/LongRunningStartup.java +++ b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/infrastructure/LongRunningStartup.java @@ -43,13 +43,10 @@ public void init() { @ApplicationScoped @Liveness public HealthCheck myStartupLiveness() { - return new HealthCheck() { - @Override - public HealthCheckResponse call() { - final var result = HealthCheckResponse - .named("My long-running startup"); - return (initializingFailed ? result.down() : result.up()).build(); - } + return () -> { + final var result = HealthCheckResponse + .named("My long-running startup"); + return (initializingFailed ? result.down() : result.up()).build(); }; } @@ -57,13 +54,10 @@ public HealthCheckResponse call() { @ApplicationScoped @Readiness public HealthCheck myStartupReadyness() { - return new HealthCheck() { - @Override - public HealthCheckResponse call() { - final var result = HealthCheckResponse - .named("My long-running startup"); - return (initialized ? result.up() : result.down()).build(); - } + return () -> { + final var result = HealthCheckResponse + .named("My long-running startup"); + return (initialized ? result.up() : result.down()).build(); }; }