Skip to content

Commit

Permalink
Merge pull request #6 from rmannibucau/rmannibucau/light-optimization…
Browse files Browse the repository at this point in the history
…-on-services

rework a bit the service layer to see some way to optimize tx blocks
  • Loading branch information
fpapon committed Apr 1, 2024
2 parents 05cec9d + 627d151 commit d11649f
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 97 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public String deleteOrder(final Request request, final String id) {
}

@JsonRpc(value = "fusion.examples.order.update", documentation = "Update an existing order")
public CompletionStage<Order> updateOrder(final Request request, final Order order) {
return CompletableFuture.completedStage(orderService.updateOrder(order.id(), order));
public Order updateOrder(final Request request, final Order order) {
return orderService.updateOrder(order);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,16 @@
import io.yupiik.fusion.examples.backend.service.ProductService;
import io.yupiik.fusion.framework.api.scope.ApplicationScoped;
import io.yupiik.fusion.framework.build.api.http.HttpMatcher;
import io.yupiik.fusion.http.server.api.HttpException;
import io.yupiik.fusion.http.server.api.Request;
import io.yupiik.fusion.http.server.api.Response;
import io.yupiik.fusion.json.JsonMapper;

import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletionStage;

import static java.util.concurrent.CompletableFuture.completedFuture;

@ApplicationScoped
public class RestEndpoint {
Expand All @@ -52,12 +57,12 @@ public Product findProduct(final Request request) {
}

@HttpMatcher(methods = "POST", path = "/order", pathMatching = HttpMatcher.PathMatching.EXACT)
public Response createOrder(final Request request, final Order order) {
return Response.of()
.status(201)
.header("content-type", "application/json")
.body(jsonMapper.toString(orderService.createOrder(order)))
.build();
public CompletionStage<Response> createOrder(final Request request, final Order order) {
return completedFuture(Response.of()
.status(201)
.header("content-type", "application/json")
.body(jsonMapper.toString(orderService.createOrder(order)))
.build());
}

@HttpMatcher(methods = "GET", path = "/order", pathMatching = HttpMatcher.PathMatching.EXACT)
Expand Down Expand Up @@ -92,6 +97,15 @@ public Response deleteOrder(final Request request) {
@HttpMatcher(methods = "PUT", path = "/order/", pathMatching = HttpMatcher.PathMatching.STARTS_WITH)
public Order updateOrder(final Request request, final Order order) {
final var id = request.path().split("/")[2];
return orderService.updateOrder(id, order);
if (!Objects.equals(id, order.id())) {
final var error = "Id does not match: '" + id + "' vs '" + order.id() + "'";
throw new HttpException(
error,
Response.of()
.status(400)
.body(jsonMapper.toString(new Error(error)))
.build());
}
return orderService.updateOrder(order);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,15 @@
import io.yupiik.fusion.persistence.api.ContextLessDatabase;
import io.yupiik.fusion.persistence.api.TransactionManager;

import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

import static java.util.Objects.isNull;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toList;

@ApplicationScoped
public class OrderService {
Expand All @@ -42,121 +45,94 @@ public OrderService(final ProductService productService, final ContextLessDataba
this.database = database;
this.txMgr = txMgr;
this.productService = productService;
// compute queries
this.queries = ofNullable(database).map(Queries::new).orElse(null);
}

private record Queries(String findAllProductForOrderQuery, String deleteAllProductForOrderQuery) {
private Queries(final ContextLessDatabase database) {
this(
database.entity(OrderProductEntity.class).getFindAllQuery()
+ " WHERE order_id = ?",
"delete from " + database.entity(OrderProductEntity.class).getTable() + " where order_id = ?"
);
}
// precompute runtime queries,
// ofNullable() cause @ApplicationScoped will create a subclass with null for all params if no no-arg constructor is defined
this.queries = ofNullable(database).map(Queries::new).orElse(null);
}

public Order findOrder(final String id) {
return txMgr.read(connection -> {
final var db = txMgr.read(connection -> {
final var orderEntity = database.findById(connection, OrderEntity.class, id);
if (isNull(orderEntity)) {
logger.severe("Order not found");
logger.severe(() -> "Order '" + id + "' not found");
throw new IllegalArgumentException("Not found");
}
final var productOrderEntities = database.query(connection, OrderProductEntity.class, this.queries.findAllProductForOrderQuery(), b -> b.bind(id));
final var order = mapToOrder(orderEntity);
order.products().addAll(productOrderEntities.stream().map(OrderProductEntity::productId).map(productService::findProduct).toList());
return order;
return new Pair<>(
orderEntity,
database.query(
connection, this.queries.findAllProductForOrder(),
b -> b.bind(id), r -> r.mapAll(s -> s.getString(1))));
});
return mapToOrder(db.first(), findProducts(db.second()));
}

public List<Order> findOrders() {
// final var orderEntities = orderDao.findAllOrder();
//
return txMgr.read(connection -> {
final var db = txMgr.read(connection -> {
final var orders = database.findAll(connection, OrderEntity.class);
return orders.stream()
.map(this::mapToOrder)
.peek(order -> order.products().addAll(
database.query(connection, OrderProductEntity.class, this.queries.findAllProductForOrderQuery(), b -> b.bind(order.id()))
.stream()
.map(OrderProductEntity::productId)
.map(productService::findProduct)
.toList()))
.toList();
final var orderIds = orders.stream().map(OrderEntity::id).toList();
final var products = orderIds.isEmpty() ?
List.<OrderProductEntity>of() :
database.query(
connection,
OrderProductEntity.class,
queries.findAllProductForOrdersBase() + orderIds.stream()
.map(i -> "?")
.collect(joining(", ", "(", ")")),
b -> orderIds.forEach(b::bind));
return new Pair<>(orders, products);
});

// return txMgr.read(connection -> {
// final var orders = database.findAll(connection, OrderEntity.class);
// final var orderIds = orders.stream().map(OrderEntity::id).distinct().toList();
// final var productOrderIdProductId = orderDao.findProductsByOrderIds(conn, orderIds).stream()
// .collect(groupingBy(OrderProductEntity::orderId));
// return orders.stream()
// .map(it -> mapToOrder(it, ofNullable(productOrderIdProductId.get(it.id())).orElse(List.of())))
// .toList();
// });
final var productsPerOrder = db.second().stream()
.collect(groupingBy(OrderProductEntity::orderId, mapping(OrderProductEntity::productId, toList())));
return db.first().stream()
.map(it -> mapToOrder(it, findProducts(productsPerOrder.getOrDefault(it.id(), List.of()))))
.toList();
}

public Order createOrder(final Order order) {
logger.fine("Create new order");
try {
final var createdOrder = txMgr.write(connection -> {
final var temp = database.insert(connection, mapToOrderEntity(order));
database.batchInsert(connection, OrderProductEntity.class, order.products().stream().map(Product::id).toList().stream()
.map(it -> new OrderProductEntity(temp.id(), it))
.iterator());
return temp;
});
final var newOrder = mapToOrder(createdOrder);
newOrder.products().addAll(order.products());
return newOrder;

} catch (Exception exception) {
// error, rollback is managed by datasource, no need to manage it by hand
logger.severe("Error on insert order");
throw new IllegalStateException("Error on order creation");
}
final var createdOrder = txMgr.write(connection -> {
final var temp = database.insert(connection, mapToOrderEntity(order));
database.batchInsert(connection, OrderProductEntity.class, order.products().stream().map(Product::id).toList().stream()
.map(it -> new OrderProductEntity(temp.id(), it))
.iterator());
return temp;
});

return mapToOrder(createdOrder, order.products());
}

public Order updateOrder(final String orderId, Order order) {
public Order updateOrder(final Order order) {
logger.fine("Update existing order");
return txMgr.write(connection -> {
final var existingOrderEntity = database.findById(connection, OrderEntity.class, orderId);
if (isNull(existingOrderEntity)) {
logger.severe("Error on update order");
throw new IllegalArgumentException("Not found");
}
database.update(connection,mapToOrderEntity(order, orderId));
return order;
});
txMgr.write(connection -> database.update(connection, mapToOrderEntity(order))); // fails if not found
return order;
}

public void deleteOrder(final String orderId) {
logger.fine("Delete existing order");
txMgr.write(connection -> {
final var existingOrderEntity = database.findById(connection, OrderEntity.class, orderId);
if (isNull(existingOrderEntity)) {
logger.severe("Error on delete order");
throw new IllegalArgumentException("Not found");
}
database.execute(connection, this.queries.deleteAllProductForOrderQuery(), b -> b.bind(orderId));
database.delete(connection, existingOrderEntity);
return existingOrderEntity;
database.execute(connection, this.queries.deleteAllProductForOrder(), b -> b.bind(orderId));
database.execute(connection, this.queries.deleteOrderEntityById(), b -> b.bind(orderId));
return null;
});
}

private Order mapToOrder(final OrderEntity orderEntity) {
private Order mapToOrder(final OrderEntity orderEntity, final List<Product> products) {
return new Order(
orderEntity.id(),
orderEntity.description(),
orderEntity.name(),
orderEntity.creationDate(),
orderEntity.lastUpdateDate(),
new ArrayList<>(),
products,
orderEntity.status());
}

private List<Product> findProducts(final List<String> products) {
return products.stream().map(productService::findProduct).toList();
}

private OrderEntity mapToOrderEntity(final Order order) {
return new OrderEntity(
order.id(),
Expand All @@ -168,14 +144,22 @@ private OrderEntity mapToOrderEntity(final Order order) {
);
}

private OrderEntity mapToOrderEntity(final Order order, final String id) {
return new OrderEntity(
id,
order.description(),
order.name(),
order.creationDate(),
order.lastUpdateDate(),
order.status()
);
private record Pair<A, B>(A first, B second) {
}

private record Queries(String findAllProductForOrder,
String findAllProductForOrdersBase,
String deleteOrderEntityById,
String deleteAllProductForOrder) {
private Queries(final ContextLessDatabase database) {
this(
"SELECT product_id as productId " +
"FROM " + database.entity(OrderProductEntity.class).getTable() + ' '
+ "WHERE order_id = ?",
database.entity(OrderProductEntity.class).getFindAllQuery() + " WHERE order_id in ",
"DELETE FROM " + database.entity(OrderEntity.class).getTable() + " WHERE id = ?",
"DELETE FROM " + database.entity(OrderProductEntity.class).getTable() + " WHERE order_id = ?"
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
import io.yupiik.fusion.framework.build.api.lifecycle.Init;
import io.yupiik.fusion.json.JsonMapper;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -40,6 +39,8 @@ public class ProductService {

private final JsonMapper jsonMapper;

private List<Product> products;

public ProductService(final BackendConfiguration configuration, final JsonMapper jsonMapper) {
this.configuration = configuration;
this.jsonMapper = jsonMapper;
Expand All @@ -54,13 +55,14 @@ public void loadDemoData() {
} catch (final Exception exception) {
logger.severe("Unable to load product inventory init list from resource file");
}
products = productInventory.values().stream().toList();
}

public Product findProduct(final String id) {
return productInventory.get(id);
}

public List<Product> findProducts() {
return new ArrayList<>(productInventory.values());
return products;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ void updateOrder(@Fusion final TestClient client, @TaskResult(CreateOrder.class)
.method("PUT",
HttpRequest.BodyPublishers.ofString("""
{
"id": "$id",
"description": "Mobile Line",
"name": "Mobile Line",
"status": "pendingActive",
Expand All @@ -130,7 +131,7 @@ void updateOrder(@Fusion final TestClient client, @TaskResult(CreateOrder.class)
}
]
}
""", StandardCharsets.UTF_8)
""".replace("$id", id), StandardCharsets.UTF_8)
)
.uri(uri.resolve("/order/" + id))
.build(),
Expand Down

0 comments on commit d11649f

Please sign in to comment.