Hello All,
I have run into a snag handling commands in nested AggregateMember instances. I am modeling a warehouse management system for an organization with many disparate campuses, each of which is comprised of one or more facilities and each facility can be sub-divided into multiple areas.
The code for the aggregate root and two entities follows.
Campus:
`
package com.myco.warehousing.command;
import static org.axonframework.commandhandling.model.AggregateLifecycle.apply;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.axonframework.commandhandling.CommandHandler;
import org.axonframework.commandhandling.model.AggregateIdentifier;
import org.axonframework.commandhandling.model.AggregateMember;
import org.axonframework.eventsourcing.EventSourcingHandler;
import org.axonframework.spring.stereotype.Aggregate;
import org.springframework.util.Assert;
import com.myco.warehousing.api.Address;
import com.myco.warehousing.api.CampusCreatedEvent;
import com.myco.warehousing.api.CreateCampusCommand;
import com.myco.warehousing.api.DescribeFacilityCommand;
import com.myco.warehousing.api.FacilityDescribedEvent;
@Aggregate(repository = “campusRepository”)
public class Campus implements Serializable {
private static final long serialVersionUID = -64868971771565457L;
@AggregateIdentifier(routingKey = “campusId”)
private String id;
private String name;
private Address address;
@AggregateMember
private Map<String, Facility> facilitiesById;
private Map<String, Facility> facilitiesByCode;
Campus() {
}
String id() {
return id;
}
String name() {
return name;
}
Address address() {
return address;
}
Map<String, Facility> facilitiesById() {
return facilitiesById == null ? Collections.emptyMap() : facilitiesById;
}
Map<String, Facility> facilitiesByCode() {
return facilitiesByCode == null ? Collections.emptyMap() : facilitiesByCode;
}
@CommandHandler
Campus(CreateCampusCommand command) {
Assert.notNull(command, “null command”);
Assert.hasText(command.getCampusId(), “null/blank campus identifier”);
Assert.hasText(command.getCampusName(), “null/blank campus name”);
Assert.notNull(command.getCampusAddress(), “null campus address”);
apply(new CampusCreatedEvent(command.getCampusId(), command.getCampusName(), command.getCampusAddress()));
}
@CommandHandler
void handle(DescribeFacilityCommand command) {
Assert.notNull(command, “null command”);
Assert.hasText(command.getCampusId(), “null/blank campus identifier”);
Assert.hasText(command.getFacilityId(), “null/blank facility identifier”);
Assert.hasText(command.getFacilityName(), “null/blank facility name”);
Assert.hasText(command.getFacilityCode(), “null/blank facility code”);
Assert.notNull(command.getFacilityAddress(), “null facility address”);
Assert.state(!facilitiesById().containsKey(command.getFacilityId()),
String.format("Facility with matching id already defined: %s -> %s ", command.getFacilityId(),
facilitiesById().get(command.getFacilityId())));
String facilityCode = command.getFacilityCode().trim().toUpperCase();
Assert.state(!facilitiesByCode().containsKey(facilityCode), String.format(
"Facility with matching code already defined: %s -> %s ", facilityCode, facilitiesByCode().get(facilityCode)));
apply(new FacilityDescribedEvent(command.getCampusId(), command.getFacilityId(), command.getFacilityName(),
facilityCode, command.getFacilityAddress()));
}
@EventSourcingHandler
void on(CampusCreatedEvent event) {
this.id = event.getCampusId();
this.name = event.getCampusName();
this.address = event.getCampusAddress();
}
@EventSourcingHandler
void on(FacilityDescribedEvent event) {
if (facilitiesById == null) {
facilitiesById = new HashMap<>();
facilitiesByCode = new HashMap<>();
}
Facility facility = new Facility(event.getFacilityId(), event.getFacilityName(), event.getFacilityCode(),
event.getFacilityAddress());
facilitiesById.put(event.getFacilityId(), facility);
facilitiesByCode.put(event.getFacilityCode(), facility);
}
}
`
Facility:
`
package com.myco.warehousing.command;
import static org.axonframework.commandhandling.model.AggregateLifecycle.apply;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import org.axonframework.commandhandling.CommandHandler;
import org.axonframework.commandhandling.model.AggregateMember;
import org.axonframework.commandhandling.model.EntityId;
import org.axonframework.eventsourcing.EventSourcingHandler;
import org.springframework.util.Assert;
import com.myco.warehousing.api.Address;
import com.myco.warehousing.api.AreaDescribedEvent;
import com.myco.warehousing.api.DescribeAreaCommand;
public class Facility {
public static enum Type {
BUILDING(“Building”), LOT(“Lot”), HANGAR(“Hangar”);
private String label;
private Type(String label) {
Assert.hasText(label, “null/blank label”);
this.label = label;
}
public String getLabel() {
return label;
}
}
@EntityId(routingKey = “facilityId”)
private String id;
private String name;
private String code;
private Address address;
@AggregateMember()
private Map<String, Area> areasById;
private Map<String, Area> areasByCode;
Facility(String id, String name, String code, Address address) {
this.id = id;
this.name = name;
this.code = code;
this.address = address;
}
String id() {
return id;
}
String name() {
return name;
}
String code() {
return code;
}
Address address() {
return address;
}
Map<String, Area> areasById() {
return areasById == null ? Collections.emptyMap() : areasById;
}
Map<String, Area> areasByCode() {
return areasByCode == null ? Collections.emptyMap() : areasByCode;
}
@CommandHandler
void handle(DescribeAreaCommand command) {
Assert.notNull(command, “null command”);
Assert.hasText(command.getCampusId(), “null/blank campus identifier”);
Assert.hasText(command.getFacilityId(), “null/blank facility identifier”);
Assert.hasText(command.getAreaId(), “null/blank area identifier”);
Assert.hasText(command.getAreaName(), “null/blank area name”);
Assert.hasText(command.getAreaCode(), “null/blank area code”);
Area area = areasById().get(command.getAreaId());
Assert.state(area == null,
String.format(“Area with matching id already defined: %s -> %s”, command.getAreaId(), area));
String areaCode = command.getAreaCode().trim().toUpperCase();
area = areasByCode().get(areaCode);
Assert.state(area == null, String.format(“Area with matching code already defined: %s -> %s”, areaCode, area));
apply(new AreaDescribedEvent(command.getCampusId(), command.getFacilityId(), command.getAreaId(),
command.getAreaName(), areaCode));
}
@EventSourcingHandler
void on(AreaDescribedEvent event) {
if (areasById == null) {
areasById = new HashMap<>();
areasByCode = new HashMap<>();
}
Area area = new Area(event.getAreaId(), event.getAreaName(), event.getAreaCode());
areasById.put(event.getAreaId(), area);
areasByCode.put(event.getAreaCode(), area);
}
@Override
public String toString() {
return “Facility [id=” + id() + “, name=” + name() + “, code=” + code() + “, address=” + address() + “, areas=”
- areasById().values().stream().map(a -> a.toString()).collect(Collectors.joining(", ", “[”, “]”)) + “]”;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((id == null) ? 0 : id.hashCode());
return result;
}
/**
- NOTE: this method is ONLY supports comparisons relative to the parent aggregate root (the owning {@link Campus}).
- The behavior of comparing two {@link Facility} instances from disparate {@link Campus} instances is undefined.