diff --git a/README.md b/README.md
index 10b9111..b53eee4 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,7 @@ It is part of the HappyCoders tutorial series on Hexagonal Architecture:
* [Part 2: Hexagonal Architecture with Java - Tutorial](https://www.happycoders.eu/software-craftsmanship/hexagonal-architecture-java/).
* [Part 3: Ports and Adapters Java Tutorial: Adding a Database Adapter](https://www.happycoders.eu/software-craftsmanship/ports-and-adapters-java-tutorial-db/).
* [Part 4: Hexagonal Architecture with Quarkus - Tutorial](https://www.happycoders.eu/software-craftsmanship/hexagonal-architecture-quarkus/).
+* [Part 5: Hexagonal Architecture with Spring Boot - Tutorial](https://www.happycoders.eu/software-craftsmanship/hexagonal-architecture-spring-boot/).
# Branches
@@ -33,7 +34,7 @@ In the `with-quarkus` branch, you'll find an implementation using [Quarkus](http
## `with-spring`
-There will soon be an additional branch with an implementation using [Spring](https://spring.io/) instead of Quarkus.
+In the `with-quarkus` branch, you'll find an implementation using [Spring](https://spring.io/) as application framework.
# Architecture Overview
@@ -51,22 +52,25 @@ The `model` module is not represented as a hexagon because it is not defined by
# How to Run the Application
-The easiest way to run the application is to start the `main` method of the `Launcher` class (you'll find it in the `boostrap` module) from your IDE.
+You can run the application in Quarkus dev mode with the following command:
+
+```shell
+mvn test-compile quarkus:dev
+```
You can use one of the following VM options to select a persistence mechanism:
* `-Dpersistence=inmemory` to select the in-memory persistence option (default)
* `-Dpersistence=mysql` to select the MySQL option
-If you selected the MySQL option, you will need a running MySQL database. The easiest way to start one is to use the following Docker command:
+For example, to run the application in MySQL mode, enter:
```shell
-docker run --name hexagon-mysql -d -p3306:3306 \
- -e MYSQL_DATABASE=shop -e MYSQL_ROOT_PASSWORD=test mysql:8.1
+mvn test-compile quarkus:dev -Dpersistence=mysql
```
-The connection parameters for the database are hardcoded in `RestEasyUndertowShopApplication.initMySqlAdapter()`. If you are using the Docker container as described above, you can leave the connection parameters as they are. Otherwise, you may need to adjust them.
-
+In dev mode, Quarkus will automatically start a MySQL database using Docker,
+and it will automatically create all database tables.
# Example Curl Commands
diff --git a/adapter/pom.xml b/adapter/pom.xml
index 011f30f..189ccd7 100644
--- a/adapter/pom.xml
+++ b/adapter/pom.xml
@@ -23,53 +23,44 @@
- jakarta.persistence
- jakarta.persistence-api
+ io.quarkus
+ quarkus-arc
- jakarta.ws.rs
- jakarta.ws.rs-api
+ io.quarkus
+ quarkus-hibernate-orm
-
-
- mysql
- mysql-connector-java
- test
+ io.quarkus
+ quarkus-hibernate-orm-panache
- io.rest-assured
- rest-assured
- test
+ io.quarkus
+ quarkus-jdbc-mysql
- org.jboss.resteasy
- resteasy-jackson2-provider
- test
+ io.quarkus
+ quarkus-resteasy
- org.glassfish
- jakarta.el
- test
-
-
- org.hibernate.orm
- hibernate-core
- test
+ io.quarkus
+ quarkus-resteasy-jackson
+
+
- org.hibernate.validator
- hibernate-validator
+ io.quarkus
+ quarkus-junit5
test
- org.jboss.resteasy
- resteasy-undertow
+ io.quarkus
+ quarkus-junit5-mockito
test
- org.testcontainers
- mysql
+ io.rest-assured
+ rest-assured
test
diff --git a/adapter/src/main/java/eu/happycoders/shop/QuarkusAppConfig.java b/adapter/src/main/java/eu/happycoders/shop/QuarkusAppConfig.java
new file mode 100644
index 0000000..9eacf0f
--- /dev/null
+++ b/adapter/src/main/java/eu/happycoders/shop/QuarkusAppConfig.java
@@ -0,0 +1,47 @@
+package eu.happycoders.shop;
+
+import eu.happycoders.shop.application.port.in.cart.AddToCartUseCase;
+import eu.happycoders.shop.application.port.in.cart.EmptyCartUseCase;
+import eu.happycoders.shop.application.port.in.cart.GetCartUseCase;
+import eu.happycoders.shop.application.port.in.product.FindProductsUseCase;
+import eu.happycoders.shop.application.port.out.persistence.CartRepository;
+import eu.happycoders.shop.application.port.out.persistence.ProductRepository;
+import eu.happycoders.shop.application.service.cart.AddToCartService;
+import eu.happycoders.shop.application.service.cart.EmptyCartService;
+import eu.happycoders.shop.application.service.cart.GetCartService;
+import eu.happycoders.shop.application.service.product.FindProductsService;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.inject.Instance;
+import jakarta.enterprise.inject.Produces;
+import jakarta.inject.Inject;
+
+class QuarkusAppConfig {
+
+ @Inject Instance cartRepository;
+
+ @Inject Instance productRepository;
+
+ @Produces
+ @ApplicationScoped
+ GetCartUseCase getCartUseCase() {
+ return new GetCartService(cartRepository.get());
+ }
+
+ @Produces
+ @ApplicationScoped
+ EmptyCartUseCase emptyCartUseCase() {
+ return new EmptyCartService(cartRepository.get());
+ }
+
+ @Produces
+ @ApplicationScoped
+ FindProductsUseCase findProductsUseCase() {
+ return new FindProductsService(productRepository.get());
+ }
+
+ @Produces
+ @ApplicationScoped
+ AddToCartUseCase addToCartUseCase() {
+ return new AddToCartService(cartRepository.get(), productRepository.get());
+ }
+}
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryCartRepository.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryCartRepository.java
index 3d8a373..0cb5baa 100644
--- a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryCartRepository.java
+++ b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryCartRepository.java
@@ -3,6 +3,8 @@
import eu.happycoders.shop.application.port.out.persistence.CartRepository;
import eu.happycoders.shop.model.cart.Cart;
import eu.happycoders.shop.model.customer.CustomerId;
+import io.quarkus.arc.lookup.LookupIfProperty;
+import jakarta.enterprise.context.ApplicationScoped;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
@@ -12,6 +14,8 @@
*
* @author Sven Woltmann
*/
+@LookupIfProperty(name = "persistence", stringValue = "inmemory", lookupIfMissing = true)
+@ApplicationScoped
public class InMemoryCartRepository implements CartRepository {
private final Map carts = new ConcurrentHashMap<>();
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryProductRepository.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryProductRepository.java
index 3bef51d..3179707 100644
--- a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryProductRepository.java
+++ b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/inmemory/InMemoryProductRepository.java
@@ -4,6 +4,8 @@
import eu.happycoders.shop.application.port.out.persistence.ProductRepository;
import eu.happycoders.shop.model.product.Product;
import eu.happycoders.shop.model.product.ProductId;
+import io.quarkus.arc.lookup.LookupIfProperty;
+import jakarta.enterprise.context.ApplicationScoped;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -15,6 +17,8 @@
*
* @author Sven Woltmann
*/
+@LookupIfProperty(name = "persistence", stringValue = "inmemory", lookupIfMissing = true)
+@ApplicationScoped
public class InMemoryProductRepository implements ProductRepository {
private final Map products = new ConcurrentHashMap<>();
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/EntityManagerFactoryFactory.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/EntityManagerFactoryFactory.java
deleted file mode 100644
index f06b17f..0000000
--- a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/EntityManagerFactoryFactory.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package eu.happycoders.shop.adapter.out.persistence.jpa;
-
-import jakarta.persistence.EntityManagerFactory;
-import jakarta.persistence.Persistence;
-import java.util.Map;
-
-/**
- * Factory for an EntityManagerFactory for connecting to a MySQL database.
- *
- * @author Sven Woltmann
- */
-public final class EntityManagerFactoryFactory {
-
- private EntityManagerFactoryFactory() {}
-
- public static EntityManagerFactory createMySqlEntityManagerFactory(
- String jdbcUrl, String user, String password) {
- return Persistence.createEntityManagerFactory(
- "eu.happycoders.shop.adapter.out.persistence.jpa",
- Map.of(
- "hibernate.dialect",
- "org.hibernate.dialect.MySQLDialect",
- "hibernate.hbm2ddl.auto",
- "update",
- "jakarta.persistence.jdbc.driver",
- "com.mysql.jdbc.Driver",
- "jakarta.persistence.jdbc.url",
- jdbcUrl,
- "jakarta.persistence.jdbc.user",
- user,
- "jakarta.persistence.jdbc.password",
- password));
- }
-}
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartPanacheRepository.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartPanacheRepository.java
new file mode 100644
index 0000000..f565e3c
--- /dev/null
+++ b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartPanacheRepository.java
@@ -0,0 +1,12 @@
+package eu.happycoders.shop.adapter.out.persistence.jpa;
+
+import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
+import jakarta.enterprise.context.ApplicationScoped;
+
+/**
+ * Panache repository for {@link CartJpaEntity}.
+ *
+ * @author Sven Woltmann
+ */
+@ApplicationScoped
+public class JpaCartPanacheRepository implements PanacheRepositoryBase {}
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartRepository.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartRepository.java
index 7435e90..c5192e3 100644
--- a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartRepository.java
+++ b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaCartRepository.java
@@ -3,8 +3,9 @@
import eu.happycoders.shop.application.port.out.persistence.CartRepository;
import eu.happycoders.shop.model.cart.Cart;
import eu.happycoders.shop.model.customer.CustomerId;
-import jakarta.persistence.EntityManager;
-import jakarta.persistence.EntityManagerFactory;
+import io.quarkus.arc.lookup.LookupIfProperty;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.transaction.Transactional;
import java.util.Optional;
/**
@@ -12,43 +13,32 @@
*
* @author Sven Woltmann
*/
+@LookupIfProperty(name = "persistence", stringValue = "mysql")
+@ApplicationScoped
public class JpaCartRepository implements CartRepository {
- private final EntityManagerFactory entityManagerFactory;
+ private final JpaCartPanacheRepository panacheRepository;
- public JpaCartRepository(EntityManagerFactory entityManagerFactory) {
- this.entityManagerFactory = entityManagerFactory;
+ public JpaCartRepository(JpaCartPanacheRepository panacheRepository) {
+ this.panacheRepository = panacheRepository;
}
@Override
+ @Transactional
public void save(Cart cart) {
- try (EntityManager entityManager = entityManagerFactory.createEntityManager()) {
- entityManager.getTransaction().begin();
- entityManager.merge(CartMapper.toJpaEntity(cart));
- entityManager.getTransaction().commit();
- }
+ panacheRepository.getEntityManager().merge(CartMapper.toJpaEntity(cart));
}
@Override
+ @Transactional
public Optional findByCustomerId(CustomerId customerId) {
- try (EntityManager entityManager = entityManagerFactory.createEntityManager()) {
- CartJpaEntity cartJpaEntity = entityManager.find(CartJpaEntity.class, customerId.value());
- return CartMapper.toModelEntityOptional(cartJpaEntity);
- }
+ CartJpaEntity cartJpaEntity = panacheRepository.findById(customerId.value());
+ return CartMapper.toModelEntityOptional(cartJpaEntity);
}
@Override
+ @Transactional
public void deleteByCustomerId(CustomerId customerId) {
- try (EntityManager entityManager = entityManagerFactory.createEntityManager()) {
- entityManager.getTransaction().begin();
-
- CartJpaEntity cartJpaEntity = entityManager.find(CartJpaEntity.class, customerId.value());
-
- if (cartJpaEntity != null) {
- entityManager.remove(cartJpaEntity);
- }
-
- entityManager.getTransaction().commit();
- }
+ panacheRepository.deleteById(customerId.value());
}
}
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductPanacheRepository.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductPanacheRepository.java
new file mode 100644
index 0000000..96d855c
--- /dev/null
+++ b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductPanacheRepository.java
@@ -0,0 +1,13 @@
+package eu.happycoders.shop.adapter.out.persistence.jpa;
+
+import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
+import jakarta.enterprise.context.ApplicationScoped;
+
+/**
+ * Panache repository for {@link ProductJpaEntity}.
+ *
+ * @author Sven Woltmann
+ */
+@ApplicationScoped
+public class JpaProductPanacheRepository
+ implements PanacheRepositoryBase {}
diff --git a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductRepository.java b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductRepository.java
index a6e2a9a..2d7eb0a 100644
--- a/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductRepository.java
+++ b/adapter/src/main/java/eu/happycoders/shop/adapter/out/persistence/jpa/JpaProductRepository.java
@@ -4,9 +4,10 @@
import eu.happycoders.shop.application.port.out.persistence.ProductRepository;
import eu.happycoders.shop.model.product.Product;
import eu.happycoders.shop.model.product.ProductId;
-import jakarta.persistence.EntityManager;
-import jakarta.persistence.EntityManagerFactory;
-import jakarta.persistence.TypedQuery;
+import io.quarkus.arc.lookup.LookupIfProperty;
+import jakarta.annotation.PostConstruct;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.transaction.Transactional;
import java.util.List;
import java.util.Optional;
@@ -15,49 +16,42 @@
*
* @author Sven Woltmann
*/
+@LookupIfProperty(name = "persistence", stringValue = "mysql")
+@ApplicationScoped
public class JpaProductRepository implements ProductRepository {
- private final EntityManagerFactory entityManagerFactory;
+ private final JpaProductPanacheRepository panacheRepository;
- public JpaProductRepository(EntityManagerFactory entityManagerFactory) {
- this.entityManagerFactory = entityManagerFactory;
- createDemoProducts();
+ public JpaProductRepository(JpaProductPanacheRepository panacheRepository) {
+ this.panacheRepository = panacheRepository;
}
- private void createDemoProducts() {
+ @PostConstruct
+ void createDemoProducts() {
DemoProducts.DEMO_PRODUCTS.forEach(this::save);
}
@Override
+ @Transactional
public void save(Product product) {
- try (EntityManager entityManager = entityManagerFactory.createEntityManager()) {
- entityManager.getTransaction().begin();
- entityManager.merge(ProductMapper.toJpaEntity(product));
- entityManager.getTransaction().commit();
- }
+ panacheRepository.getEntityManager().merge(ProductMapper.toJpaEntity(product));
}
@Override
+ @Transactional
public Optional findById(ProductId productId) {
- try (EntityManager entityManager = entityManagerFactory.createEntityManager()) {
- ProductJpaEntity jpaEntity = entityManager.find(ProductJpaEntity.class, productId.value());
- return ProductMapper.toModelEntityOptional(jpaEntity);
- }
+ ProductJpaEntity jpaEntity = panacheRepository.findById(productId.value());
+ return ProductMapper.toModelEntityOptional(jpaEntity);
}
@Override
+ @Transactional
public List findByNameOrDescription(String queryString) {
- try (EntityManager entityManager = entityManagerFactory.createEntityManager()) {
- TypedQuery query =
- entityManager
- .createQuery(
- "from ProductJpaEntity where name like :query or description like :query",
- ProductJpaEntity.class)
- .setParameter("query", "%" + queryString + "%");
-
- List entities = query.getResultList();
-
- return ProductMapper.toModelEntities(entities);
- }
+ List entities =
+ panacheRepository
+ .find("name like ?1 or description like ?1", "%" + queryString + "%")
+ .list();
+
+ return ProductMapper.toModelEntities(entities);
}
}
diff --git a/adapter/src/main/resources/META-INF/persistence.xml b/adapter/src/main/resources/META-INF/persistence.xml
deleted file mode 100644
index 6794017..0000000
--- a/adapter/src/main/resources/META-INF/persistence.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
- eu.happycoders.shop.adapter.out.persistence.jpa.CartJpaEntity
- eu.happycoders.shop.adapter.out.persistence.jpa.CartLineItemJpaEntity
- eu.happycoders.shop.adapter.out.persistence.jpa.ProductJpaEntity
- true
-
-
diff --git a/adapter/src/test/java/eu/happycoders/shop/adapter/TestProfileWithMySQL.java b/adapter/src/test/java/eu/happycoders/shop/adapter/TestProfileWithMySQL.java
new file mode 100644
index 0000000..c097e95
--- /dev/null
+++ b/adapter/src/test/java/eu/happycoders/shop/adapter/TestProfileWithMySQL.java
@@ -0,0 +1,12 @@
+package eu.happycoders.shop.adapter;
+
+import io.quarkus.test.junit.QuarkusTestProfile;
+import java.util.Map;
+
+public class TestProfileWithMySQL implements QuarkusTestProfile {
+
+ @Override
+ public Map getConfigOverrides() {
+ return Map.of("persistence", "mysql");
+ }
+}
diff --git a/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/HttpTestCommons.java b/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/HttpTestCommons.java
index 2c4b51c..12a181d 100644
--- a/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/HttpTestCommons.java
+++ b/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/HttpTestCommons.java
@@ -7,9 +7,6 @@
public final class HttpTestCommons {
- // So the tests can run when the application runs on port 8080:
- public static final int TEST_PORT = 8082;
-
private HttpTestCommons() {}
public static void assertThatResponseIsError(
diff --git a/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/cart/CartsControllerTest.java b/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/cart/CartsControllerTest.java
index 7757835..2e2b48a 100644
--- a/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/cart/CartsControllerTest.java
+++ b/adapter/src/test/java/eu/happycoders/shop/adapter/in/rest/cart/CartsControllerTest.java
@@ -1,6 +1,5 @@
package eu.happycoders.shop.adapter.in.rest.cart;
-import static eu.happycoders.shop.adapter.in.rest.HttpTestCommons.TEST_PORT;
import static eu.happycoders.shop.adapter.in.rest.HttpTestCommons.assertThatResponseIsError;
import static eu.happycoders.shop.adapter.in.rest.cart.CartsControllerAssertions.assertThatResponseIsCart;
import static eu.happycoders.shop.model.money.TestMoneyFactory.euros;
@@ -8,7 +7,6 @@
import static io.restassured.RestAssured.given;
import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST;
import static jakarta.ws.rs.core.Response.Status.NO_CONTENT;
-import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -21,62 +19,27 @@
import eu.happycoders.shop.model.customer.CustomerId;
import eu.happycoders.shop.model.product.Product;
import eu.happycoders.shop.model.product.ProductId;
+import io.quarkus.test.InjectMock;
+import io.quarkus.test.junit.QuarkusTest;
import io.restassured.response.Response;
-import jakarta.ws.rs.core.Application;
-import java.util.Set;
-import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer;
-import org.junit.jupiter.api.AfterAll;
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import org.mockito.Mockito;
+@QuarkusTest
class CartsControllerTest {
private static final CustomerId TEST_CUSTOMER_ID = new CustomerId(61157);
private static final Product TEST_PRODUCT_1 = createTestProduct(euros(19, 99));
private static final Product TEST_PRODUCT_2 = createTestProduct(euros(25, 99));
- private static final AddToCartUseCase addToCartUseCase = mock(AddToCartUseCase.class);
- private static final GetCartUseCase getCartUseCase = mock(GetCartUseCase.class);
- private static final EmptyCartUseCase emptyCartUseCase = mock(EmptyCartUseCase.class);
-
- private static UndertowJaxrsServer server;
-
- @BeforeAll
- static void init() {
- server =
- new UndertowJaxrsServer()
- .setPort(TEST_PORT)
- .start()
- .deploy(
- new Application() {
- @Override
- public Set