556 lines
16 KiB
Java
556 lines
16 KiB
Java
/*
|
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
|
* license agreements; and to You under the Apache License, version 2.0:
|
|
*
|
|
* https://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* This file is part of the Apache Pekko project, derived from Akka.
|
|
*/
|
|
|
|
/*
|
|
* Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
|
|
*/
|
|
|
|
package jdocs.persistence;
|
|
|
|
import docs.persistence.ExampleJsonMarshaller;
|
|
import docs.persistence.proto.FlightAppModels;
|
|
|
|
import java.io.NotSerializableException;
|
|
import java.nio.charset.Charset;
|
|
import java.nio.charset.StandardCharsets;
|
|
|
|
import spray.json.JsObject;
|
|
|
|
import org.apache.pekko.persistence.journal.EventAdapter;
|
|
import org.apache.pekko.persistence.journal.EventSeq;
|
|
import org.apache.pekko.protobufv3.internal.InvalidProtocolBufferException;
|
|
import org.apache.pekko.serialization.SerializerWithStringManifest;
|
|
|
|
public class PersistenceSchemaEvolutionDocTest {
|
|
|
|
public
|
|
// #protobuf-read-optional-model
|
|
static enum SeatType {
|
|
Window("W"),
|
|
Aisle("A"),
|
|
Other("O"),
|
|
Unknown("");
|
|
|
|
private final String code;
|
|
|
|
private SeatType(String code) {
|
|
this.code = code;
|
|
}
|
|
|
|
public static SeatType fromCode(String c) {
|
|
if (Window.code.equals(c)) return Window;
|
|
else if (Aisle.code.equals(c)) return Aisle;
|
|
else if (Other.code.equals(c)) return Other;
|
|
else return Unknown;
|
|
}
|
|
}
|
|
// #protobuf-read-optional-model
|
|
|
|
public
|
|
// #protobuf-read-optional-model
|
|
static class SeatReserved {
|
|
public final String letter;
|
|
public final int row;
|
|
public final SeatType seatType;
|
|
|
|
public SeatReserved(String letter, int row, SeatType seatType) {
|
|
this.letter = letter;
|
|
this.row = row;
|
|
this.seatType = seatType;
|
|
}
|
|
}
|
|
// #protobuf-read-optional-model
|
|
|
|
public
|
|
// #protobuf-read-optional
|
|
/**
|
|
* Example serializer impl which uses protocol buffers generated classes (proto.*) to perform the
|
|
* to/from binary marshalling.
|
|
*/
|
|
static class AddedFieldsSerializerWithProtobuf extends SerializerWithStringManifest {
|
|
@Override
|
|
public int identifier() {
|
|
return 67876;
|
|
}
|
|
|
|
private final String seatReservedManifest = SeatReserved.class.getName();
|
|
|
|
@Override
|
|
public String manifest(Object o) {
|
|
return o.getClass().getName();
|
|
}
|
|
|
|
@Override
|
|
public Object fromBinary(byte[] bytes, String manifest) throws NotSerializableException {
|
|
if (seatReservedManifest.equals(manifest)) {
|
|
// use generated protobuf serializer
|
|
try {
|
|
return seatReserved(FlightAppModels.SeatReserved.parseFrom(bytes));
|
|
} catch (InvalidProtocolBufferException e) {
|
|
throw new IllegalArgumentException(e.getMessage());
|
|
}
|
|
} else {
|
|
throw new NotSerializableException("Unable to handle manifest: " + manifest);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public byte[] toBinary(Object o) {
|
|
if (o instanceof SeatReserved) {
|
|
SeatReserved s = (SeatReserved) o;
|
|
return FlightAppModels.SeatReserved.newBuilder()
|
|
.setRow(s.row)
|
|
.setLetter(s.letter)
|
|
.setSeatType(s.seatType.code)
|
|
.build()
|
|
.toByteArray();
|
|
|
|
} else {
|
|
throw new IllegalArgumentException("Unable to handle: " + o);
|
|
}
|
|
}
|
|
|
|
// -- fromBinary helpers --
|
|
|
|
private SeatReserved seatReserved(FlightAppModels.SeatReserved p) {
|
|
return new SeatReserved(p.getLetter(), p.getRow(), seatType(p));
|
|
}
|
|
|
|
// handle missing field by assigning "Unknown" value
|
|
private SeatType seatType(FlightAppModels.SeatReserved p) {
|
|
if (p.hasSeatType()) return SeatType.fromCode(p.getSeatType());
|
|
else return SeatType.Unknown;
|
|
}
|
|
}
|
|
// #protobuf-read-optional
|
|
|
|
public static class RenamePlainJson {
|
|
public
|
|
// #rename-plain-json
|
|
static class JsonRenamedFieldAdapter implements EventAdapter {
|
|
// use your favorite json library
|
|
private final ExampleJsonMarshaller marshaller = new ExampleJsonMarshaller();
|
|
|
|
private final String V1 = "v1";
|
|
private final String V2 = "v2";
|
|
|
|
// this could be done independently for each event type
|
|
@Override
|
|
public String manifest(Object event) {
|
|
return V2;
|
|
}
|
|
|
|
@Override
|
|
public JsObject toJournal(Object event) {
|
|
return marshaller.toJson(event);
|
|
}
|
|
|
|
@Override
|
|
public EventSeq fromJournal(Object event, String manifest) {
|
|
if (event instanceof JsObject) {
|
|
JsObject json = (JsObject) event;
|
|
if (V1.equals(manifest)) json = rename(json, "code", "seatNr");
|
|
return EventSeq.single(json);
|
|
} else {
|
|
throw new IllegalArgumentException(
|
|
"Can only work with JSON, was: " + event.getClass().getName());
|
|
}
|
|
}
|
|
|
|
private JsObject rename(JsObject json, String from, String to) {
|
|
// use your favorite json library to rename the field
|
|
JsObject renamed = json;
|
|
return renamed;
|
|
}
|
|
}
|
|
// #rename-plain-json
|
|
}
|
|
|
|
public static class SimplestCustomSerializer {
|
|
|
|
public
|
|
// #simplest-custom-serializer-model
|
|
static class Person {
|
|
public final String name;
|
|
public final String surname;
|
|
|
|
public Person(String name, String surname) {
|
|
this.name = name;
|
|
this.surname = surname;
|
|
}
|
|
}
|
|
// #simplest-custom-serializer-model
|
|
|
|
public
|
|
// #simplest-custom-serializer
|
|
/**
|
|
* Simplest possible serializer, uses a string representation of the Person class.
|
|
*
|
|
* <p>Usually a serializer like this would use a library like: protobuf, kryo, avro, cap'n
|
|
* proto, flatbuffers, SBE or some other dedicated serializer backend to perform the actual
|
|
* to/from bytes marshalling.
|
|
*/
|
|
static class SimplestPossiblePersonSerializer extends SerializerWithStringManifest {
|
|
private final Charset utf8 = StandardCharsets.UTF_8;
|
|
|
|
private final String personManifest = Person.class.getName();
|
|
|
|
// unique identifier of the serializer
|
|
@Override
|
|
public int identifier() {
|
|
return 1234567;
|
|
}
|
|
|
|
// extract manifest to be stored together with serialized object
|
|
@Override
|
|
public String manifest(Object o) {
|
|
return o.getClass().getName();
|
|
}
|
|
|
|
// serialize the object
|
|
@Override
|
|
public byte[] toBinary(Object obj) {
|
|
if (obj instanceof Person) {
|
|
Person p = (Person) obj;
|
|
return (p.name + "|" + p.surname).getBytes(utf8);
|
|
} else {
|
|
throw new IllegalArgumentException(
|
|
"Unable to serialize to bytes, clazz was: " + obj.getClass().getName());
|
|
}
|
|
}
|
|
|
|
// deserialize the object, using the manifest to indicate which logic to apply
|
|
@Override
|
|
public Object fromBinary(byte[] bytes, String manifest) throws NotSerializableException {
|
|
if (personManifest.equals(manifest)) {
|
|
String nameAndSurname = new String(bytes, utf8);
|
|
String[] parts = nameAndSurname.split("[|]");
|
|
return new Person(parts[0], parts[1]);
|
|
} else {
|
|
throw new NotSerializableException(
|
|
"Unable to deserialize from bytes, manifest was: "
|
|
+ manifest
|
|
+ "! Bytes length: "
|
|
+ bytes.length);
|
|
}
|
|
}
|
|
}
|
|
// #simplest-custom-serializer
|
|
}
|
|
|
|
public static class SamplePayload {
|
|
private final Object payload;
|
|
|
|
public SamplePayload(Object payload) {
|
|
this.payload = payload;
|
|
}
|
|
|
|
public Object getPayload() {
|
|
return payload;
|
|
}
|
|
}
|
|
|
|
// #split-events-during-recovery
|
|
interface Version1 {};
|
|
|
|
interface Version2 {}
|
|
|
|
// #split-events-during-recovery
|
|
public
|
|
// #split-events-during-recovery
|
|
// V1 event:
|
|
static class UserDetailsChanged implements Version1 {
|
|
public final String name;
|
|
public final String address;
|
|
|
|
public UserDetailsChanged(String name, String address) {
|
|
this.name = name;
|
|
this.address = address;
|
|
}
|
|
}
|
|
|
|
// #split-events-during-recovery
|
|
public
|
|
// #split-events-during-recovery
|
|
// corresponding V2 events:
|
|
static class UserNameChanged implements Version2 {
|
|
public final String name;
|
|
|
|
public UserNameChanged(String name) {
|
|
this.name = name;
|
|
}
|
|
}
|
|
// #split-events-during-recovery
|
|
public
|
|
// #split-events-during-recovery
|
|
static class UserAddressChanged implements Version2 {
|
|
public final String address;
|
|
|
|
public UserAddressChanged(String address) {
|
|
this.address = address;
|
|
}
|
|
}
|
|
|
|
// #split-events-during-recovery
|
|
public
|
|
// #split-events-during-recovery
|
|
// event splitting adapter:
|
|
static class UserEventsAdapter implements EventAdapter {
|
|
@Override
|
|
public String manifest(Object event) {
|
|
return "";
|
|
}
|
|
|
|
@Override
|
|
public EventSeq fromJournal(Object event, String manifest) {
|
|
if (event instanceof UserDetailsChanged) {
|
|
UserDetailsChanged c = (UserDetailsChanged) event;
|
|
if (c.name == null) return EventSeq.single(new UserAddressChanged(c.address));
|
|
else if (c.address == null) return EventSeq.single(new UserNameChanged(c.name));
|
|
else return EventSeq.create(new UserNameChanged(c.name), new UserAddressChanged(c.address));
|
|
} else {
|
|
return EventSeq.single(event);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Object toJournal(Object event) {
|
|
return event;
|
|
}
|
|
}
|
|
// #split-events-during-recovery
|
|
|
|
public static class CustomerBlinked {
|
|
public final long customerId;
|
|
|
|
public CustomerBlinked(long customerId) {
|
|
this.customerId = customerId;
|
|
}
|
|
}
|
|
|
|
public
|
|
// #string-serializer-skip-deleved-event-by-manifest
|
|
static class EventDeserializationSkipped {
|
|
public static EventDeserializationSkipped instance = new EventDeserializationSkipped();
|
|
|
|
private EventDeserializationSkipped() {}
|
|
}
|
|
|
|
// #string-serializer-skip-deleved-event-by-manifest
|
|
public
|
|
// #string-serializer-skip-deleved-event-by-manifest
|
|
static class RemovedEventsAwareSerializer extends SerializerWithStringManifest {
|
|
private final Charset utf8 = StandardCharsets.UTF_8;
|
|
private final String customerBlinkedManifest = "blinked";
|
|
|
|
// unique identifier of the serializer
|
|
@Override
|
|
public int identifier() {
|
|
return 8337;
|
|
}
|
|
|
|
// extract manifest to be stored together with serialized object
|
|
@Override
|
|
public String manifest(Object o) {
|
|
if (o instanceof CustomerBlinked) return customerBlinkedManifest;
|
|
else return o.getClass().getName();
|
|
}
|
|
|
|
@Override
|
|
public byte[] toBinary(Object o) {
|
|
return o.toString().getBytes(utf8); // example serialization
|
|
}
|
|
|
|
@Override
|
|
public Object fromBinary(byte[] bytes, String manifest) {
|
|
if (customerBlinkedManifest.equals(manifest)) return EventDeserializationSkipped.instance;
|
|
else return new String(bytes, utf8);
|
|
}
|
|
}
|
|
// #string-serializer-skip-deleved-event-by-manifest
|
|
|
|
public
|
|
// #string-serializer-skip-deleved-event-by-manifest-adapter
|
|
static class SkippedEventsAwareAdapter implements EventAdapter {
|
|
@Override
|
|
public String manifest(Object event) {
|
|
return "";
|
|
}
|
|
|
|
@Override
|
|
public Object toJournal(Object event) {
|
|
return event;
|
|
}
|
|
|
|
@Override
|
|
public EventSeq fromJournal(Object event, String manifest) {
|
|
if (event == EventDeserializationSkipped.instance) return EventSeq.empty();
|
|
else return EventSeq.single(event);
|
|
}
|
|
}
|
|
// #string-serializer-skip-deleved-event-by-manifest-adapter
|
|
|
|
// #string-serializer-handle-rename
|
|
public
|
|
// #string-serializer-handle-rename
|
|
static class RenamedEventAwareSerializer extends SerializerWithStringManifest {
|
|
private final Charset utf8 = StandardCharsets.UTF_8;
|
|
|
|
// unique identifier of the serializer
|
|
@Override
|
|
public int identifier() {
|
|
return 8337;
|
|
}
|
|
|
|
private final String oldPayloadClassName =
|
|
"docs.persistence.OldPayload"; // class NOT available anymore
|
|
private final String myPayloadClassName = SamplePayload.class.getName();
|
|
|
|
// extract manifest to be stored together with serialized object
|
|
@Override
|
|
public String manifest(Object o) {
|
|
return o.getClass().getName();
|
|
}
|
|
|
|
@Override
|
|
public byte[] toBinary(Object o) {
|
|
if (o instanceof SamplePayload) {
|
|
SamplePayload s = (SamplePayload) o;
|
|
return s.payload.toString().getBytes(utf8);
|
|
} else {
|
|
// previously also handled "old" events here.
|
|
throw new IllegalArgumentException(
|
|
"Unable to serialize to bytes, clazz was: " + o.getClass().getName());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Object fromBinary(byte[] bytes, String manifest) throws NotSerializableException {
|
|
if (oldPayloadClassName.equals(manifest)) return new SamplePayload(new String(bytes, utf8));
|
|
else if (myPayloadClassName.equals(manifest))
|
|
return new SamplePayload(new String(bytes, utf8));
|
|
else throw new NotSerializableException("unexpected manifest [" + manifest + "]");
|
|
}
|
|
}
|
|
// #string-serializer-handle-rename
|
|
|
|
public
|
|
// #detach-models
|
|
// Domain model - highly optimised for domain language and maybe "fluent" usage
|
|
static class Customer {
|
|
public final String name;
|
|
|
|
public Customer(String name) {
|
|
this.name = name;
|
|
}
|
|
}
|
|
|
|
// #detach-models
|
|
public
|
|
// #detach-models
|
|
static class Seat {
|
|
public final String code;
|
|
|
|
public Seat(String code) {
|
|
this.code = code;
|
|
}
|
|
|
|
public SeatBooked bookFor(Customer customer) {
|
|
return new SeatBooked(code, customer);
|
|
}
|
|
}
|
|
|
|
// #detach-models
|
|
public
|
|
// #detach-models
|
|
static class SeatBooked {
|
|
public final String code;
|
|
public final Customer customer;
|
|
|
|
public SeatBooked(String code, Customer customer) {
|
|
this.code = code;
|
|
this.customer = customer;
|
|
}
|
|
}
|
|
|
|
// #detach-models
|
|
public
|
|
// #detach-models
|
|
// Data model - highly optimised for schema evolution and persistence
|
|
static class SeatBookedData {
|
|
public final String code;
|
|
public final String customerName;
|
|
|
|
public SeatBookedData(String code, String customerName) {
|
|
this.code = code;
|
|
this.customerName = customerName;
|
|
}
|
|
}
|
|
// #detach-models
|
|
|
|
// #detach-models-adapter
|
|
class DetachedModelsAdapter implements EventAdapter {
|
|
@Override
|
|
public String manifest(Object event) {
|
|
return "";
|
|
}
|
|
|
|
@Override
|
|
public Object toJournal(Object event) {
|
|
if (event instanceof SeatBooked) {
|
|
SeatBooked s = (SeatBooked) event;
|
|
return new SeatBookedData(s.code, s.customer.name);
|
|
} else {
|
|
throw new IllegalArgumentException("Unsupported: " + event.getClass());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public EventSeq fromJournal(Object event, String manifest) {
|
|
if (event instanceof SeatBookedData) {
|
|
SeatBookedData d = (SeatBookedData) event;
|
|
return EventSeq.single(new SeatBooked(d.code, new Customer(d.customerName)));
|
|
} else {
|
|
throw new IllegalArgumentException("Unsupported: " + event.getClass());
|
|
}
|
|
}
|
|
}
|
|
// #detach-models-adapter
|
|
|
|
public
|
|
// #detach-models-adapter-json
|
|
static class JsonDataModelAdapter implements EventAdapter {
|
|
|
|
// use your favorite json library
|
|
private final ExampleJsonMarshaller marshaller = new ExampleJsonMarshaller();
|
|
|
|
@Override
|
|
public String manifest(Object event) {
|
|
return "";
|
|
}
|
|
|
|
@Override
|
|
public JsObject toJournal(Object event) {
|
|
return marshaller.toJson(event);
|
|
}
|
|
|
|
@Override
|
|
public EventSeq fromJournal(Object event, String manifest) {
|
|
if (event instanceof JsObject) {
|
|
JsObject json = (JsObject) event;
|
|
return EventSeq.single(marshaller.fromJson(json));
|
|
} else {
|
|
throw new IllegalArgumentException(
|
|
"Unable to fromJournal a non-JSON object! Was: " + event.getClass());
|
|
}
|
|
}
|
|
}
|
|
// #detach-models-adapter-json
|
|
|
|
}
|