pekko/docs/src/test/java/jdocs/persistence/PersistenceSchemaEvolutionDocTest.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
}