534 lines
15 KiB
Java
534 lines
15 KiB
Java
/**
|
|
* Copyright (C) 2009-2017 Lightbend Inc. <http://www.lightbend.com>
|
|
*/
|
|
|
|
package docs.persistence;
|
|
|
|
import docs.persistence.proto.FlightAppModels;
|
|
import java.nio.charset.Charset;
|
|
import spray.json.JsObject;
|
|
|
|
import akka.japi.Util;
|
|
import akka.persistence.journal.EventAdapter;
|
|
import akka.persistence.journal.EventSeq;
|
|
import akka.protobuf.InvalidProtocolBufferException;
|
|
import akka.serialization.SerializerWithStringManifest;
|
|
|
|
public class PersistenceSchemaEvolutionDocTest {
|
|
|
|
static
|
|
//#protobuf-read-optional-model
|
|
public 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
|
|
|
|
static
|
|
//#protobuf-read-optional-model
|
|
public 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
|
|
|
|
static
|
|
//#protobuf-read-optional
|
|
/**
|
|
* Example serializer impl which uses protocol buffers generated classes (proto.*)
|
|
* to perform the to/from binary marshalling.
|
|
*/
|
|
public 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) {
|
|
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 IllegalArgumentException("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 {
|
|
static
|
|
//#rename-plain-json
|
|
public 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 {
|
|
|
|
static
|
|
//#simplest-custom-serializer-model
|
|
public 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
|
|
|
|
static
|
|
//#simplest-custom-serializer
|
|
/**
|
|
* Simplest possible serializer, uses a string representation of the Person class.
|
|
*
|
|
* 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.
|
|
*/
|
|
public class SimplestPossiblePersonSerializer extends SerializerWithStringManifest {
|
|
private final Charset utf8 = Charset.forName("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) {
|
|
if (personManifest.equals(manifest)) {
|
|
String nameAndSurname = new String(bytes, utf8);
|
|
String[] parts = nameAndSurname.split("[|]");
|
|
return new Person(parts[0], parts[1]);
|
|
} else {
|
|
throw new IllegalArgumentException(
|
|
"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 V1 {};
|
|
interface V2 {}
|
|
|
|
//#split-events-during-recovery
|
|
static
|
|
//#split-events-during-recovery
|
|
// V1 event:
|
|
public class UserDetailsChanged implements V1 {
|
|
public final String name;
|
|
public final String address;
|
|
public UserDetailsChanged(String name, String address) {
|
|
this.name = name;
|
|
this.address = address;
|
|
}
|
|
}
|
|
|
|
//#split-events-during-recovery
|
|
static
|
|
//#split-events-during-recovery
|
|
// corresponding V2 events:
|
|
public class UserNameChanged implements V2 {
|
|
public final String name;
|
|
|
|
public UserNameChanged(String name) {
|
|
this.name = name;
|
|
}
|
|
}
|
|
//#split-events-during-recovery
|
|
static
|
|
//#split-events-during-recovery
|
|
public class UserAddressChanged implements V2 {
|
|
public final String address;
|
|
|
|
public UserAddressChanged(String address) {
|
|
this.address = address;
|
|
}
|
|
}
|
|
|
|
//#split-events-during-recovery
|
|
static
|
|
//#split-events-during-recovery
|
|
// event splitting adapter:
|
|
public 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
|
|
|
|
|
|
static public class CustomerBlinked {
|
|
public final long customerId;
|
|
|
|
public CustomerBlinked(long customerId) {
|
|
this.customerId = customerId;
|
|
}
|
|
}
|
|
|
|
static
|
|
//#string-serializer-skip-deleved-event-by-manifest
|
|
public class EventDeserializationSkipped {
|
|
public static EventDeserializationSkipped instance =
|
|
new EventDeserializationSkipped();
|
|
|
|
private EventDeserializationSkipped() {
|
|
}
|
|
}
|
|
|
|
//#string-serializer-skip-deleved-event-by-manifest
|
|
static
|
|
//#string-serializer-skip-deleved-event-by-manifest
|
|
public class RemovedEventsAwareSerializer extends SerializerWithStringManifest {
|
|
private final Charset utf8 = Charset.forName("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
|
|
|
|
static
|
|
//#string-serializer-skip-deleved-event-by-manifest-adapter
|
|
public 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
|
|
static
|
|
//#string-serializer-handle-rename
|
|
public class RenamedEventAwareSerializer extends SerializerWithStringManifest {
|
|
private final Charset utf8 = Charset.forName("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) {
|
|
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 IllegalArgumentException("unexpected manifest [" + manifest + "]");
|
|
}
|
|
}
|
|
//#string-serializer-handle-rename
|
|
|
|
static
|
|
//#detach-models
|
|
// Domain model - highly optimised for domain language and maybe "fluent" usage
|
|
public class Customer {
|
|
public final String name;
|
|
|
|
public Customer(String name) {
|
|
this.name = name;
|
|
}
|
|
}
|
|
|
|
//#detach-models
|
|
static
|
|
//#detach-models
|
|
public 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
|
|
static
|
|
//#detach-models
|
|
public class SeatBooked {
|
|
public final String code;
|
|
public final Customer customer;
|
|
|
|
public SeatBooked(String code, Customer customer) {
|
|
this.code = code;
|
|
this.customer = customer;
|
|
}
|
|
}
|
|
|
|
//#detach-models
|
|
static
|
|
//#detach-models
|
|
// Data model - highly optimised for schema evolution and persistence
|
|
public 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
|
|
|
|
static
|
|
//#detach-models-adapter-json
|
|
public 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
|
|
|
|
}
|
|
|