Uwe Schäfer & Jörg Adler
Mercateo AG
Jörg Adler, Softwareentwickler
joerg.adler@mercateo.com
@joerg_adler
Uwe Schäfer, Softwareentwickler
uwe.schaefer@mercateo.com
@codesmell
Welche SQL Statements löschen Daten?
Insert
Update
Delete
Wissen Softwareentwickler, welche Daten in Zukunft gebraucht werden und damit nicht gelöscht werden dürfen?
t
add 2 t-shirts
change to 1 t-shirt
add socks
remove socks
order
Article
Cart
Line-Item
ID
CREATION_TIME
CUSTOMER_ID
CLOSED
ID
CART_ID
ARTICLE_ID
QUANTITY
ID
NAME
DESCRIPTION
PRICE
INSERT INTO CARTS VALUES (NEWID(), NOW(), customer_id, 0)
INSERT INTO LINE_ITEMS VALUES(NEWID(), cart_id, article_id_t_shirt, 2)
UPDATE LINE_ITEMS SET QUANTITY = 1 WHERE CART_ID = cart_id AND ARTICLE_ID = article_id_t_shirt
INSERT INTO LINE_ITEMS VALUES(NEWID(), cart_id, article_id_socks, 1)
DELETE FROM LINE_ITEMS WHERE CART_ID = cart_id AND ARTICLE_ID = article_id_socks
bei einem Artikelupdate:
UPDATE ARTICLES SET PRICE = 100 WHERE ID = article_id_t_shirt
INSERT INTO LINE_ITEMS VALUES(NEWID(), cart_id, article_id_t_shirt, 1)
UPDATE CARTS SET CLOSED = 1 WHERE ID = cart_id
Schreiben aller Änderungen (Events) im System in ein Log
Events sind Fakten, die in der Vergangenheit liegen
Events sind unveränderlich
"Events erzählen eine Geschichte"
LineItemAddedToCart
LineItemRemovedFromCart
LineItemQuantityChanged
CheckoutStarted
cartId
quantity
articleId
...
cartId
articleId
...
shoppingCartId
quantity
articleId
...
cartId
...
ArcticleImported
articleId
name
description
...
add 2 t-shirts
change to 1 t-shirt
add socks
remove socks
order
LineItemAdded
ToCart
LineItemQuantity
Changed
LineItemAdded
ToCart
LineItemRemoved
FromCart
CheckoutStarted
t
Log
Shop-Gui
Reporting
Artikelsuche
icons: https://icons8.com
Warenkörbe
Reports
Artikel-Index
Status des Systems für jeden Zeitpunkt rekonstruierbar (time machine)
Eine Stelle der Wahrheit für das ganze System
jederzeit neue Lesemodelle aufbaubar
debugging ist sehr viel einfacher (auch lokal)
Auditing
header:
{
"id" : "1bd92ce5-b419-4adc-879f-19b20c1bf9e0",
"ns" : "jugsaxony",
"type" : "camp.jugsaxony.es.event.cart.LineItemAdded",
"aggIds" : [
"5c000000-0000-0000-0000-000000000001"
]
}
payload:
{
"cartId" : "5c000000-0000-0000-0000-000000000001",
"customerId": "c0000000-0000-0000-0000-000000000001",
"articleId" : "a0000000-0000-0000-0000-000000000002",
"quantity" : 2
}
Beispiel FactCast / JSON
@Builder @Getter
public class LineItemAdded implements Event{
UUID customerId; // not stricly necessary, but helpful Context information
@AggregateId
UUID cartId;
UUID articleId;
int quantity;
}
LineItemAdded event = new LineItemAdded()
.articleId(tshirtId)
.cartId(cartId)
.customerId(customerId)
.quantity(2);
eventStore.publish( Util.toFact(event) );
@Data
public class Cart {
final UUID customerId;
final UUID cartId;
final Map<UUID, LineItem> lineItems = new HashMap<>();
boolean closed = false;
}
@Data
public class LineItem {
final UUID articleId;
int quantity = 1;
}
class CartReadModel extends PullReadModel {
@Getter private Cart cart;
CartReadModel(FactCast eventStore, UUID cartId) {
super(eventStore,()->FactSpec.ns("jugsaxony").aggId(cartId));
cart = new Cart(cartId);
}
public void apply(LineItemAdded event) {
LineItem li = new LineItem(event.getArticleId());
li.setQuantity(event.getQuantity());
cart.getLineItems().put(li.getArticleId(), li);
}
public void apply(LineItemRemoved event) {
cart.getLineItems().remove(event.getArticleId());
}
public void apply(LineItemQuantityChanged fact) {
cart.getLineItems().get(fact.getArticleId()).setQuantity(fact.getQuantity());
}
public void apply(CheckoutStarted event) {
cart.setClosed(true);
}
}
public class PullReadModel extends ReadModel {
public PullReadModel(FactCast eventStore, Supplier<FactSpec> spec) {
super(eventStore, spec);
}
private void pullFromScratch() {
eventStore.subscribeToFacts(
SubscriptionRequest.catchup(factsOfInterest).fromScratch(),
this)
.awaitComplete();
}
//...
}
public interface CartRepository {
Cart findByCartId(UUID cartId);
}
@Component @RequiredArgsConstructor
public class CartRepositoryImpl implements CartRepository {
final FactCast eventStore;
@Override
public Cart findByCartId(UUID cartId) {
return new CartReadModel(eventStore, cartId).pull();
}
}
@Service @RequiredArgsConstructor
public class CartSnapshotRepositoryImpl implements CartRepository {
final FactCast eventStore;
final Map<UUID, CartReadModel> modelCache = new LRUMap<>();
@Override
public Cart findByCartId(UUID cartId) {
CartReadModel readModel = modelCache.computeIfAbsent(cartId,
id -> new CartReadModel( eventStore, id ) );
readModel.pull();
return readModel.getCart();
}
}
public class PullReadModel extends ReadModel {
// ...
public void pull() {
if (lastFactConsumed == null) {
pullFromScratch();
} else {
eventStore.subscribeToFacts(
SubscriptionRequest.catchup(factsOfInterest)
.from(lastFactConsumed), this)
.awaitComplete();
}
}
}
@Service
public class RecommendedArticles extends PushReadModel {
final Map<UUID, Set<UUID>> recommendedArticlesByCustomer =
new ConcurrentHashMap<>();
public void apply(LineItemRemoved event) {
Set<UUID> list = getForCustomer(event.getCustomerId());
list.add(event.getArticleId());
}
public void apply(CheckoutStarted event) {
List<UUID> articlesInCart = event.getLineItems()
.stream()
.map(LineItem::getArticleId)
.collect(Collectors.toList());
Set<UUID> list = getForCustomer(event.getCustomerId());
list.removeAll(articlesInCart);
}
// ...
}
public class PushReadModel extends ReadModel implements SmartInitializingSingleton {
public PushReadModel(FactCast eventStore, Supplier<FactSpec> spec) {
super(eventStore, spec);
}
@Override
public void afterSingletonsInstantiated() {
eventStore.subscribeToFacts(
SubscriptionRequest.follow(factsOfInterest).fromScratch(), this);
}
}
@service @RequiredArgsConstructor public class CartService {
public void addToCart(UUID cartId, UUID articleId, UUID customerId, int quantity) {
// acquire necessary locks
Cart currentCart = cartRepo.findByCartId(cartId);
if (currentCart.isClosed()) {
throw new IllegalStateException("Already closed");
}
if (currentCart.getLineItems().containsKey(articleId)) {
// increaseQuantity of the corresponding LineItem;
LineItem lineItem = currentCart.getLineItems().get(articleId);
LineItemQuantityChanged event = new LineItemQuantityChanged()...
eventStore.publish(Util.toFact(event));
} else {
// add new LineItem
LineItemAdded event = new LineItemAdded()...
eventStore.publish(Util.toFact(event));
}
// release locks
}
// ...
}
Diese Folien stehen unter CC BY-SA 4.0 Lizenz
https://creativecommons.org/licenses/by-sa/4.0/