Maneje la Evolución del Esquema en IPF Events

A lo largo de la vida de un IPF flow, se realizarán cambios en el events que son persistidos por ese flujo. Los cambios en los flujos puede suceder por muchas razones, tales como:

  • Mejor comprensión empresarial del flujo que conduce a un cambio en los requisitos.

  • Las suposiciones en tiempo de diseño no son correctas y, por lo tanto, requieren un cambio en lo inicialmente acordado.events

  • Razones regulatorias para necesitar capturar más datos en un event

La naturaleza técnica de estos cambios puede variar, pero generalmente son variantes de los patrones a continuación, que también servirán como los subtítulos de esta guía.

Los patrones para la evolución del esquema son:

  • Añadiendo un campo de datos a un event

  • Cambio del nombre de clase completamente calificado (FQCN) de un event

  • Cambio del nombre de un elemento de datos en un event

  • Cambiando el data type de un elemento en un event

  • Dividir un event en múltiples, más pequeñas events

  • Eliminando un event

El proceso para actualizar events está fuera del alcance de este documento. Siga la parte de DSL del tutorial para event definición y modificación.

Estos event las migraciones deben implementarse únicamente si el flujo en sí no está cambiando.

Si el flujo en sí también está cambiando además de su events, entonces considere crear una nueva versión del flujo en conjunto. Consulte la parte de DSL del tutorial para saber cómo definir múltiples versiones de flujos.

Añadiendo un Campo de Datos a un Event

Considere la v1 de un event siendo lo siguiente:

public class UserRegistered extends DomainEvent {
    private final String name;
}

Esto se serializaría en JSON as:

{"name": "jeff"}

Si los nuevos datos que se están añadiendo son simplemente un nuevo campo opcional, como este:

package com.myorg.events;

import java.time. LocalDate;

public class UserRegistered extends DomainEvent {
    private final String name;
    private final LocalDate dob;
}

La antigua versión serializada seguirá siendo analizada con éxito como un UserRegistered, y no se requiere ningún cambio. Pero, por supuesto, el dob el campo será null.

Sin embargo, las reglas de negocio pueden no permitir el dob el campo debe ser nulo, pero puede haber una fecha de nacimiento especial como marcador de posición 0001-01-01`que puede utilizar para indicar la falta de disponibilidad de una fecha de nacimiento para este usuario. En cuyo caso, esto `JacksonMigration verificará si dob, y-si está ausente-lo establecerá en el valor predeterminado antes de que sea deserializado como UserRegistered:

package com.myorg.migrations;

import com.fasterxml.jackson.databind. JsonNode;
import com.fasterxml.jackson.databind.node. ObjectNode;

public class DobFillerMigration extends JacksonMigration {

    private static final String DOB_FIELD_NAME = "dob";

    @Override
    public int currentVersion() {
        return 2;
    }

    @Override
    public JsonNode transform(int fromVersion, JsonNode json) {
        if (! json.has(DOB_FIELD_NAME)) {
            ((ObjectNode) json).set(DOB_FIELD_NAME, "0001-01-01");
        }
    }
}

Y luego vinculado al UsuarioRegistrado event agregándolo a la configuración de IPF de la siguiente manera:

akka.serialization.jackson.migrations {
  "com.myorg.events. UserRegistered" = "com.myorg.migrations. DobFillerMigration"
}

Recuerde, si el nuevo campo se permite que sea nulo, no es necesario escribir ninguna migración.

Cambio del nombre de clase completamente calificado (FQCN) de un event, o renombrar un event

Esto no puede ser remediado arreglando un JacksonMigration, ya que JacksonMigration trabaja en el cuerpo y no en el tipo.

Sin embargo, si no ha cambiado nada más aparte del nombre de clase completamente calificado (o simplemente el nombre) de la event, y el Icono MongoDB Akka Persistence el plugin está siendo utilizado, esto puede ser remediado utilizando un MongoDB declaración de actualización.

