Объект значения как @AggregateIdentifier и @TargetAggregateIdentifier - PullRequest
0 голосов
/ 29 апреля 2020

Сначала я прошу прощения за очень длинный пост. Существует достаточно кода, чтобы показать детальное понимание проблемы и, следовательно, потерять материал для публикации ... Пожалуйста, будьте любезны прочитать все это: -)

Я пытаюсь разработать событие приложение на основе источников, использующее инфраструктуру Axon вместе с приложением Spring Boot. Ниже приведен ряд определений классов, составляющих реализации агрегатов, команд и событий.

Я написал очень простое тестовое приложение (Spring), которое не более чем отправляет CreateAppointmentCommand в Axon. Эта команда создания использует назначенный вручную AppointmentId (который является подклассом AbstractId) и возвращает этот идентификатор для дальнейшего использования. В этом нет ничего плохого, обработчик команды конструктора в Appointment вызывается как положено, и соответствующий ApppointmentCreatedEvent запускается, а также обрабатывается, как ожидается классом Appoitment. Все идет нормально. Проблема возникает, когда я отправляю ConfirmAppointmentCommand с идентификатором, возвращаемым командой create. При таких обстоятельствах я получаю сообщение об ошибке:

Команда 'ConfirmAppointmentCommand' привела к появлению org.axonframework.commandhandling.CommandExecutionException (Предоставлен идентификатор неправильного типа для класса Appointment. Ожидаемый: класс AppointmentId, полученный класс java .lang.String)

Я не понимаю несколько вещей в этой настройке, связанных с этим сообщением об ошибке:

  1. Почему работают команда create и событие как и ожидалось, когда они используют один и тот же подход (по крайней мере, насколько я понимаю) по сравнению с командой / событием подтверждения?
  2. Почему Аксон жалуется на AppointmentId в качестве идентификатора (как правило, агрегатного), в то время как соответствующий код (см. ниже) аннотирует оба типа String для @AggregateIdentier и @TargetAggregateIdentier?
  3. Разрешено ли хранить агрегат напрямую в постоянном хранилище, используя один и тот же код как для агрегата, так и для сущности (в в этом случае хранилище JPA, управляемое Sprin g и связан с реляционной базой данных) при использовании Axon (я не думаю, что мне следует использовать подход с накоплением состояний, описанный в справочном руководстве, потому что я все еще хочу, чтобы мое решение было ориентировано на события для создания и обновления встреч )?
  4. Это правильный подход для поддержания состояния агрегата в актуальном состоянии с использованием механизма событий, и можно ли использовать другой класс Spring @Component, реализующий серию методов @EventHandler для выполнения операции CRUD в отношении реляционной базы данных. В последнем случае событие create обрабатывается, как и ожидалось, и встреча сохраняется в базе данных. События подтверждения не запускаются из-за предыдущего сообщения об ошибке.
  5. Ссылаясь на пункт 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);       
    }
}

Мало, вы сделали это до конца поста. Спасибо за это в первую очередь, пожалуйста! Не могли бы вы посоветовать мне, где дела идут плохо или что я делаю неправильно?

С уважением, Курт

1 Ответ

1 голос
/ 30 апреля 2020

Вопрос 3 Из вашего третьего вопроса я замечаю, что вы не хотите использовать агрегированный подход с сохранением состояния в Axon, а вместо этого используют Event Sourcing. С другой стороны, вы тоже храните Агрегат как объект состояния, делая его сущностью.

Каково ваше намерение в этом? Если вы хотите использовать Appointment для возврата заинтересованным сторонам, вы должны знать, что вы не следуете CQRS по этому вопросу.

Аннотированный класс @Aggregate в Axon обычно указывает на Командную модель , Таким образом, он используется исключительно для приема команд, определения возможности выполнения выражения намерения этой команды и публикации событий sh в результате этого.

Добавлено, вы заявляете, что помещаете это в приложение Spring Boot. Оттуда я предполагаю, что вы также используете зависимость axon-spring-boot-starter. При использовании автоконфигурации Axon Spring @Aggregate работает как «Spring Stereotype». Кроме того, если аннотированный объект @Aggregate имеет значение , а также с аннотацией @Entity, то при автоматической конфигурации предполагается, что вы хотите сохранить агрегат как есть. Таким образом, по умолчанию будет иметь сохраняемый государством агрегат; что-то, что вы заявляете, не то, что вы хотите.

Вопросы 1 и 2 Команда create, вероятно, работает, так как это точка инициации Агрегата. Следовательно, он еще не извлекает существующий формат на основе идентификатора.

Во-вторых, полученное вами исключение, хотя оно заключено в CommandExecutionException, первоначально исходило из вашей базы данных. Быстрый поиск текста Provided id of the wrong type for class в базе кода Аксона ни к чему не приводит. Обратите внимание, что Аксон всегда будет считать, что идентификаторы можно преобразовать в String. Следовательно, выделенный метод toString() может быть полезным, чтобы не добавлять нежелательную информацию в String.

Это та часть, о которой Аллард спрашивает больше информации, поскольку это, вероятно, связано с тем фактом, что Агрегат по сути сейчас хранится в состоянии. Таким образом, исключение всплывает из реализации JPA, используемой GenericJpaRepository (именно этот репозиторий, который Axon автоматически конфигурирует для вас с учетом текущей настройки) для данного Агрегата.

Вопросы 4 и 5 Совершенно нормально обновлять Aggregate с помощью @EventSourcingHandler аннотированных методов и иметь отдельный компонент Spring в вашем приложении, который обрабатывает события для обновления проекций. Я бы расценил это как «путь к go» при выполнении CQRS через Аксона.

Последнее беспокойство, которое у вас возникло, требует от меня сделать предположение. Я предполагаю, что вы не настроили ничего, определяющего c вокруг используемого обработчика событий . Это означает, что Axon автоматически настроит для вас TrackingEventProcessor. Одна из вещей, которую делает эта реализация, - это сохранение прогресса "насколько далеко он находится с обработкой событий в потоке событий" в токене. Эти токены, в свою очередь, должны храниться вместе с вашими проекциями, так как они определяют, насколько актуальными являются ваши прогнозы, когда дело доходит до всего потока событий.

Если вы заметили, что обработчики событий изнутри обработки событий Компонент, который будет вызываться при каждом запуске, для меня это сигнал о том, что таблица token_entry либо отсутствует, либо очищается при каждом запуске.

Завершение Здесь довольно много глотка Надеюсь, это поможет тебе, Курт! Если что-то неясно, пожалуйста, прокомментируйте мой ответ; Я обновлю свой ответ соответственно.

...