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..63a3d33 --- /dev/null +++ b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomerEntityRepository.java @@ -0,0 +1,10 @@ +package de.schulung.sample.quarkus.persistence; + +import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.UUID; + +@ApplicationScoped +public class CustomerEntityRepository implements PanacheRepositoryBase { +} 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..aa44708 --- /dev/null +++ b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomersSinkJdbcImpl.java @@ -0,0 +1,208 @@ +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; +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; + + /* ******************************************************* * + * 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); + } + + private static Date convertDate(LocalDate date) { + return Optional.ofNullable(date) + .map(Date::valueOf) + .orElse(null); + } + + private static CustomerState convertState(int value) { + return CustomerState.values()[value]; + } + + private static int convertState(CustomerState value) { + return Optional.ofNullable(value) + .map(CustomerState::ordinal) + .orElse(0); + + } + + /* ******************************************************* * + * 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 readAll(rs); + + } catch (SQLException e) { + throw new RuntimeException(e); // eigene Exception? + } + } + + @Override + 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) { + 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) { + try (Connection con = ds.getConnection(); + PreparedStatement stmt = con.prepareStatement( + "delete from CUSTOMERS where UUID=?" + )) { + + stmt.setString(1, convertUuid(uuid)); + return stmt.executeUpdate() > 0; + + } catch (SQLException e) { + throw new RuntimeException(e); // eigene Exception? + } + } + + @Override + public long count() { + 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/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 ?? + } +} 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..d81c28e --- /dev/null +++ b/customer-api-provider/src/main/java/de/schulung/sample/quarkus/persistence/CustomersSinkPanacheImpl.java @@ -0,0 +1,71 @@ +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.transaction.Transactional; +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 = "panache" +) +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) { + return repo.list("state", state) + .stream() + .map(mapper::map); + } + + @Override + public Optional findByUuid(UUID uuid) { + return repo.findByIdOptional(uuid) + .map(mapper::map); + } + + @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 + @Transactional + public boolean delete(UUID uuid) { + return this.repo.deleteById(uuid); + } + + @Override + public boolean exists(UUID uuid) { + return repo.findByIdOptional(uuid).isPresent(); + } + + @Override + public long count() { + return repo.count(); + } +} diff --git a/customer-api-provider/src/main/resources/application.properties b/customer-api-provider/src/main/resources/application.properties index c601ba1..b200a40 100644 --- a/customer-api-provider/src/main/resources/application.properties +++ b/customer-api-provider/src/main/resources/application.properties @@ -8,4 +8,15 @@ 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 + +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/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/PersistencePanacheCustomersSinkTests.java b/customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/PersistencePanacheCustomersSinkTests.java new file mode 100644 index 0000000..780a549 --- /dev/null +++ b/customer-api-provider/src/test/java/de/schulung/sample/quarkus/persistence/PersistencePanacheCustomersSinkTests.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(UsePanacheImplementation.class) +public class PersistencePanacheCustomersSinkTests { + + @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/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" + ); + } +} 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" + ); + } +} diff --git a/docs/06-mandantenfaehigkeit.png b/docs/06-mandantenfaehigkeit.png new file mode 100644 index 0000000..049f444 Binary files /dev/null and b/docs/06-mandantenfaehigkeit.png differ diff --git a/docs/07-jdbc-jpa-panache.png b/docs/07-jdbc-jpa-panache.png new file mode 100644 index 0000000..590ff3e Binary files /dev/null and b/docs/07-jdbc-jpa-panache.png differ diff --git a/docs/README.md b/docs/README.md index 92df401..2949b92 100644 --- a/docs/README.md +++ b/docs/README.md @@ -20,4 +20,9 @@ Bei `@Singleton` wird kein Proxy erzeugt. Das kann notwendig sein, z.B. wenn die Klasse `final` ist. -![Dynamic Proxies im CDI](05-dynamic-proxies.png) \ No newline at end of file +![Dynamic Proxies im CDI](05-dynamic-proxies.png) + +## Datenbankzugriffe + +![Mandantenfähigkeit](06-mandantenfaehigkeit.png) +![JDBC-JPA-Panache](07-jdbc-jpa-panache.png) \ No newline at end of file