Como ejemplo, si el nombre del paquete de la events fue mal escrito como com.myorg.evnets y queremos corregir el error tipográfico, el la siguiente instrucción de actualización cambiará el nombre del paquete de todos com.myorg.evnets events to com.myorg.events:

const updates = [];
db.journal.find({"eventPayloads.payload.type":/com.iconsolutions.instantpayments/})
    .forEach(doc => {
        doc.eventPayloads.forEach(pld => {
            const oldFQCN = pld.payload.type;
            const newFQCN = oldFQCN.replace("com.iconsolutions", "com.monkey");
            updates.push({"updateOne": {
                "filter": {$and: [{"_id": doc._id}, {"eventPayloads.payload.type":oldFQCN}]},
                "update": {$set: {"eventPayloads.$.payload.type": newFQCN}}
            }})
        })
    });
print(updates);
// uncomment after this line to actually run the update
// const result = db.journal.bulkWrite(updates);
// print(JSON.stringify(result));

Algo a tener en cuenta sobre el fragmento anterior:

  • El nombre predeterminado del diario es journal pero esto puede ser anulado con iconsolutions.akka.persistence.mongodb.journal-collection

Cambio del Nombre de un Elemento de Datos en un Event

Esto también puede resolverse escribiendo un JacksonMigration.

Considere la v1 de la event siendo:

import java.time. LocalDate;

public class UserRegistered extends DomainEvent {
    private final String name;
    private final LocalDate dob;
}

Esto se serializaría en JSON as:

{"name": "jeff", "dob": "1985-01-01"}

Pero usted decide que dob puede que no sea tan claro y decidir renombrarlo a dateOfBirth:

package com.myorg.events;

import java.time. LocalDate;

public class UserRegistered extends DomainEvent {
    private final String name;
    private final LocalDate dateOfBirth;
}

Escriba la siguiente migración para renombrar dob to dateOfBirth:

package com.myorg.migrations;

import com.fasterxml.jackson.databind. JsonNode;
import com.fasterxml.jackson.databind.node. ObjectNode;

public class DobRenameMigration extends JacksonMigration {

    private static final String OLD_FIELD_NAME = "dob";
    private static final String NEW_FIELD_NAME = "dateOfBirth";

    @Override
    public int currentVersion() {
        return 2;
    }

    @Override
    public JsonNode transform(int fromVersion, JsonNode json) {
        if (json.has(OLD_FIELD_NAME)) {
            //get value of dob
            var seqValue = json.get(OLD_FIELD_NAME);
            //set it to new field
            ((ObjectNode) json).set(NEW_FIELD_NAME, seqValue);
            //remove old field
            ((ObjectNode) json).remove(OLD_FIELD_NAME);
        }
        return json;
    }
}

Y luego vinculado al UsuarioRegistrado event agregándolo a la configuración de IPF de la siguiente manera:

akka.serialization.jackson.migrations {
  "com.myorg.events. UserRegistered" = "com.myorg.migrations. DobRenameMigration"
}

Cambiando el Data Type de un Elemento en un Event

Cambiar un tipo puede significar múltiples cosas, tales como:

  • Un elemento de datos que se divide en múltiples elementos

  • Pasando de un tipo simple a un tipo complejo

Pero ambos se manejan de manera más o menos similar, que consiste en escribir un JacksonMigration para mapear los valores del antiguo versión de la event a su nueva representación.

Continuando con el ejemplo anterior v1 event:

package com.myorg.events;

import java.time. LocalDate;

public class UserRegistered extends DomainEvent {
    private final String name;
    private final LocalDate dateOfBirth;
}

Imagine que la empresa desea devolver los detalles del nombre en un documento separado.Name objeto:

package com.myorg.model;

import java.time. LocalDate;

public class Name {
    private final String firstName;
    private final String middleName;
    private final String lastName;
}

Así que el nuevo event se ve así:

package com.myorg.events;

import com.myorg.model. Name;
import java.time. LocalDate;

public class UserRegistered extends DomainEvent {
    private final Name name;
    private final LocalDate dateOfBirth;
}

