2015-07-22 16:25:17 +02:00
|
|
|
/*
|
2016-01-25 10:16:14 +01:00
|
|
|
* Copyright (C) 2009-2016 Typesafe Inc. <http://www.typesafe.com>
|
2015-07-22 16:25:17 +02:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
package docs.persistence
|
|
|
|
|
|
|
|
|
|
import scala.language.reflectiveCalls
|
|
|
|
|
import java.nio.charset.Charset
|
|
|
|
|
import akka.actor.ActorSystem
|
|
|
|
|
import akka.persistence.journal.{ EventAdapter, EventSeq }
|
|
|
|
|
import akka.serialization.{ SerializationExtension, SerializerWithStringManifest }
|
|
|
|
|
import akka.testkit.TestKit
|
|
|
|
|
import com.typesafe.config._
|
|
|
|
|
import org.scalatest.WordSpec
|
|
|
|
|
import spray.json.JsObject
|
|
|
|
|
import scala.concurrent.duration._
|
2015-09-08 08:01:07 +02:00
|
|
|
import docs.persistence.proto.FlightAppModels
|
2015-07-22 16:25:17 +02:00
|
|
|
|
|
|
|
|
class PersistenceSchemaEvolutionDocSpec extends WordSpec {
|
|
|
|
|
|
|
|
|
|
val customSerializerConfig =
|
|
|
|
|
"""
|
|
|
|
|
//#custom-serializer-config
|
|
|
|
|
akka.actor {
|
|
|
|
|
serializers {
|
|
|
|
|
my-payload = "docs.persistence.MyPayloadSerializer"
|
|
|
|
|
my-snapshot = "docs.persistence.MySnapshotSerializer"
|
|
|
|
|
}
|
|
|
|
|
serialization-bindings {
|
|
|
|
|
"docs.persistence.MyPayload" = my-payload
|
|
|
|
|
"docs.persistence.MySnapshot" = my-snapshot
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
//#custom-serializer-config
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
val system = ActorSystem("PersistenceSchemaEvolutionDocSpec", ConfigFactory.parseString(customSerializerConfig))
|
|
|
|
|
try {
|
|
|
|
|
SerializationExtension(system)
|
|
|
|
|
} finally {
|
|
|
|
|
TestKit.shutdownActorSystem(system, 10.seconds, false)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class ProtobufReadOptional {
|
|
|
|
|
|
|
|
|
|
//#protobuf-read-optional-model
|
|
|
|
|
sealed abstract class SeatType { def code: String }
|
|
|
|
|
object SeatType {
|
|
|
|
|
def fromString(s: String) = s match {
|
|
|
|
|
case Window.code => Window
|
|
|
|
|
case Aisle.code => Aisle
|
|
|
|
|
case Other.code => Other
|
|
|
|
|
case _ => Unknown
|
|
|
|
|
}
|
|
|
|
|
case object Window extends SeatType { override val code = "W" }
|
|
|
|
|
case object Aisle extends SeatType { override val code = "A" }
|
|
|
|
|
case object Other extends SeatType { override val code = "O" }
|
|
|
|
|
case object Unknown extends SeatType { override val code = "" }
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case class SeatReserved(letter: String, row: Int, seatType: SeatType)
|
|
|
|
|
//#protobuf-read-optional-model
|
|
|
|
|
|
|
|
|
|
//#protobuf-read-optional
|
|
|
|
|
/**
|
|
|
|
|
* Example serializer impl which uses protocol buffers generated classes (proto.*)
|
|
|
|
|
* to perform the to/from binary marshalling.
|
|
|
|
|
*/
|
|
|
|
|
class AddedFieldsSerializerWithProtobuf extends SerializerWithStringManifest {
|
|
|
|
|
override def identifier = 67876
|
|
|
|
|
|
|
|
|
|
final val SeatReservedManifest = classOf[SeatReserved].getName
|
|
|
|
|
|
|
|
|
|
override def manifest(o: AnyRef): String = o.getClass.getName
|
|
|
|
|
|
|
|
|
|
override def fromBinary(bytes: Array[Byte], manifest: String): AnyRef =
|
|
|
|
|
manifest match {
|
|
|
|
|
case SeatReservedManifest =>
|
2015-09-08 08:01:07 +02:00
|
|
|
// use generated protobuf serializer
|
|
|
|
|
seatReserved(FlightAppModels.SeatReserved.parseFrom(bytes))
|
2015-07-22 16:25:17 +02:00
|
|
|
case _ =>
|
|
|
|
|
throw new IllegalArgumentException("Unable to handle manifest: " + manifest)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override def toBinary(o: AnyRef): Array[Byte] = o match {
|
|
|
|
|
case s: SeatReserved =>
|
2015-09-08 08:01:07 +02:00
|
|
|
FlightAppModels.SeatReserved.newBuilder
|
|
|
|
|
.setRow(s.row)
|
|
|
|
|
.setLetter(s.letter)
|
|
|
|
|
.setSeatType(s.seatType.code)
|
2015-07-22 16:25:17 +02:00
|
|
|
.build().toByteArray
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// -- fromBinary helpers --
|
|
|
|
|
|
2015-09-08 08:01:07 +02:00
|
|
|
private def seatReserved(p: FlightAppModels.SeatReserved): SeatReserved =
|
2015-07-22 16:25:17 +02:00
|
|
|
SeatReserved(p.getLetter, p.getRow, seatType(p))
|
|
|
|
|
|
|
|
|
|
// handle missing field by assigning "Unknown" value
|
2015-09-08 08:01:07 +02:00
|
|
|
private def seatType(p: FlightAppModels.SeatReserved): SeatType =
|
2015-07-22 16:25:17 +02:00
|
|
|
if (p.hasSeatType) SeatType.fromString(p.getSeatType) else SeatType.Unknown
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
//#protobuf-read-optional
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class ProtoBufRename {
|
|
|
|
|
val protoIDL = """
|
|
|
|
|
//#protobuf-rename-proto
|
|
|
|
|
// protobuf message definition, BEFORE:
|
|
|
|
|
message SeatReserved {
|
|
|
|
|
required string code = 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// protobuf message definition, AFTER:
|
|
|
|
|
message SeatReserved {
|
|
|
|
|
required string seatNr = 1; // field renamed, id remains the same
|
|
|
|
|
}
|
|
|
|
|
//#protobuf-rename-proto
|
|
|
|
|
"""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class RenamePlainJson {
|
|
|
|
|
//#rename-plain-json
|
|
|
|
|
class JsonRenamedFieldAdapter extends EventAdapter {
|
|
|
|
|
val marshaller = new ExampleJsonMarshaller
|
|
|
|
|
|
|
|
|
|
val V1 = "v1"
|
|
|
|
|
val V2 = "v2"
|
|
|
|
|
|
|
|
|
|
// this could be done independently for each event type
|
2015-09-08 08:01:07 +02:00
|
|
|
override def manifest(event: Any): String = V2
|
2015-07-22 16:25:17 +02:00
|
|
|
|
|
|
|
|
override def toJournal(event: Any): JsObject =
|
|
|
|
|
marshaller.toJson(event)
|
|
|
|
|
|
|
|
|
|
override def fromJournal(event: Any, manifest: String): EventSeq = event match {
|
|
|
|
|
case json: JsObject => EventSeq(marshaller.fromJson(manifest match {
|
2015-09-08 08:01:07 +02:00
|
|
|
case V1 => rename(json, "code", "seatNr")
|
2015-07-22 16:25:17 +02:00
|
|
|
case V2 => json // pass-through
|
|
|
|
|
case unknown => throw new IllegalArgumentException(s"Unknown manifest: $unknown")
|
|
|
|
|
}))
|
|
|
|
|
case _ =>
|
|
|
|
|
val c = event.getClass
|
|
|
|
|
throw new IllegalArgumentException("Can only work with JSON, was: %s".format(c))
|
|
|
|
|
}
|
|
|
|
|
|
2015-09-08 08:01:07 +02:00
|
|
|
def rename(json: JsObject, from: String, to: String): JsObject = {
|
|
|
|
|
val value = json.fields(from)
|
|
|
|
|
val withoutOld = json.fields - from
|
|
|
|
|
JsObject(withoutOld + (to -> value))
|
2015-07-22 16:25:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
//#rename-plain-json
|
|
|
|
|
}
|
|
|
|
|
|
2015-09-08 08:01:07 +02:00
|
|
|
object SimplestCustomSerializer {
|
2015-07-22 16:25:17 +02:00
|
|
|
|
|
|
|
|
//#simplest-custom-serializer-model
|
|
|
|
|
final case class Person(name: String, surname: String)
|
|
|
|
|
//#simplest-custom-serializer-model
|
|
|
|
|
|
|
|
|
|
//#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.
|
|
|
|
|
*/
|
|
|
|
|
class SimplestPossiblePersonSerializer extends SerializerWithStringManifest {
|
|
|
|
|
val Utf8 = Charset.forName("UTF-8")
|
|
|
|
|
|
|
|
|
|
val PersonManifest = classOf[Person].getName
|
|
|
|
|
|
|
|
|
|
// unique identifier of the serializer
|
|
|
|
|
def identifier = 1234567
|
|
|
|
|
|
|
|
|
|
// extract manifest to be stored together with serialized object
|
|
|
|
|
override def manifest(o: AnyRef): String = o.getClass.getName
|
|
|
|
|
|
|
|
|
|
// serialize the object
|
2015-09-08 08:01:07 +02:00
|
|
|
override def toBinary(obj: AnyRef): Array[Byte] = obj match {
|
2015-07-22 16:25:17 +02:00
|
|
|
case p: Person => s"""${p.name}|${p.surname}""".getBytes(Utf8)
|
|
|
|
|
case _ => throw new IllegalArgumentException(
|
|
|
|
|
s"Unable to serialize to bytes, clazz was: ${obj.getClass}!")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// deserialize the object, using the manifest to indicate which logic to apply
|
2015-09-08 08:01:07 +02:00
|
|
|
override def fromBinary(bytes: Array[Byte], manifest: String): AnyRef =
|
|
|
|
|
manifest match {
|
|
|
|
|
case PersonManifest =>
|
|
|
|
|
val nameAndSurname = new String(bytes, Utf8)
|
|
|
|
|
val Array(name, surname) = nameAndSurname.split("[|]")
|
|
|
|
|
Person(name, surname)
|
|
|
|
|
case _ => throw new IllegalArgumentException(
|
|
|
|
|
s"Unable to deserialize from bytes, manifest was: $manifest! Bytes length: " +
|
|
|
|
|
bytes.length)
|
|
|
|
|
}
|
2015-07-22 16:25:17 +02:00
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//#simplest-custom-serializer
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class PersonSerializerSettingsBox {
|
|
|
|
|
val PersonSerializerSettings = """
|
|
|
|
|
//#simplest-custom-serializer-config
|
|
|
|
|
# application.conf
|
|
|
|
|
akka {
|
|
|
|
|
actor {
|
|
|
|
|
serializers {
|
|
|
|
|
person = "docs.persistence.SimplestPossiblePersonSerializer"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
serialization-bindings {
|
|
|
|
|
"docs.persistence.Person" = person
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
//#simplest-custom-serializer-config
|
|
|
|
|
"""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final case class SamplePayload(p: Any)
|
|
|
|
|
|
|
|
|
|
//#split-events-during-recovery
|
|
|
|
|
trait V1
|
|
|
|
|
trait V2
|
|
|
|
|
|
|
|
|
|
// V1 event:
|
|
|
|
|
final case class UserDetailsChanged(name: String, address: String) extends V1
|
|
|
|
|
|
|
|
|
|
// corresponding V2 events:
|
|
|
|
|
final case class UserNameChanged(name: String) extends V2
|
|
|
|
|
final case class UserAddressChanged(address: String) extends V2
|
|
|
|
|
|
|
|
|
|
// event splitting adapter:
|
|
|
|
|
class UserEventsAdapter extends EventAdapter {
|
|
|
|
|
override def manifest(event: Any): String = ""
|
|
|
|
|
|
|
|
|
|
override def fromJournal(event: Any, manifest: String): EventSeq = event match {
|
|
|
|
|
case UserDetailsChanged(null, address) => EventSeq(UserAddressChanged(address))
|
|
|
|
|
case UserDetailsChanged(name, null) => EventSeq(UserNameChanged(name))
|
2015-08-19 13:58:29 +02:00
|
|
|
case UserDetailsChanged(name, address) =>
|
|
|
|
|
EventSeq(UserNameChanged(name),
|
|
|
|
|
UserAddressChanged(address))
|
2015-07-22 16:25:17 +02:00
|
|
|
case event: V2 => EventSeq(event)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override def toJournal(event: Any): Any = event
|
|
|
|
|
}
|
|
|
|
|
//#split-events-during-recovery
|
|
|
|
|
|
|
|
|
|
final case class CustomerBlinked(customerId: Long)
|
|
|
|
|
|
|
|
|
|
//#string-serializer-skip-deleved-event-by-manifest
|
|
|
|
|
case object EventDeserializationSkipped
|
|
|
|
|
|
|
|
|
|
class RemovedEventsAwareSerializer extends SerializerWithStringManifest {
|
2015-09-08 08:01:07 +02:00
|
|
|
val utf8 = Charset.forName("UTF-8")
|
2015-07-22 16:25:17 +02:00
|
|
|
override def identifier: Int = 8337
|
|
|
|
|
|
|
|
|
|
val SkipEventManifestsEvents = Set(
|
|
|
|
|
"docs.persistence.CustomerBlinked" // ...
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
override def manifest(o: AnyRef): String = o.getClass.getName
|
|
|
|
|
|
|
|
|
|
override def toBinary(o: AnyRef): Array[Byte] = o match {
|
2015-09-08 08:01:07 +02:00
|
|
|
case _ => o.toString.getBytes(utf8) // example serialization
|
2015-07-22 16:25:17 +02:00
|
|
|
}
|
|
|
|
|
|
2015-09-08 08:01:07 +02:00
|
|
|
override def fromBinary(bytes: Array[Byte], manifest: String): AnyRef =
|
|
|
|
|
manifest match {
|
|
|
|
|
case m if SkipEventManifestsEvents.contains(m) =>
|
|
|
|
|
EventDeserializationSkipped
|
2015-07-22 16:25:17 +02:00
|
|
|
|
2015-09-08 08:01:07 +02:00
|
|
|
case other => new String(bytes, utf8)
|
|
|
|
|
}
|
2015-07-22 16:25:17 +02:00
|
|
|
}
|
|
|
|
|
//#string-serializer-skip-deleved-event-by-manifest
|
|
|
|
|
|
|
|
|
|
//#string-serializer-skip-deleved-event-by-manifest-adapter
|
|
|
|
|
class SkippedEventsAwareAdapter extends EventAdapter {
|
|
|
|
|
override def manifest(event: Any) = ""
|
|
|
|
|
override def toJournal(event: Any) = event
|
|
|
|
|
|
|
|
|
|
override def fromJournal(event: Any, manifest: String) = event match {
|
|
|
|
|
case EventDeserializationSkipped => EventSeq.empty
|
|
|
|
|
case _ => EventSeq(event)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
//#string-serializer-skip-deleved-event-by-manifest-adapter
|
|
|
|
|
|
|
|
|
|
//#string-serializer-handle-rename
|
|
|
|
|
class RenamedEventAwareSerializer extends SerializerWithStringManifest {
|
|
|
|
|
val Utf8 = Charset.forName("UTF-8")
|
|
|
|
|
override def identifier: Int = 8337
|
|
|
|
|
|
|
|
|
|
val OldPayloadClassName = "docs.persistence.OldPayload" // class NOT available anymore
|
|
|
|
|
val MyPayloadClassName = classOf[SamplePayload].getName
|
|
|
|
|
|
|
|
|
|
override def manifest(o: AnyRef): String = o.getClass.getName
|
|
|
|
|
|
|
|
|
|
override def toBinary(o: AnyRef): Array[Byte] = o match {
|
|
|
|
|
case SamplePayload(data) => s"""$data""".getBytes(Utf8)
|
|
|
|
|
// previously also handled "old" events here.
|
|
|
|
|
}
|
|
|
|
|
|
2015-09-08 08:01:07 +02:00
|
|
|
override def fromBinary(bytes: Array[Byte], manifest: String): AnyRef =
|
|
|
|
|
manifest match {
|
|
|
|
|
case OldPayloadClassName => SamplePayload(new String(bytes, Utf8))
|
|
|
|
|
case MyPayloadClassName => SamplePayload(new String(bytes, Utf8))
|
|
|
|
|
case other => throw new Exception(s"unexpected manifest [$other]")
|
|
|
|
|
}
|
2015-07-22 16:25:17 +02:00
|
|
|
}
|
|
|
|
|
//#string-serializer-handle-rename
|
|
|
|
|
|
|
|
|
|
//#detach-models
|
|
|
|
|
/** Domain model - highly optimised for domain language and maybe "fluent" usage */
|
|
|
|
|
object DomainModel {
|
|
|
|
|
final case class Customer(name: String)
|
|
|
|
|
final case class Seat(code: String) {
|
|
|
|
|
def bookFor(customer: Customer): SeatBooked = SeatBooked(code, customer)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final case class SeatBooked(code: String, customer: Customer)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Data model - highly optimised for schema evolution and persistence */
|
|
|
|
|
object DataModel {
|
|
|
|
|
final case class SeatBooked(code: String, customerName: String)
|
|
|
|
|
}
|
|
|
|
|
//#detach-models
|
|
|
|
|
|
|
|
|
|
//#detach-models-adapter
|
|
|
|
|
class DetachedModelsAdapter extends EventAdapter {
|
|
|
|
|
override def manifest(event: Any): String = ""
|
|
|
|
|
|
|
|
|
|
override def toJournal(event: Any): Any = event match {
|
|
|
|
|
case DomainModel.SeatBooked(code, customer) =>
|
|
|
|
|
DataModel.SeatBooked(code, customer.name)
|
|
|
|
|
}
|
|
|
|
|
override def fromJournal(event: Any, manifest: String): EventSeq = event match {
|
|
|
|
|
case DataModel.SeatBooked(code, customerName) =>
|
|
|
|
|
EventSeq(DomainModel.SeatBooked(code, DomainModel.Customer(customerName)))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
//#detach-models-adapter
|
|
|
|
|
|
|
|
|
|
// act as-if JSON library
|
|
|
|
|
class ExampleJsonMarshaller {
|
|
|
|
|
def toJson(any: Any): JsObject = JsObject()
|
|
|
|
|
def fromJson(json: JsObject): Any = new Object
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//#detach-models-adapter-json
|
|
|
|
|
class JsonDataModelAdapter extends EventAdapter {
|
|
|
|
|
override def manifest(event: Any): String = ""
|
|
|
|
|
|
|
|
|
|
val marshaller = new ExampleJsonMarshaller
|
|
|
|
|
|
|
|
|
|
override def toJournal(event: Any): JsObject =
|
|
|
|
|
marshaller.toJson(event)
|
|
|
|
|
|
|
|
|
|
override def fromJournal(event: Any, manifest: String): EventSeq = event match {
|
|
|
|
|
case json: JsObject =>
|
|
|
|
|
EventSeq(marshaller.fromJson(json))
|
|
|
|
|
case _ =>
|
|
|
|
|
throw new IllegalArgumentException(
|
|
|
|
|
"Unable to fromJournal a non-JSON object! Was: " + event.getClass)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
//#detach-models-adapter-json
|