diff --git a/patterns/cqrs-es/pom.xml b/patterns/cqrs-es/pom.xml new file mode 100644 index 0000000000..3c54038837 --- /dev/null +++ b/patterns/cqrs-es/pom.xml @@ -0,0 +1,30 @@ + + 4.0.0 + cqrs-es + 1.0-SNAPSHOT + cqrs-es + + com.baeldung + patterns + 1.0.0-SNAPSHOT + + + 1.8 + 1.8 + + + + org.projectlombok + lombok + 1.18.12 + + + junit + junit + 4.13 + test + + + \ No newline at end of file diff --git a/patterns/cqrs-es/src/main/java/com/baeldung/patterns/cqrs/aggregates/UserAggregate.java b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/cqrs/aggregates/UserAggregate.java new file mode 100644 index 0000000000..96476dc15a --- /dev/null +++ b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/cqrs/aggregates/UserAggregate.java @@ -0,0 +1,30 @@ +package com.baeldung.patterns.cqrs.aggregates; + +import com.baeldung.patterns.cqrs.commands.CreateUserCommand; +import com.baeldung.patterns.cqrs.commands.UpdateUserCommand; +import com.baeldung.patterns.cqrs.repository.UserWriteRepository; +import com.baeldung.patterns.domain.User; + +public class UserAggregate { + + private UserWriteRepository writeRepository; + + public UserAggregate(UserWriteRepository repository) { + this.writeRepository = repository; + } + + public User handleCreateUserCommand(CreateUserCommand command) { + User user = new User(command.getUserId(), command.getFirstName(), command.getLastName()); + writeRepository.addUser(user.getUserid(), user); + return user; + } + + public User handleUpdateUserCommand(UpdateUserCommand command) { + User user = writeRepository.getUser(command.getUserId()); + user.setAddresses(command.getAddresses()); + user.setContacts(command.getContacts()); + writeRepository.addUser(user.getUserid(), user); + return user; + } + +} diff --git a/patterns/cqrs-es/src/main/java/com/baeldung/patterns/cqrs/commands/CreateUserCommand.java b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/cqrs/commands/CreateUserCommand.java new file mode 100644 index 0000000000..362804c610 --- /dev/null +++ b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/cqrs/commands/CreateUserCommand.java @@ -0,0 +1,14 @@ +package com.baeldung.patterns.cqrs.commands; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class CreateUserCommand { + + private String userId; + private String firstName; + private String lastName; + +} diff --git a/patterns/cqrs-es/src/main/java/com/baeldung/patterns/cqrs/commands/UpdateUserCommand.java b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/cqrs/commands/UpdateUserCommand.java new file mode 100644 index 0000000000..58a4889458 --- /dev/null +++ b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/cqrs/commands/UpdateUserCommand.java @@ -0,0 +1,20 @@ +package com.baeldung.patterns.cqrs.commands; + +import java.util.HashSet; +import java.util.Set; + +import com.baeldung.patterns.domain.Address; +import com.baeldung.patterns.domain.Contact; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class UpdateUserCommand { + + private String userId; + private Set
addresses = new HashSet<>(); + private Set contacts = new HashSet<>(); + +} diff --git a/patterns/cqrs-es/src/main/java/com/baeldung/patterns/cqrs/projections/UserProjection.java b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/cqrs/projections/UserProjection.java new file mode 100644 index 0000000000..40a56e727f --- /dev/null +++ b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/cqrs/projections/UserProjection.java @@ -0,0 +1,37 @@ +package com.baeldung.patterns.cqrs.projections; + +import java.util.Set; + +import com.baeldung.patterns.cqrs.queries.AddressByRegionQuery; +import com.baeldung.patterns.cqrs.queries.ContactByTypeQuery; +import com.baeldung.patterns.cqrs.repository.UserReadRepository; +import com.baeldung.patterns.domain.Address; +import com.baeldung.patterns.domain.Contact; +import com.baeldung.patterns.domain.UserAddress; +import com.baeldung.patterns.domain.UserContact; + +public class UserProjection { + + private UserReadRepository repository; + + public UserProjection(UserReadRepository repository) { + this.repository = repository; + } + + public Set handle(ContactByTypeQuery query) throws Exception { + UserContact userContact = repository.getUserContact(query.getUserId()); + if (userContact == null) + throw new Exception("User does not exist."); + return userContact.getContactByType() + .get(query.getContactType()); + } + + public Set
handle(AddressByRegionQuery query) throws Exception { + UserAddress userAddress = repository.getUserAddress(query.getUserId()); + if (userAddress == null) + throw new Exception("User does not exist."); + return userAddress.getAddressByRegion() + .get(query.getState()); + } + +} diff --git a/patterns/cqrs-es/src/main/java/com/baeldung/patterns/cqrs/projectors/UserProjector.java b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/cqrs/projectors/UserProjector.java new file mode 100644 index 0000000000..0344c352d9 --- /dev/null +++ b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/cqrs/projectors/UserProjector.java @@ -0,0 +1,49 @@ +package com.baeldung.patterns.cqrs.projectors; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import com.baeldung.patterns.cqrs.repository.UserReadRepository; +import com.baeldung.patterns.domain.Address; +import com.baeldung.patterns.domain.Contact; +import com.baeldung.patterns.domain.User; +import com.baeldung.patterns.domain.UserAddress; +import com.baeldung.patterns.domain.UserContact; + +public class UserProjector { + + UserReadRepository readRepository = new UserReadRepository(); + + public UserProjector(UserReadRepository readRepository) { + this.readRepository = readRepository; + } + + public void project(User user) { + UserContact userContact = Optional.ofNullable(readRepository.getUserContact(user.getUserid())) + .orElse(new UserContact()); + Map> contactByType = new HashMap<>(); + for (Contact contact : user.getContacts()) { + Set contacts = Optional.ofNullable(contactByType.get(contact.getType())) + .orElse(new HashSet<>()); + contacts.add(contact); + contactByType.put(contact.getType(), contacts); + } + userContact.setContactByType(contactByType); + readRepository.addUserContact(user.getUserid(), userContact); + + UserAddress userAddress = Optional.ofNullable(readRepository.getUserAddress(user.getUserid())) + .orElse(new UserAddress()); + Map> addressByRegion = new HashMap<>(); + for (Address address : user.getAddresses()) { + Set
addresses = Optional.ofNullable(addressByRegion.get(address.getState())) + .orElse(new HashSet<>()); + addresses.add(address); + addressByRegion.put(address.getState(), addresses); + } + userAddress.setAddressByRegion(addressByRegion); + readRepository.addUserAddress(user.getUserid(), userAddress); + } +} diff --git a/patterns/cqrs-es/src/main/java/com/baeldung/patterns/cqrs/queries/AddressByRegionQuery.java b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/cqrs/queries/AddressByRegionQuery.java new file mode 100644 index 0000000000..4a0f0769f2 --- /dev/null +++ b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/cqrs/queries/AddressByRegionQuery.java @@ -0,0 +1,12 @@ +package com.baeldung.patterns.cqrs.queries; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class AddressByRegionQuery { + + private String userId; + private String state; +} diff --git a/patterns/cqrs-es/src/main/java/com/baeldung/patterns/cqrs/queries/ContactByTypeQuery.java b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/cqrs/queries/ContactByTypeQuery.java new file mode 100644 index 0000000000..b6c271b472 --- /dev/null +++ b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/cqrs/queries/ContactByTypeQuery.java @@ -0,0 +1,12 @@ +package com.baeldung.patterns.cqrs.queries; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class ContactByTypeQuery { + + private String userId; + private String contactType; +} diff --git a/patterns/cqrs-es/src/main/java/com/baeldung/patterns/cqrs/repository/UserReadRepository.java b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/cqrs/repository/UserReadRepository.java new file mode 100644 index 0000000000..a7ef2f6f96 --- /dev/null +++ b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/cqrs/repository/UserReadRepository.java @@ -0,0 +1,31 @@ +package com.baeldung.patterns.cqrs.repository; + +import java.util.HashMap; +import java.util.Map; + +import com.baeldung.patterns.domain.UserAddress; +import com.baeldung.patterns.domain.UserContact; + +public class UserReadRepository { + + private Map userAddress = new HashMap<>(); + + private Map userContact = new HashMap<>(); + + public void addUserAddress(String id, UserAddress user) { + userAddress.put(id, user); + } + + public UserAddress getUserAddress(String id) { + return userAddress.get(id); + } + + public void addUserContact(String id, UserContact user) { + userContact.put(id, user); + } + + public UserContact getUserContact(String id) { + return userContact.get(id); + } + +} diff --git a/patterns/cqrs-es/src/main/java/com/baeldung/patterns/cqrs/repository/UserWriteRepository.java b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/cqrs/repository/UserWriteRepository.java new file mode 100644 index 0000000000..8636e36225 --- /dev/null +++ b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/cqrs/repository/UserWriteRepository.java @@ -0,0 +1,20 @@ +package com.baeldung.patterns.cqrs.repository; + +import java.util.HashMap; +import java.util.Map; + +import com.baeldung.patterns.domain.User; + +public class UserWriteRepository { + + private Map store = new HashMap<>(); + + public void addUser(String id, User user) { + store.put(id, user); + } + + public User getUser(String id) { + return store.get(id); + } + +} diff --git a/patterns/cqrs-es/src/main/java/com/baeldung/patterns/crud/repository/UserRepository.java b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/crud/repository/UserRepository.java new file mode 100644 index 0000000000..b22d40e6e0 --- /dev/null +++ b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/crud/repository/UserRepository.java @@ -0,0 +1,20 @@ +package com.baeldung.patterns.crud.repository; + +import java.util.HashMap; +import java.util.Map; + +import com.baeldung.patterns.domain.User; + +public class UserRepository { + + private Map store = new HashMap<>(); + + public void addUser(String id, User user) { + store.put(id, user); + } + + public User getUser(String id) { + return store.get(id); + } + +} diff --git a/patterns/cqrs-es/src/main/java/com/baeldung/patterns/crud/service/UserService.java b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/crud/service/UserService.java new file mode 100644 index 0000000000..d21a796304 --- /dev/null +++ b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/crud/service/UserService.java @@ -0,0 +1,55 @@ +package com.baeldung.patterns.crud.service; + +import java.util.Set; +import java.util.stream.Collectors; + +import com.baeldung.patterns.crud.repository.UserRepository; +import com.baeldung.patterns.domain.Address; +import com.baeldung.patterns.domain.Contact; +import com.baeldung.patterns.domain.User; + +public class UserService { + + private UserRepository repository; + + public UserService(UserRepository repository) { + this.repository = repository; + } + + public void createUser(String userId, String firstName, String lastName) { + User user = new User(userId, firstName, lastName); + repository.addUser(userId, user); + } + + public void updateUser(String userId, Set contacts, Set
addresses) throws Exception { + User user = repository.getUser(userId); + if (user == null) + throw new Exception("User does not exist."); + user.setContacts(contacts); + user.setAddresses(addresses); + repository.addUser(userId, user); + } + + public Set getContactByType(String userId, String contactType) throws Exception { + User user = repository.getUser(userId); + if (user == null) + throw new Exception("User does not exit."); + Set contacts = user.getContacts(); + return contacts.stream() + .filter(c -> c.getType() + .equals(contactType)) + .collect(Collectors.toSet()); + } + + public Set
getAddressByRegion(String userId, String state) throws Exception { + User user = repository.getUser(userId); + if (user == null) + throw new Exception("User does not exist."); + Set
addresses = user.getAddresses(); + return addresses.stream() + .filter(a -> a.getState() + .equals(state)) + .collect(Collectors.toSet()); + } + +} diff --git a/patterns/cqrs-es/src/main/java/com/baeldung/patterns/domain/Address.java b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/domain/Address.java new file mode 100644 index 0000000000..dfaa724434 --- /dev/null +++ b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/domain/Address.java @@ -0,0 +1,14 @@ +package com.baeldung.patterns.domain; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class Address { + + private String city; + private String state; + private String postcode; + +} diff --git a/patterns/cqrs-es/src/main/java/com/baeldung/patterns/domain/Contact.java b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/domain/Contact.java new file mode 100644 index 0000000000..a151cdb4ff --- /dev/null +++ b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/domain/Contact.java @@ -0,0 +1,13 @@ +package com.baeldung.patterns.domain; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class Contact { + + private String type; + private String detail; + +} diff --git a/patterns/cqrs-es/src/main/java/com/baeldung/patterns/domain/User.java b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/domain/User.java new file mode 100644 index 0000000000..8f59723894 --- /dev/null +++ b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/domain/User.java @@ -0,0 +1,22 @@ +package com.baeldung.patterns.domain; + +import java.util.HashSet; +import java.util.Set; + +import lombok.Data; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +@Data +@RequiredArgsConstructor +public class User { + @NonNull + private String userid; + @NonNull + private String firstname; + @NonNull + private String lastname; + private Set contacts = new HashSet<>(); + private Set
addresses = new HashSet<>(); + +} diff --git a/patterns/cqrs-es/src/main/java/com/baeldung/patterns/domain/UserAddress.java b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/domain/UserAddress.java new file mode 100644 index 0000000000..8105a53265 --- /dev/null +++ b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/domain/UserAddress.java @@ -0,0 +1,14 @@ +package com.baeldung.patterns.domain; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import lombok.Data; + +@Data +public class UserAddress { + + private Map> addressByRegion = new HashMap<>(); + +} diff --git a/patterns/cqrs-es/src/main/java/com/baeldung/patterns/domain/UserContact.java b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/domain/UserContact.java new file mode 100644 index 0000000000..1768dadc39 --- /dev/null +++ b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/domain/UserContact.java @@ -0,0 +1,14 @@ +package com.baeldung.patterns.domain; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import lombok.Data; + +@Data +public class UserContact { + + private Map> contactByType = new HashMap<>(); + +} diff --git a/patterns/cqrs-es/src/main/java/com/baeldung/patterns/es/events/Event.java b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/es/events/Event.java new file mode 100644 index 0000000000..4718450cac --- /dev/null +++ b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/es/events/Event.java @@ -0,0 +1,15 @@ +package com.baeldung.patterns.es.events; + +import java.util.Date; +import java.util.UUID; + +import lombok.ToString; + +@ToString +public abstract class Event { + + public final UUID id = UUID.randomUUID(); + + public final Date created = new Date(); + +} diff --git a/patterns/cqrs-es/src/main/java/com/baeldung/patterns/es/events/UserAddressAddedEvent.java b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/es/events/UserAddressAddedEvent.java new file mode 100644 index 0000000000..caa640798b --- /dev/null +++ b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/es/events/UserAddressAddedEvent.java @@ -0,0 +1,16 @@ +package com.baeldung.patterns.es.events; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@AllArgsConstructor +@EqualsAndHashCode(callSuper = false) +public class UserAddressAddedEvent extends Event { + + private String city; + private String state; + private String postCode; + +} diff --git a/patterns/cqrs-es/src/main/java/com/baeldung/patterns/es/events/UserAddressRemovedEvent.java b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/es/events/UserAddressRemovedEvent.java new file mode 100644 index 0000000000..f0702d8810 --- /dev/null +++ b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/es/events/UserAddressRemovedEvent.java @@ -0,0 +1,16 @@ +package com.baeldung.patterns.es.events; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@AllArgsConstructor +@EqualsAndHashCode(callSuper = false) +public class UserAddressRemovedEvent extends Event { + + private String city; + private String state; + private String postCode; + +} diff --git a/patterns/cqrs-es/src/main/java/com/baeldung/patterns/es/events/UserContactAddedEvent.java b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/es/events/UserContactAddedEvent.java new file mode 100644 index 0000000000..0bce2f997e --- /dev/null +++ b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/es/events/UserContactAddedEvent.java @@ -0,0 +1,15 @@ +package com.baeldung.patterns.es.events; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@AllArgsConstructor +@EqualsAndHashCode(callSuper = false) +public class UserContactAddedEvent extends Event { + + private String contactType; + private String contactDetails; + +} diff --git a/patterns/cqrs-es/src/main/java/com/baeldung/patterns/es/events/UserContactRemovedEvent.java b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/es/events/UserContactRemovedEvent.java new file mode 100644 index 0000000000..9c043971e7 --- /dev/null +++ b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/es/events/UserContactRemovedEvent.java @@ -0,0 +1,15 @@ +package com.baeldung.patterns.es.events; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@AllArgsConstructor +@EqualsAndHashCode(callSuper = false) +public class UserContactRemovedEvent extends Event { + + private String contactType; + private String contactDetails; + +} diff --git a/patterns/cqrs-es/src/main/java/com/baeldung/patterns/es/events/UserCreatedEvent.java b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/es/events/UserCreatedEvent.java new file mode 100644 index 0000000000..8e725cd667 --- /dev/null +++ b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/es/events/UserCreatedEvent.java @@ -0,0 +1,16 @@ +package com.baeldung.patterns.es.events; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@AllArgsConstructor +@EqualsAndHashCode(callSuper = false) +public class UserCreatedEvent extends Event { + + private String userId; + private String firstName; + private String lastName; + +} diff --git a/patterns/cqrs-es/src/main/java/com/baeldung/patterns/es/repository/EventStore.java b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/es/repository/EventStore.java new file mode 100644 index 0000000000..87914d8c12 --- /dev/null +++ b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/es/repository/EventStore.java @@ -0,0 +1,29 @@ +package com.baeldung.patterns.es.repository; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.baeldung.patterns.es.events.Event; + +public class EventStore { + + private Map> store = new HashMap<>(); + + public void addEvent(String id, Event event) { + List events = store.get(id); + if (events == null) { + events = new ArrayList(); + events.add(event); + store.put(id, events); + } else { + events.add(event); + } + } + + public List getEvents(String id) { + return store.get(id); + } + +} diff --git a/patterns/cqrs-es/src/main/java/com/baeldung/patterns/es/service/UserService.java b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/es/service/UserService.java new file mode 100644 index 0000000000..9650c4294f --- /dev/null +++ b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/es/service/UserService.java @@ -0,0 +1,72 @@ +package com.baeldung.patterns.es.service; + +import java.util.Set; +import java.util.stream.Collectors; + +import com.baeldung.patterns.domain.Address; +import com.baeldung.patterns.domain.Contact; +import com.baeldung.patterns.domain.User; +import com.baeldung.patterns.es.events.UserAddressAddedEvent; +import com.baeldung.patterns.es.events.UserAddressRemovedEvent; +import com.baeldung.patterns.es.events.UserContactAddedEvent; +import com.baeldung.patterns.es.events.UserContactRemovedEvent; +import com.baeldung.patterns.es.events.UserCreatedEvent; +import com.baeldung.patterns.es.repository.EventStore; + +public class UserService { + + private EventStore repository; + + public UserService(EventStore repository) { + this.repository = repository; + } + + public void createUser(String userId, String firstName, String lastName) { + repository.addEvent(userId, new UserCreatedEvent(userId, firstName, lastName)); + } + + public void updateUser(String userId, Set contacts, Set
addresses) throws Exception { + User user = UserUtility.recreateUserState(repository, userId); + if (user == null) + throw new Exception("User does not exist."); + + user.getContacts() + .stream() + .filter(c -> !contacts.contains(c)) + .forEach(c -> repository.addEvent(userId, new UserContactRemovedEvent(c.getType(), c.getDetail()))); + contacts.stream() + .filter(c -> !user.getContacts() + .contains(c)) + .forEach(c -> repository.addEvent(userId, new UserContactAddedEvent(c.getType(), c.getDetail()))); + user.getAddresses() + .stream() + .filter(a -> !addresses.contains(a)) + .forEach(a -> repository.addEvent(userId, new UserAddressRemovedEvent(a.getCity(), a.getState(), a.getPostcode()))); + addresses.stream() + .filter(a -> !user.getAddresses() + .contains(a)) + .forEach(a -> repository.addEvent(userId, new UserAddressAddedEvent(a.getCity(), a.getState(), a.getPostcode()))); + } + + public Set getContactByType(String userId, String contactType) throws Exception { + User user = UserUtility.recreateUserState(repository, userId); + if (user == null) + throw new Exception("User does not exist."); + return user.getContacts() + .stream() + .filter(c -> c.getType() + .equals(contactType)) + .collect(Collectors.toSet()); + } + + public Set
getAddressByRegion(String userId, String state) throws Exception { + User user = UserUtility.recreateUserState(repository, userId); + if (user == null) + throw new Exception("User does not exist."); + return user.getAddresses() + .stream() + .filter(a -> a.getState() + .equals(state)) + .collect(Collectors.toSet()); + } +} diff --git a/patterns/cqrs-es/src/main/java/com/baeldung/patterns/es/service/UserUtility.java b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/es/service/UserUtility.java new file mode 100644 index 0000000000..e44e404588 --- /dev/null +++ b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/es/service/UserUtility.java @@ -0,0 +1,62 @@ +package com.baeldung.patterns.es.service; + +import java.util.List; +import java.util.UUID; + +import com.baeldung.patterns.domain.Address; +import com.baeldung.patterns.domain.Contact; +import com.baeldung.patterns.domain.User; +import com.baeldung.patterns.es.events.Event; +import com.baeldung.patterns.es.events.UserAddressAddedEvent; +import com.baeldung.patterns.es.events.UserAddressRemovedEvent; +import com.baeldung.patterns.es.events.UserContactAddedEvent; +import com.baeldung.patterns.es.events.UserContactRemovedEvent; +import com.baeldung.patterns.es.events.UserCreatedEvent; +import com.baeldung.patterns.es.repository.EventStore; + +public class UserUtility { + + public static User recreateUserState(EventStore store, String userId) { + User user = null; + + List events = store.getEvents(userId); + for (Event event : events) { + if (event instanceof UserCreatedEvent) { + UserCreatedEvent e = (UserCreatedEvent) event; + user = new User(UUID.randomUUID() + .toString(), e.getFirstName(), e.getLastName()); + } + if (event instanceof UserAddressAddedEvent) { + UserAddressAddedEvent e = (UserAddressAddedEvent) event; + Address address = new Address(e.getCity(), e.getState(), e.getPostCode()); + if (user != null) + user.getAddresses() + .add(address); + } + if (event instanceof UserAddressRemovedEvent) { + UserAddressRemovedEvent e = (UserAddressRemovedEvent) event; + Address address = new Address(e.getCity(), e.getState(), e.getPostCode()); + if (user != null) + user.getAddresses() + .remove(address); + } + if (event instanceof UserContactAddedEvent) { + UserContactAddedEvent e = (UserContactAddedEvent) event; + Contact contact = new Contact(e.getContactType(), e.getContactDetails()); + if (user != null) + user.getContacts() + .add(contact); + } + if (event instanceof UserContactRemovedEvent) { + UserContactRemovedEvent e = (UserContactRemovedEvent) event; + Contact contact = new Contact(e.getContactType(), e.getContactDetails()); + if (user != null) + user.getContacts() + .remove(contact); + } + } + + return user; + } + +} diff --git a/patterns/cqrs-es/src/main/java/com/baeldung/patterns/escqrs/aggregates/UserAggregate.java b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/escqrs/aggregates/UserAggregate.java new file mode 100644 index 0000000000..04bfeb634d --- /dev/null +++ b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/escqrs/aggregates/UserAggregate.java @@ -0,0 +1,87 @@ +package com.baeldung.patterns.escqrs.aggregates; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import com.baeldung.patterns.cqrs.commands.CreateUserCommand; +import com.baeldung.patterns.cqrs.commands.UpdateUserCommand; +import com.baeldung.patterns.domain.Address; +import com.baeldung.patterns.domain.Contact; +import com.baeldung.patterns.domain.User; +import com.baeldung.patterns.es.events.Event; +import com.baeldung.patterns.es.events.UserAddressAddedEvent; +import com.baeldung.patterns.es.events.UserAddressRemovedEvent; +import com.baeldung.patterns.es.events.UserContactAddedEvent; +import com.baeldung.patterns.es.events.UserContactRemovedEvent; +import com.baeldung.patterns.es.events.UserCreatedEvent; +import com.baeldung.patterns.es.repository.EventStore; +import com.baeldung.patterns.es.service.UserUtility; + +public class UserAggregate { + + private EventStore writeRepository; + + public UserAggregate(EventStore repository) { + this.writeRepository = repository; + } + + public List handleCreateUserCommand(CreateUserCommand command) { + UserCreatedEvent event = new UserCreatedEvent(command.getUserId(), command.getFirstName(), command.getLastName()); + writeRepository.addEvent(command.getUserId(), event); + return Arrays.asList(event); + } + + public List handleUpdateUserCommand(UpdateUserCommand command) { + User user = UserUtility.recreateUserState(writeRepository, command.getUserId()); + List events = new ArrayList<>(); + + List contactsToRemove = user.getContacts() + .stream() + .filter(c -> !command.getContacts() + .contains(c)) + .collect(Collectors.toList()); + for (Contact contact : contactsToRemove) { + UserContactRemovedEvent contactRemovedEvent = new UserContactRemovedEvent(contact.getType(), contact.getDetail()); + events.add(contactRemovedEvent); + writeRepository.addEvent(command.getUserId(), contactRemovedEvent); + } + + List contactsToAdd = command.getContacts() + .stream() + .filter(c -> !user.getContacts() + .contains(c)) + .collect(Collectors.toList()); + for (Contact contact : contactsToAdd) { + UserContactAddedEvent contactAddedEvent = new UserContactAddedEvent(contact.getType(), contact.getDetail()); + events.add(contactAddedEvent); + writeRepository.addEvent(command.getUserId(), contactAddedEvent); + } + + List
addressesToRemove = user.getAddresses() + .stream() + .filter(a -> !command.getAddresses() + .contains(a)) + .collect(Collectors.toList()); + for (Address address : addressesToRemove) { + UserAddressRemovedEvent addressRemovedEvent = new UserAddressRemovedEvent(address.getCity(), address.getState(), address.getPostcode()); + events.add(addressRemovedEvent); + writeRepository.addEvent(command.getUserId(), addressRemovedEvent); + } + + List
addressesToAdd = command.getAddresses() + .stream() + .filter(a -> !user.getAddresses() + .contains(a)) + .collect(Collectors.toList()); + for (Address address : addressesToAdd) { + UserAddressAddedEvent addressAddedEvent = new UserAddressAddedEvent(address.getCity(), address.getState(), address.getPostcode()); + events.add(addressAddedEvent); + writeRepository.addEvent(command.getUserId(), addressAddedEvent); + } + + return events; + } + +} diff --git a/patterns/cqrs-es/src/main/java/com/baeldung/patterns/escqrs/projectors/UserProjector.java b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/escqrs/projectors/UserProjector.java new file mode 100644 index 0000000000..d388abe7cb --- /dev/null +++ b/patterns/cqrs-es/src/main/java/com/baeldung/patterns/escqrs/projectors/UserProjector.java @@ -0,0 +1,91 @@ +package com.baeldung.patterns.escqrs.projectors; + +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import com.baeldung.patterns.cqrs.repository.UserReadRepository; +import com.baeldung.patterns.domain.Address; +import com.baeldung.patterns.domain.Contact; +import com.baeldung.patterns.domain.UserAddress; +import com.baeldung.patterns.domain.UserContact; +import com.baeldung.patterns.es.events.Event; +import com.baeldung.patterns.es.events.UserAddressAddedEvent; +import com.baeldung.patterns.es.events.UserAddressRemovedEvent; +import com.baeldung.patterns.es.events.UserContactAddedEvent; +import com.baeldung.patterns.es.events.UserContactRemovedEvent; + +public class UserProjector { + + UserReadRepository readRepository = new UserReadRepository(); + + public UserProjector(UserReadRepository readRepository) { + this.readRepository = readRepository; + } + + public void project(String userId, List events) { + + for (Event event : events) { + if (event instanceof UserAddressAddedEvent) + apply(userId, (UserAddressAddedEvent) event); + if (event instanceof UserAddressRemovedEvent) + apply(userId, (UserAddressRemovedEvent) event); + if (event instanceof UserContactAddedEvent) + apply(userId, (UserContactAddedEvent) event); + if (event instanceof UserContactRemovedEvent) + apply(userId, (UserContactRemovedEvent) event); + } + + } + + public void apply(String userId, UserAddressAddedEvent event) { + Address address = new Address(event.getCity(), event.getState(), event.getPostCode()); + UserAddress userAddress = Optional.ofNullable(readRepository.getUserAddress(userId)) + .orElse(new UserAddress()); + Set
addresses = Optional.ofNullable(userAddress.getAddressByRegion() + .get(address.getState())) + .orElse(new HashSet<>()); + addresses.add(address); + userAddress.getAddressByRegion() + .put(address.getState(), addresses); + readRepository.addUserAddress(userId, userAddress); + } + + public void apply(String userId, UserAddressRemovedEvent event) { + Address address = new Address(event.getCity(), event.getState(), event.getPostCode()); + UserAddress userAddress = readRepository.getUserAddress(userId); + if (userAddress != null) { + Set
addresses = userAddress.getAddressByRegion() + .get(address.getState()); + if (addresses != null) + addresses.remove(address); + readRepository.addUserAddress(userId, userAddress); + } + } + + public void apply(String userId, UserContactAddedEvent event) { + Contact contact = new Contact(event.getContactType(), event.getContactDetails()); + UserContact userContact = Optional.ofNullable(readRepository.getUserContact(userId)) + .orElse(new UserContact()); + Set contacts = Optional.ofNullable(userContact.getContactByType() + .get(contact.getType())) + .orElse(new HashSet<>()); + contacts.add(contact); + userContact.getContactByType() + .put(contact.getType(), contacts); + readRepository.addUserContact(userId, userContact); + } + + public void apply(String userId, UserContactRemovedEvent event) { + Contact contact = new Contact(event.getContactType(), event.getContactDetails()); + UserContact userContact = readRepository.getUserContact(userId); + if (userContact != null) { + Set contacts = userContact.getContactByType() + .get(contact.getType()); + if (contacts != null) + contacts.remove(contact); + readRepository.addUserContact(userId, userContact); + } + } +} diff --git a/patterns/cqrs-es/src/test/java/com/baeldung/patterns/cqrs/ApplicationUnitTest.java b/patterns/cqrs-es/src/test/java/com/baeldung/patterns/cqrs/ApplicationUnitTest.java new file mode 100644 index 0000000000..7f68a64274 --- /dev/null +++ b/patterns/cqrs-es/src/test/java/com/baeldung/patterns/cqrs/ApplicationUnitTest.java @@ -0,0 +1,74 @@ +package com.baeldung.patterns.cqrs; + +import static org.junit.Assert.assertEquals; + +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.Before; +import org.junit.Test; + +import com.baeldung.patterns.cqrs.aggregates.UserAggregate; +import com.baeldung.patterns.cqrs.commands.CreateUserCommand; +import com.baeldung.patterns.cqrs.commands.UpdateUserCommand; +import com.baeldung.patterns.cqrs.projections.UserProjection; +import com.baeldung.patterns.cqrs.projectors.UserProjector; +import com.baeldung.patterns.cqrs.queries.AddressByRegionQuery; +import com.baeldung.patterns.cqrs.queries.ContactByTypeQuery; +import com.baeldung.patterns.cqrs.repository.UserReadRepository; +import com.baeldung.patterns.cqrs.repository.UserWriteRepository; +import com.baeldung.patterns.domain.Address; +import com.baeldung.patterns.domain.Contact; +import com.baeldung.patterns.domain.User; + +public class ApplicationUnitTest { + + private UserWriteRepository writeRepository; + private UserReadRepository readRepository; + private UserProjector projector; + private UserAggregate userAggregate; + private UserProjection userProjection; + + @Before + public void setUp() { + writeRepository = new UserWriteRepository(); + readRepository = new UserReadRepository(); + projector = new UserProjector(readRepository); + userAggregate = new UserAggregate(writeRepository); + userProjection = new UserProjection(readRepository); + } + + @Test + public void givenCQRSApplication_whenCommandRun_thenQueryShouldReturnResult() throws Exception { + String userId = UUID.randomUUID() + .toString(); + User user = null; + CreateUserCommand createUserCommand = new CreateUserCommand(userId, "Tom", "Sawyer"); + user = userAggregate.handleCreateUserCommand(createUserCommand); + projector.project(user); + + UpdateUserCommand updateUserCommand = new UpdateUserCommand(user.getUserid(), Stream.of(new Address("New York", "NY", "10001"), new Address("Los Angeles", "CA", "90001")) + .collect(Collectors.toSet()), + Stream.of(new Contact("EMAIL", "tom.sawyer@gmail.com"), new Contact("EMAIL", "tom.sawyer@rediff.com")) + .collect(Collectors.toSet())); + user = userAggregate.handleUpdateUserCommand(updateUserCommand); + projector.project(user); + + updateUserCommand = new UpdateUserCommand(userId, Stream.of(new Address("New York", "NY", "10001"), new Address("Housten", "TX", "77001")) + .collect(Collectors.toSet()), + Stream.of(new Contact("EMAIL", "tom.sawyer@gmail.com"), new Contact("PHONE", "700-000-0001")) + .collect(Collectors.toSet())); + user = userAggregate.handleUpdateUserCommand(updateUserCommand); + projector.project(user); + + ContactByTypeQuery contactByTypeQuery = new ContactByTypeQuery(userId, "EMAIL"); + assertEquals(Stream.of(new Contact("EMAIL", "tom.sawyer@gmail.com")) + .collect(Collectors.toSet()), userProjection.handle(contactByTypeQuery)); + AddressByRegionQuery addressByRegionQuery = new AddressByRegionQuery(userId, "NY"); + assertEquals(Stream.of(new Address("New York", "NY", "10001")) + .collect(Collectors.toSet()), userProjection.handle(addressByRegionQuery)); + + } + +} diff --git a/patterns/cqrs-es/src/test/java/com/baeldung/patterns/crud/ApplicationUnitTest.java b/patterns/cqrs-es/src/test/java/com/baeldung/patterns/crud/ApplicationUnitTest.java new file mode 100644 index 0000000000..3fabfe405d --- /dev/null +++ b/patterns/cqrs-es/src/test/java/com/baeldung/patterns/crud/ApplicationUnitTest.java @@ -0,0 +1,48 @@ +package com.baeldung.patterns.crud; + +import static org.junit.Assert.assertEquals; + +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.Before; +import org.junit.Test; + +import com.baeldung.patterns.crud.repository.UserRepository; +import com.baeldung.patterns.crud.service.UserService; +import com.baeldung.patterns.domain.Address; +import com.baeldung.patterns.domain.Contact; + +public class ApplicationUnitTest { + + private UserRepository repository; + + @Before + public void setUp() { + repository = new UserRepository(); + } + + @Test + public void givenCRUDApplication_whenDataCreated_thenDataCanBeFetched() throws Exception { + UserService service = new UserService(repository); + String userId = UUID.randomUUID() + .toString(); + + service.createUser(userId, "Tom", "Sawyer"); + service.updateUser(userId, Stream.of(new Contact("EMAIL", "tom.sawyer@gmail.com"), new Contact("EMAIL", "tom.sawyer@rediff.com"), new Contact("PHONE", "700-000-0001")) + .collect(Collectors.toSet()), + Stream.of(new Address("New York", "NY", "10001"), new Address("Los Angeles", "CA", "90001"), new Address("Housten", "TX", "77001")) + .collect(Collectors.toSet())); + service.updateUser(userId, Stream.of(new Contact("EMAIL", "tom.sawyer@gmail.com"), new Contact("PHONE", "700-000-0001")) + .collect(Collectors.toSet()), + Stream.of(new Address("New York", "NY", "10001"), new Address("Housten", "TX", "77001")) + .collect(Collectors.toSet())); + + assertEquals(Stream.of(new Contact("EMAIL", "tom.sawyer@gmail.com")) + .collect(Collectors.toSet()), service.getContactByType(userId, "EMAIL")); + assertEquals(Stream.of(new Address("New York", "NY", "10001")) + .collect(Collectors.toSet()), service.getAddressByRegion(userId, "NY")); + } + +} diff --git a/patterns/cqrs-es/src/test/java/com/baeldung/patterns/es/ApplicationUnitTest.java b/patterns/cqrs-es/src/test/java/com/baeldung/patterns/es/ApplicationUnitTest.java new file mode 100644 index 0000000000..61e7b4c05a --- /dev/null +++ b/patterns/cqrs-es/src/test/java/com/baeldung/patterns/es/ApplicationUnitTest.java @@ -0,0 +1,50 @@ +package com.baeldung.patterns.es; + +import static org.junit.Assert.assertEquals; + +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.Before; +import org.junit.Test; + +import com.baeldung.patterns.domain.Address; +import com.baeldung.patterns.domain.Contact; +import com.baeldung.patterns.es.repository.EventStore; +import com.baeldung.patterns.es.service.UserService; + +public class ApplicationUnitTest { + + private EventStore repository; + private UserService service; + + @Before + public void setUp() { + repository = new EventStore(); + service = new UserService(repository); + } + + @Test + public void givenCRUDApplication_whenDataCreated_thenDataCanBeFetched() throws Exception { + String userId = UUID.randomUUID() + .toString(); + + service.createUser(userId, "Tom", "Sawyer"); + service.updateUser(userId, Stream.of(new Contact("EMAIL", "tom.sawyer@gmail.com"), new Contact("EMAIL", "tom.sawyer@rediff.com"), new Contact("PHONE", "700-000-0001")) + .collect(Collectors.toSet()), + Stream.of(new Address("New York", "NY", "10001"), new Address("Los Angeles", "CA", "90001"), new Address("Housten", "TX", "77001")) + .collect(Collectors.toSet())); + service.updateUser(userId, Stream.of(new Contact("EMAIL", "tom.sawyer@gmail.com"), new Contact("PHONE", "700-000-0001")) + .collect(Collectors.toSet()), + Stream.of(new Address("New York", "NY", "10001"), new Address("Housten", "TX", "77001")) + .collect(Collectors.toSet())); + + assertEquals(Stream.of(new Contact("EMAIL", "tom.sawyer@gmail.com")) + .collect(Collectors.toSet()), service.getContactByType(userId, "EMAIL")); + assertEquals(Stream.of(new Address("New York", "NY", "10001")) + .collect(Collectors.toSet()), service.getAddressByRegion(userId, "NY")); + + } + +} diff --git a/patterns/cqrs-es/src/test/java/com/baeldung/patterns/escqrs/ApplicationUnitTest.java b/patterns/cqrs-es/src/test/java/com/baeldung/patterns/escqrs/ApplicationUnitTest.java new file mode 100644 index 0000000000..e0460b2f12 --- /dev/null +++ b/patterns/cqrs-es/src/test/java/com/baeldung/patterns/escqrs/ApplicationUnitTest.java @@ -0,0 +1,76 @@ +package com.baeldung.patterns.escqrs; + +import static org.junit.Assert.assertEquals; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.Before; +import org.junit.Test; + +import com.baeldung.patterns.cqrs.commands.CreateUserCommand; +import com.baeldung.patterns.cqrs.commands.UpdateUserCommand; +import com.baeldung.patterns.cqrs.projections.UserProjection; +import com.baeldung.patterns.cqrs.queries.AddressByRegionQuery; +import com.baeldung.patterns.cqrs.queries.ContactByTypeQuery; +import com.baeldung.patterns.cqrs.repository.UserReadRepository; +import com.baeldung.patterns.domain.Address; +import com.baeldung.patterns.domain.Contact; +import com.baeldung.patterns.es.events.Event; +import com.baeldung.patterns.es.repository.EventStore; +import com.baeldung.patterns.escqrs.aggregates.UserAggregate; +import com.baeldung.patterns.escqrs.projectors.UserProjector; + +public class ApplicationUnitTest { + + private EventStore writeRepository; + private UserReadRepository readRepository; + private UserProjector projector; + private UserAggregate userAggregate; + private UserProjection userProjection; + + @Before + public void setUp() { + writeRepository = new EventStore(); + readRepository = new UserReadRepository(); + projector = new UserProjector(readRepository); + userAggregate = new UserAggregate(writeRepository); + userProjection = new UserProjection(readRepository); + } + + @Test + public void givenCQRSApplication_whenCommandRun_thenQueryShouldReturnResult() throws Exception { + String userId = UUID.randomUUID() + .toString(); + List events = null; + CreateUserCommand createUserCommand = new CreateUserCommand(userId, "Kumar", "Chandrakant"); + events = userAggregate.handleCreateUserCommand(createUserCommand); + + projector.project(userId, events); + + UpdateUserCommand updateUserCommand = new UpdateUserCommand(userId, Stream.of(new Address("New York", "NY", "10001"), new Address("Los Angeles", "CA", "90001")) + .collect(Collectors.toSet()), + Stream.of(new Contact("EMAIL", "tom.sawyer@gmail.com"), new Contact("EMAIL", "tom.sawyer@rediff.com")) + .collect(Collectors.toSet())); + events = userAggregate.handleUpdateUserCommand(updateUserCommand); + projector.project(userId, events); + + updateUserCommand = new UpdateUserCommand(userId, Stream.of(new Address("New York", "NY", "10001"), new Address("Housten", "TX", "77001")) + .collect(Collectors.toSet()), + Stream.of(new Contact("EMAIL", "tom.sawyer@gmail.com"), new Contact("PHONE", "700-000-0001")) + .collect(Collectors.toSet())); + events = userAggregate.handleUpdateUserCommand(updateUserCommand); + projector.project(userId, events); + + ContactByTypeQuery contactByTypeQuery = new ContactByTypeQuery(userId, "EMAIL"); + assertEquals(Stream.of(new Contact("EMAIL", "tom.sawyer@gmail.com")) + .collect(Collectors.toSet()), userProjection.handle(contactByTypeQuery)); + AddressByRegionQuery addressByRegionQuery = new AddressByRegionQuery(userId, "NY"); + assertEquals(Stream.of(new Address("New York", "NY", "10001")) + .collect(Collectors.toSet()), userProjection.handle(addressByRegionQuery)); + + } + +} diff --git a/patterns/pom.xml b/patterns/pom.xml index 4c17055231..e1753aba56 100644 --- a/patterns/pom.xml +++ b/patterns/pom.xml @@ -5,13 +5,11 @@ patterns patterns pom - com.baeldung parent-modules 1.0.0-SNAPSHOT - design-patterns-architectural design-patterns-behavioral @@ -21,11 +19,11 @@ design-patterns-functional design-patterns-structural dip + cqrs-es front-controller intercepting-filter solid - @@ -36,7 +34,6 @@ - @@ -53,9 +50,7 @@ - 9.4.0.v20161208 - - + \ No newline at end of file