Suponiendo la siguiente regla de migración imaginaria del negocio:

  • Si un nombre contiene dos tokens, divídalo en nombre y apellido en ese orden.

  • Si un nombre contiene tres tokens, divídalo en nombre, segundo nombre y apellido en ese orden.

    ¡Esta no es una buena manera de descomponer el nombre de alguien en partes constitutivas!

Bajo estas reglas, la migración sería:

package com.myorg.migrations;

import com.fasterxml.jackson.databind. JsonNode;
import com.fasterxml.jackson.databind.node. JsonNodeFactory;import com.fasterxml.jackson.databind.node. ObjectNode;

public class NameMigration extends JacksonMigration {

    @Override
    public int currentVersion() {
        return 2;
    }

    @Override
    public JsonNode transform(int fromVersion, JsonNode json) {
        var name = json.get("name").asText();
        var nameNode = JsonNodeFactory.instance.objectNode();
        var nameSplit = name.split("\s");
        if (nameSplit.length == 2) {
            nameNode.set("firstName", nameSplit[0]);
            nameNode.set("lastName", nameSplit[1]);
        } else if (nameSplit.length == 3) {
            nameNode.set("firstName", nameSplit[0]);
            nameNode.set("middleName", nameSplit[1]);
            nameNode.set("lastName", nameSplit[2]);
        }
        ((ObjectNode) json).set("name", nameNode);
        return json;
    }
}

Y en la configuración de IPF:

akka.serialization.jackson.migrations {
  "com.myorg.events. UserRegistered" = "com.myorg.migrations. NameMigration"
}

Dividir un Event En múltiples, más pequeñas Events

Esto puede ser implementado utilizando un EventAdapter. El enfoque es similar a la redacción de una JacksonMigration, pero es implementado después del events han sido deserializados. Consulte el Akka documentos para más información sobre este tema:https://doc.akka.io/docs/akka/current/persistence-schema-evolution.html#split-large-event-into-fine-grained-events[Divida grande event en detalles finos events].

Para implementar un EventAdapter que se pasará a la EventSourcedBehaviour para el flujo, al iniciar el dominio, utilizar withBehaviourExtensions para suministrar un BehaviourExtensions implementación como esta:

import java.util. Optional;

public class MyBehaviourExtensions implements BehaviourExtensions {
    @Override
    public Optional<EventAdapter<Event, Event>> eventAdapter() {
        return Optional.of(new EventAdapter<>() {
            @Override
            public Event toJournal(Event event) {
                return event;
            }

            @Override
            public String manifest(Event event) {
                return "";
            }

            @Override
            public EventSeq<Event> fromJournal(Event event, String manifest) {
                //if it's MySuperEvent, devolve it into two smaller events
                if (event instanceof MySuperEvent) {
                    var mse = (MySuperEvent) event;
                    return EventSeq.create(List.of(
                            new MySmallerEvent1(mse.data1()),
                            new MySmallerEvent2(mse.data2())
                    ));
                }
                //otherwise return any other event as-is
                return EventSeq.single(event);
            }
        });
    }
}

y luego..

@EventListener
public void init(ContextRefreshedEvent event, MyBehaviourExtensions myBehaviourExtensions) {
    new MyDomain. Builder(actorSystem)
            .withEventBus(eventBus)
            .withSchedulerAdapter(schedulerAdapter)
            .withSystemAActionAdapter(new SampleSanctionsActionAdapter(sanctionsAdapter))
            .withSystemBActionAdapter(new SampleSanctionsActionAdapter(sanctionsAdapter))
            .withBehaviourExtensions(myBehaviourExtensions) (1)
            .build();
}
1 Cómo añadir la implementación de BehaviourExtensions

Eliminando un Event

Esto es lo mismo que arriba, pero en su lugar emita un EventSeq.empty(the rest de la implementación es la misma):

@Override
public EventSeq<Event> fromJournal(Event event, String manifest) {
    //if it's MyUnwantedEvent, pretend we didn't see it
    if (event instanceof MyUnwantedEvent) {
        return EventSeq.empty();
    }
    //otherwise return any other event as-is
    return EventSeq.single(event);
}