
Part 2: Quarkus Persistence, CRUD with Panache. E-commerce example.
In the first part of this tutorial we developed the entity for the products using the Active Record Patttern. In this tutorial we are creating the rest of the entities needed below
Customer
Order
OrderItem
The relations are:
OneToMany: one customer has many orders
OneToMany: one order has many order items
OneToOne: one order item has one product
Step 1. Entity classes
Customer.Java
package com.platform.ecommerce.order.model;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
public class Customer extends PanacheEntity{
@Column(nullable = false)
public String name;
@Column(nullable = false)
public String surname;
@Column(nullable = false)
public String email;
@Column(nullable = false)
public String address;
@Column(nullable = false)
public String phone;
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonIgnore
public List<Order> orderList = new ArrayList<>();
}
Order.Java
package com.platform.ecommerce.order.model;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import io.quarkus.logging.Log;
import io.quarkus.panache.common.Sort;
import jakarta.persistence.*;
import jakarta.transaction.Transactional;
import org.hibernate.annotations.CreationTimestamp;
import jakarta.persistence.Column;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "orders")
public class Order extends PanacheEntity {
@Column(nullable = false)
public double totalAmount;
@Column(nullable = false)
public String status;
@CreationTimestamp
@Column(updatable = false, nullable = false)
public ZonedDateTime created;
@ManyToOne()
@JoinColumn(name = "customer_id", nullable = false)
public Customer customer;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
public List<OrderItem> orderItems = new ArrayList<>();
@Transient
public void setTotalPrice() {
double total = 0D;
for (OrderItem item : orderItems) {
total += item.totalAmount;
}
totalAmount = total;
}
public static Order findOrderById(Long id) {
return findById(id);
}
public static List<Order> findOrdersByStatus(String status) {
List<Order> orderList = Order.list("status", Sort.by("customer.id").and("created"), status);
return orderList;
}
public static List<Order> findOrdersByCustomerId(Long customerId) {
List<Order> orderList = Order.list("customer.id", Sort.by("created"), customerId);
return orderList;
}
@Transactional
public static void createOrder(Long customerId, Order order) {
Customer customer = Customer.findById(customerId);
order.customer = customer;
order.persist();
}
@Transactional
public static void addOrderItem(Long orderId, Product product, Integer quantity) {
Order order = findOrderById(orderId);
OrderItem orderItem = new OrderItem(order,product,quantity);
order.orderItems.add(orderItem);
order.setTotalPrice();
order.persist();
}
@Transactional
public static void updateOrderItemQuantity(Long orderId, Long productId, Integer quantity) {
Order order = findOrderById(orderId);
Log.info("Order found:"+order.id);
OrderItem orderItem = order.orderItems.stream().filter(o -> productId.equals(o.product.id)).findFirst().orElse(null);
Log.info("Order item found:"+orderItem.id);
orderItem.quantity = quantity;
Log.info("Update quantity to:"+quantity);
orderItem.setTotalPrice();
order.setTotalPrice();
order.persist();
}
@Transactional
public static void updateOrderStatus(Long id, String status) {
Order order = findOrderById(id);
order.status = status;
order.persist();
}
@Transactional
public static void deleteOrderItem(Long orderId, Long productId) {
Order order = findOrderById(orderId);
Log.info("Order found:"+order.id);
OrderItem orderItem = order.orderItems.stream().filter(o -> productId.equals(o.product.id)).findFirst().orElse(null);
Log.info("Order item found:"+orderItem.id);
order.orderItems.remove(orderItem);
Log.info("Remove item:"+orderItem.id);
order.setTotalPrice();
order.persist();
}
@Transactional
public static void deleteOrder(Long id) {
findById(id).delete();
}
}Tip:Notice how the functionality of adding order items and removing order items is done within the addOrderItem and removeOrderItem functions of the orders class. Since we are using CascadeType.ALL at the @OneToMany relationship any change to the list of orderItems, which is a change in the parent entity will be propagated to the child class which in this case is the OrderItem.
In addition we are using the productId and not the OrderItem id to search through the list of OrderItems.
OrderItem.Java
package com.platform.ecommerce.order.model;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.*;
@Entity
public class OrderItem extends PanacheEntity {
//default constructor
public OrderItem() {
}
public OrderItem(Order order, Product product, Integer quantity) {
this.order = order;
this.product = product;
this.quantity = quantity;
totalAmount = product.price*quantity;
}
public Long getId() {
return id;
}
@Column(nullable = false)
public double totalAmount;
@Column(nullable = false)
public Integer quantity;
@Transient
public Long productId;
@OneToOne
@JoinColumn(name = "product_id",referencedColumnName = "id",insertable = true,updatable = false)
public Product product = new Product();
@ManyToOne(optional = false, fetch = FetchType.EAGER)
@JoinColumn(name="order_id", nullable=false)
@JsonIgnore
public Order order;
@Transient
public void setTotalPrice() {
totalAmount = product.price*quantity;
}
}
Step 2. Create Resource Endpoint for Orders.
OrderResource.java
package com.platform.ecommerce.order.resource;
import com.platform.ecommerce.order.model.OrderItem;
import com.platform.ecommerce.order.model.Order;
import io.quarkus.logging.Log;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.List;
@Path("order-service/v1/order")
@ApplicationScoped
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class OrderResource {
@GET
@Path("all")
public List<Order> getAllOrders() {return Order.listAll(); }
@GET
@Path("{id}")
public Order findById(@PathParam("id") Long id) {
Order order = Order.findById(id);
return order;
}
@POST
@Path("create/{customerId}")
public Response createOrder(@PathParam("customerId") Long customerId, Order order) {
Order.createOrder(customerId,order);
Log.info("Order created:"+order.id);
return Response.status(Response.Status.CREATED).build();
}
@PUT
@Path("update/status/{id}")
public Response updateOrderStatus(@PathParam("id") Long id, String status) {
Order.updateOrderStatus(id,status);
return Response.status(Response.Status.NO_CONTENT).build();
}
@Path("add/item/{orderId}")
@POST
public Response addOrderItem(@PathParam("orderId") Long orderId, OrderItem orderItemForm) {
Product product = Product.findProductById(orderItemForm.productId);
Order.addOrderItem(orderId, product, orderItemForm.quantity);
return Response.status(Response.Status.OK).build();
}
@Path("update/item/{orderId}")
@PUT
public Response updateOrderItemQuantity(@PathParam("orderId") Long orderId, OrderItem orderItemForm) {
Order.updateOrderItemQuantity(orderId,orderItemForm.productId,orderItemForm.quantity);
return Response.status(Response.Status.OK).build();
}
@Path("delete/item/{orderId}")
@PUT
public Response deleteOrderItem(@PathParam("orderId") Long orderId, OrderItem orderItemForm) {
Order.deleteOrderItem(orderId,orderItemForm.productId);
return Response.status(Response.Status.OK).build();
}
@GET
@Path("customer/{id}")
public List<Order> findOrdersByCustomerId(@PathParam("id") Long id) {
return Order.findOrdersByCustomerId(id);
}
@DELETE
@Path("delete/{id}")
public Response deleteOrder(@PathParam("id") Long id) {
Order.deleteOrder(id);
return Response.status(Response.Status.OK).build();
}
}Step 3. Add Sample Data for Customers.
To load SQL statements when Hibernate ORM starts, add an import.sql file to the root of your resources directory. This script can contain any SQL DML statements. Make sure to terminate each statement with a semicolon.(Source:https://quarkus.io/guides/hibernate-orm).
We have already added some custom data for products in the first part. Now its time to add a couple of customer records.
import.sql
INSERT INTO customer (id, name, surname,email,address,phone) VALUES ( nextval('customer_seq'), 'Marco','Verratti','[email protected]','Princes Park (Le Parc des Princes)','000000000');
INSERT INTO customer (id, name, surname,email,address,phone) VALUES ( nextval('customer_seq'), 'Kylian','Mbappé','kylian.mbappé@gmail.com','Princes Park (Le Parc des Princes)','000000000');
INSERT INTO product (id, name, price,stock) VALUES ( nextval('product_seq'), 'MacMini M1',699.00,0);
INSERT INTO product (id, name, price,stock) VALUES ( nextval('product_seq'), 'MacMini M2',899.00,5);
INSERT INTO product (id, name, price,stock) VALUES ( nextval('product_seq'), 'MacBook Air M1',999.00,100);
INSERT INTO product (id, name, price,stock) VALUES ( nextval('product_seq'), 'MacBook Air M2',1399.00,100);
INSERT INTO product (id, name, price,stock) VALUES ( nextval('product_seq'), 'MacStudio M2 Max',2390.00,100);
INSERT INTO product (id, name, price,stock) VALUES ( nextval('product_seq'), 'MacStudio M2 Ultra',4399.00,100);Step 4. Create Testing for our Orders Endpoint.
Its time to write our Test for our OrderResource. I’m using the @Order annotation to specify the order of execution. For this test we create 1 order, then we add 1 order item of product with id:1 and quantuty 2. Then we update the quantity to 40 and test again. Finally we delete the order item and test if we are fetching an order with an empty list of order items.
package com.platform.ecommerce.order.resource;
import com.platform.ecommerce.order.model.OrderStatus;
import com.platform.ecommerce.order.model.OrderItem;
import io.quarkus.test.common.http.TestHTTPEndpoint;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.json.Json;
import jakarta.json.JsonObject;
import jakarta.ws.rs.core.MediaType;
import org.junit.jupiter.api.*;
import static io.restassured.RestAssured.given;
@QuarkusTest
@TestHTTPEndpoint(OrderResource.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class OrderResourceTest {
@Test
@Order(1)
public void createOrder() {
JsonObject order = Json.createObjectBuilder()
.add("status", OrderStatus.Pending).build();
// Test POST
given()
.contentType(MediaType.APPLICATION_JSON)
.body(order.toString())
.when()
.when().post("create/{customerId}",1)
.then()
.statusCode(201);
}
@Test
@Order(2)
public void addOrderItem() {
JsonObject orderItem = Json.createObjectBuilder()
.add("productId", 1)
.add("quantity", 2)
.build();
// Test POST
given()
.contentType(MediaType.APPLICATION_JSON)
.body(orderItem.toString())
.when()
.when().post("add/item/{orderId}",1)
.then()
.statusCode(200);
}
@Test
@Order(3)
public void whenFetchOrder_ThenAtLeastOneOrderShouldBeFound() {
//TEST GET
com.platform.ecommerce.order.model.Order order = given()
.accept(MediaType.APPLICATION_JSON)
.when().get("{id}",1)
.then()
.statusCode(200)
.extract()
.body().as(com.platform.ecommerce.order.model.Order.class);
}
@Test
@Order(4)
public void whenUpdateOrderItemQuantity_ThenQuantityShouldBeEqual() {
JsonObject orderItemJSON = Json.createObjectBuilder()
.add("productId", 1)
.add("quantity", 40)
.build();
// Test UPDATE
given()
.contentType(MediaType.APPLICATION_JSON)
.body(orderItemJSON.toString())
.when().put("update/item/{orderId}",1)
.then()
.statusCode(200);
//FETCH ORDER
com.platform.ecommerce.order.model.Order order = given()
.accept(MediaType.APPLICATION_JSON)
.when().get("{id}",1)
.then()
.statusCode(200)
.extract()
.body().as(com.platform.ecommerce.order.model.Order.class);
//EXTRACT THE ORDER ITEM FOR PRODUCT WITH ID=1
OrderItem orderItem = order.orderItems.stream().filter(o -> o.product.id.intValue()==1).findFirst().orElse(null);
Assertions.assertEquals(orderItem.quantity,40);
}
@Test
@Order(4)
public void whenDeleteOrderItem_ThenSizeOfItemsShouldBeZero() {
JsonObject orderItemJSON = Json.createObjectBuilder()
.add("productId", 1)
.build();
// Test UPDATE
given()
.contentType(MediaType.APPLICATION_JSON)
.body(orderItemJSON.toString())
.when().put("delete/item/{orderId}",1)
.then()
.statusCode(200);
//FETCH ORDER
com.platform.ecommerce.order.model.Order order = given()
.accept(MediaType.APPLICATION_JSON)
.when().get("{id}",1)
.then()
.statusCode(200)
.extract()
.body().as(com.platform.ecommerce.order.model.Order.class);
//EXTRACT THE ORDER ITEM FOR PRODUCT WITH ID=1
Assertions.assertEquals(order.orderItems.size(),0);
}
}
Last step is to open up a terminal and run the test by typing
mvn clean testRun the program by typing mvn quarkus:dev if you are using maven or if you are using quarkus cli type quarkus dev
mvn quarkus:dev OR quarkus dev
That’s all!
Our system is a basic one and there is lots of functionality to be added.
its a monolithic application. Also all the ‘product’ functionality is handled by this application, although it should be handled in a separate micro-service one.
For the purpose of the tutorial I did that on-purpose, and in the articles to come will we break our application into 2 different ones, into 2 different micro services.
In addition in the next articles we will explore how we can add security, how to check items from the inventory, configuration profiles, kubernetes deployments and much more.
Thank you for reading this article!
TUTORIAL PARTS
Part 1: Quarkus Persistence, CRUD with Panache. E-commerce example.
Part 2: Quarkus Persistence, CRUD with Panache. E-commerce example.



