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