Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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

Expand Down
49 changes: 20 additions & 29 deletions adapter/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,53 +23,44 @@

<!-- External -->
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
<groupId>jakarta.ws.rs</groupId>
<artifactId>jakarta.ws.rs-api</artifactId>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm</artifactId>
</dependency>

<!-- Test scope -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>test</scope>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-mysql</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jackson2-provider</artifactId><!-- Required at runtime to (de)serialize JSON -->
<scope>test</scope>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.el</artifactId><!-- Required by Hibernate at runtime -->
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<scope>test</scope>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-jackson</artifactId>
</dependency>

<!-- Test scope -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId><!-- Required by Hibernate at runtime -->
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-undertow</artifactId>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-mockito</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>

Expand Down
47 changes: 47 additions & 0 deletions adapter/src/main/java/eu/happycoders/shop/QuarkusAppConfig.java
Original file line number Diff line number Diff line change
@@ -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> cartRepository;

@Inject Instance<ProductRepository> 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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -12,6 +14,8 @@
*
* @author Sven Woltmann
*/
@LookupIfProperty(name = "persistence", stringValue = "inmemory", lookupIfMissing = true)
@ApplicationScoped
public class InMemoryCartRepository implements CartRepository {

private final Map<CustomerId, Cart> carts = new ConcurrentHashMap<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -15,6 +17,8 @@
*
* @author Sven Woltmann
*/
@LookupIfProperty(name = "persistence", stringValue = "inmemory", lookupIfMissing = true)
@ApplicationScoped
public class InMemoryProductRepository implements ProductRepository {

private final Map<ProductId, Product> products = new ConcurrentHashMap<>();
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<CartJpaEntity, Integer> {}
Original file line number Diff line number Diff line change
Expand Up @@ -3,52 +3,42 @@
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;

/**
* Persistence adapter: Stores carts via JPA in a database.
*
* @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<Cart> 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());
}
}
Original file line number Diff line number Diff line change
@@ -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<ProductJpaEntity, String> {}
Loading