Documentation for a newer release is available. View Latest

Manejo de la evolución de esquemas en eventos de IPF

A lo largo de la vida de un flujo de IPF, se realizarán cambios en los eventos que persiste ese flujo. Los cambios en los flujos pueden ocurrir por múltiples razones, como:

  • Mejor entendimiento del negocio del flujo que conduce a un cambio en requisitos

  • Suposiciones en diseño que no resultaron correctas y requieren cambiar los eventos acordados inicialmente

  • Razones regulatorias para capturar más datos en un evento

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

Los patrones para la evolución de esquemas son:

  • Añadir un campo de datos a un evento

  • Cambiar el nombre de la clase totalmente cualificada (FQCN) de un evento

  • Cambiar el nombre de un elemento de datos en un evento

  • Cambiar el tipo de datos de un elemento en un evento

  • Dividir un evento en varios eventos más pequeños

  • Eliminar un evento

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

Estas migraciones de eventos solo deben implementarse si el flujo en sí no está cambiando.

Si el flujo también está cambiando además de sus eventos, entonces considera crear una nueva versión del flujo por completo. Consulta la parte de DSL del tutorial para cómo definir múltiples versiones de flujos.

Añadir un campo de datos a un evento

Considera que la v1 de un evento es la siguiente:

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

Esto se serializaría en JSON como:

{"name": "jeff"}

Si los nuevos datos añadidos son simplemente un nuevo campo opcional, como esto:

package com.myorg.events;

import java.time.LocalDate;

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

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

Sin embargo, las reglas de negocio pueden no permitir que el campo dob sea null, pero podría haber una fecha de nacimiento especial de marcador 0001-01-01 que puedas usar para indicar la falta de disponibilidad de la fecha de nacimiento para este usuario. En ese caso, esta JacksonMigration verificará dob y, si falta, lo establecerá al valor por defecto antes de deserializarse 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 se vincula al evento UserRegistered añadiéndolo a la configuración de IPF así:

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

Recuerda: si el nuevo campo puede ser null, no es necesario escribir ninguna migración.

Cambiar el nombre de la clase totalmente cualificada (FQCN) de un evento, o renombrar un evento

Esto no puede solucionarse con una JacksonMigration, ya que JacksonMigration funciona sobre el cuerpo y no sobre el tipo.

Sin embargo, si nada más ha cambiado aparte del nombre de clase totalmente cualificado (o solo el nombre) del evento, y se está usando el plugin Icon MongoDB Akka Persistence, esto puede remediarse usando una sentencia de actualización de MongoDB.

Como ejemplo, si el nombre del paquete de los eventos estaba mal escrito como com.myorg.evnets y queremos corregir el error tipográfico, la siguiente sentencia de actualización cambiará el nombre de paquete de todos los eventos com.myorg.evnets a 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);
// descomenta después de esta línea para ejecutar realmente la actualización
// const result = db.journal.bulkWrite(updates);
// print(JSON.stringify(result));

Algo a tener en cuenta del fragmento anterior:

  • El nombre por defecto del journal es journal pero puede sobrescribirse con iconsolutions.akka.persistence.mongodb.journal-collection

Cambiar el nombre de un elemento de datos en un evento

Esto también puede resolverse escribiendo una JacksonMigration.

Considera que la v1 del evento es:

import java.time.LocalDate;

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

Esto se serializaría en JSON como:

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

Pero decides que dob puede no ser tan claro y decides renombrarlo a dateOfBirth:

package com.myorg.events;

import java.time.LocalDate;

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

Escribe la siguiente migración para renombrar dob a 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 se vincula al evento UserRegistered añadiéndolo a la configuración de IPF así:

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

Cambiar el tipo de datos de un elemento en un evento

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

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

  • Pasar de un tipo simple a un tipo complejo

Pero ambos se manejan más o menos de la misma manera: escribiendo una JacksonMigration para mapear los valores desde la versión antigua del evento a su nueva representación.

Continuando con el ejemplo anterior del evento v1:

package com.myorg.events;

import java.time.LocalDate;

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

Imagina que el negocio quiere descomponer los detalles del nombre en un objeto Name separado:

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 evento 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;
}

Asumiendo la siguiente regla imaginaria de migración desde el negocio:

  • Si un nombre contiene dos tokens, dividir en nombre y apellido en ese orden

  • Si un nombre contiene tres tokens, dividir en nombre, segundo nombre y apellido en ese orden

¡Esta no es una buena forma de descomponer el nombre de alguien en sus partes constituyentes!

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 evento en varios eventos más pequeños

Esto puede implementarse usando un EventAdapter. El enfoque es similar a escribir una JacksonMigration, pero se implementa después de que los eventos hayan sido deserializados. Consulta la documentación de Akka para más información sobre este tema: Split large event into fine-grained events.

Para implementar un EventAdapter que se pasará al EventSourcedBehaviour del flujo, al iniciar el dominio, usa withBehaviourExtensions para proporcionar una implementación de BehaviourExtensions 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) {
                //si es MySuperEvent, descomponerlo en dos eventos más pequeños
                if (event instanceof MySuperEvent) {
                    var mse = (MySuperEvent) event;
                    return EventSeq.create(List.of(
                            new MySmallerEvent1(mse.data1()),
                            new MySmallerEvent2(mse.data2())
                    ));
                }
                //en caso contrario, devolver cualquier otro evento tal cual
                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

Eliminar un evento

Es lo mismo que lo anterior, pero en su lugar emite un EventSeq.empty (el resto de la implementación es el mismo):

@Override
public EventSeq<Event> fromJournal(Event event, String manifest) {
    //si es MyUnwantedEvent, fingir que no lo vimos
    if (event instanceof MyUnwantedEvent) {
        return EventSeq.empty();
    }
    //en caso contrario devolver cualquier otro evento tal cual
    return EventSeq.single(event);
}