Сначала я прошу прощения за очень длинный пост. Существует достаточно кода, чтобы показать детальное понимание проблемы и, следовательно, потерять материал для публикации ... Пожалуйста, будьте любезны прочитать все это: -)
Я пытаюсь разработать событие приложение на основе источников, использующее инфраструктуру Axon вместе с приложением Spring Boot. Ниже приведен ряд определений классов, составляющих реализации агрегатов, команд и событий.
Я написал очень простое тестовое приложение (Spring), которое не более чем отправляет CreateAppointmentCommand в Axon. Эта команда создания использует назначенный вручную AppointmentId (который является подклассом AbstractId) и возвращает этот идентификатор для дальнейшего использования. В этом нет ничего плохого, обработчик команды конструктора в Appointment вызывается как положено, и соответствующий ApppointmentCreatedEvent запускается, а также обрабатывается, как ожидается классом Appoitment. Все идет нормально. Проблема возникает, когда я отправляю ConfirmAppointmentCommand с идентификатором, возвращаемым командой create. При таких обстоятельствах я получаю сообщение об ошибке:
Команда 'ConfirmAppointmentCommand' привела к появлению org.axonframework.commandhandling.CommandExecutionException (Предоставлен идентификатор неправильного типа для класса Appointment. Ожидаемый: класс AppointmentId, полученный класс java .lang.String)
Я не понимаю несколько вещей в этой настройке, связанных с этим сообщением об ошибке:
- Почему работают команда create и событие как и ожидалось, когда они используют один и тот же подход (по крайней мере, насколько я понимаю) по сравнению с командой / событием подтверждения?
- Почему Аксон жалуется на AppointmentId в качестве идентификатора (как правило, агрегатного), в то время как соответствующий код (см. ниже) аннотирует оба типа String для @AggregateIdentier и @TargetAggregateIdentier?
- Разрешено ли хранить агрегат напрямую в постоянном хранилище, используя один и тот же код как для агрегата, так и для сущности (в в этом случае хранилище JPA, управляемое Sprin g и связан с реляционной базой данных) при использовании Axon (я не думаю, что мне следует использовать подход с накоплением состояний, описанный в справочном руководстве, потому что я все еще хочу, чтобы мое решение было ориентировано на события для создания и обновления встреч )?
- Это правильный подход для поддержания состояния агрегата в актуальном состоянии с использованием механизма событий, и можно ли использовать другой класс Spring @Component, реализующий серию методов @EventHandler для выполнения операции CRUD в отношении реляционной базы данных. В последнем случае событие create обрабатывается, как и ожидалось, и встреча сохраняется в базе данных. События подтверждения не запускаются из-за предыдущего сообщения об ошибке.
- Ссылаясь на пункт 4, я немного запутался в том, что произойдет, если Аксон перезапустится и начнет передавать различные события в обработчик событий в 4. Не приведет ли это к большому количеству ошибок базы данных, потому что встреча все еще сохраняется в базе данных, или в худшем случае бесконечные дубликаты тех же самых встреч? Другими словами, кажется, что-то не так с подходом, который я использую в этом проекте, и в моем понимании управляемых событиями приложений / сервисов.
Пожалуйста, обратитесь к различным определениям классов ниже для получения более подробной информации. , Сначала у меня есть root агрегатное Назначение, которое будет одновременно использоваться как сущность JPA.
@Aggregate
@Entity
@Table(name = "t_appointment")
public final class Appointment extends AbstractEntity<AppointmentId> {
//JPA annotated class members left out for brevity
@PersistenceConstructor
private Appointment() {
super(null);
//Sets all remaining class members to null.
}
@CommandHandler
private Appointment(CreateAppointmentCommand command) {
super(command.getAggregateId());
validateFields(getEntityId(), ...);
AggregateLifecycle.apply(new AppointmentCreatedEvent(getEntityId(), ...);
}
@EventSourcingHandler
private void on(AppointmentCreatedEvent event) {
validateFields(event.getAggregateId(), ...);
initFields(event.getAggregateId(), ...);
}
private void validateFields(AppointmentId appointmentId, ...) {
//Check if all arguments are within the required boundaries.
}
private void initFields(AppointmentId appointmentId, ...) {
//Set all class level variables to passed in value.
}
@CommandHandler
private void handle(ConfirmAppointmentCommand command) {
AggregateLifecycle.apply(new AppointmentConfirmedEvent(command.getAggregateId()));
}
@EventSourcingHandler
private void on(AppointmentConfirmedEvent event) {
confirm();
}
public void confirm() {
changeState(State.CONFIRMED);
}
//Similar state changing command/event handlers left out for brevity.
private void changeState(State newState) {
switch (state) {
...
}
}
//All getter methods left out for brevity. The aggregate does NOT provide any setters.
@Override
public String toString() {
return "Appointment [...]";
}
}
Класс AbstractEntity является базовым классом для всех сущностей и агрегатов JPA. Этот класс имеет следующее определение.
@MappedSuperclass
@SuppressWarnings("serial")
public abstract class AbstractEntity<ENTITY_ID extends AbstractId> implements Serializable{
@EmbeddedId
private ENTITY_ID entityId;
@AggregateIdentifier
private String targetId;
protected AbstractEntity(ENTITY_ID id) {
this.LOG = LogManager.getLogger(getClass());
this.entityId = id;
this.targetId = id != null ? id.getId() : null;
}
public final ENTITY_ID getEntityId() {
return entityId;
}
@Override
public final int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((entityId == null) ? 0 : entityId.hashCode());
return result;
}
@Override
public final boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
AbstractEntity<?> other = (AbstractEntity<?>) obj;
if (entityId == null) {
if (other.entityId != null)
return false;
} else if (!entityId.equals(other.entityId))
return false;
return true;
}
}
EntityId (который будет использоваться в качестве первичного ключа для сущностей JPA) - это объект «сложного» значения, имеющий следующее определение базового класса.
@MappedSuperclass
@SuppressWarnings("serial")
public abstract class AbstractId implements Serializable{
@Column(name = "id")
private String id;
protected AbstractId() {
this.id = UUID.randomUUID().toString();
}
public final String getId() {
return id;
}
@Override
public final int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((id == null) ? 0 : id.hashCode());
return result;
}
@Override
public final boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
AbstractId other = (AbstractId) obj;
if (id == null) {
if (other.id != null)
return false;
} else if (!id.equals(other.id))
return false;
return true;
}
public final String toString() {
return id;
}
}
Внутри агрегата используется ряд команд и событий. Каждая команда является подклассом Command.
@SuppressWarnings("serial")
public abstract class Command<AGGREGATE_ID extends AbstractId> implements Serializable{
private AGGREGATE_ID aggregateId;
@TargetAggregateIdentifier
private String targetId;
protected Command(AGGREGATE_ID aggregateId) {
if(aggregateId == null) {
throw new InvalidArgumentException(...);
}
this.aggregateId = aggregateId;
this.targetId = aggregateId != null ? aggregateId.getId() : null;
}
public final AGGREGATE_ID getAggregateId() {
return aggregateId;
}
}
Указанным классом команд (который вызывает трудности в моем подходе) является ConfirmAppointmentCommand, который на самом деле является не более чем конкретной реализацией базового класса Command. Поэтому реализация очень прямолинейна.
public final class ConfirmAppointmentCommand extends Command<AppointmentId> {
private static final long serialVersionUID = 6618106729289153342L;
public ConfirmAppointmentCommand(AppointmentId appointmentId) {
super(appointmentId);
}
}
CreateAppointmentCommand очень похож на ConfirmAppointmentCommand и определяется следующим образом.
public final class CreateAppointmentCommand extends Command<AppointmentId> {
private static final long serialVersionUID = -5445719522854349344L;
//Some additional class members left out for brevity.
public CreateAppointmentCommand(AppointmentId appointmentId, ...) {
super(appointmentId);
//Check to verify the provided method arguments are left out.
//Set all verified class members to the corresponding values.
}
//Getters for all class members, no setters are being implemented.
}
Для различных событий, используемых в проекте, a аналогичный подход используется. Все события являются подклассами базового класса DomainEvent, как определено ниже.
@SuppressWarnings("serial")
public abstract class DomainEvent<T extends AbstractId> implements Serializable{
private T aggregateId;
protected DomainEvent(T aggregateId) {
if(aggregateId == null) {
throw new InvalidArgumentException(ErrorCodes.AGGREGATE_ID_MISSING);
}
this.aggregateId = aggregateId;
}
public final T getAggregateId() {
return aggregateId;
}
}
AppointmentCreatedEvent довольно прост.
public final class AppointmentCreatedEvent extends DomainEvent<AppointmentId> {
private static final long serialVersionUID = -5265970306200850734L;
//Class members left out for brevity
public AppointmentCreatedEvent(AppointmentId appointmentId, ...) {
super(appointmentId);
//Check to verify the provided method arguments are left out.
//Set all verified class members to the corresponding values.
}
//Getters for all class members, no setters are being implemented.
}
И, наконец, для полноты, AppointmentConfirmedEvent.
public final class AppointmentConfirmedEvent extends DomainEvent<AppointmentId> {
private static final long serialVersionUID = 5415394808454635999L;
public AppointmentConfirmedEvent(AppointmentId appointmentId) {
super(appointmentId);
}
}
Мало, вы сделали это до конца поста. Спасибо за это в первую очередь, пожалуйста! Не могли бы вы посоветовать мне, где дела идут плохо или что я делаю неправильно?
С уважением, Курт