move code to src/test
* so that it compiles and tests pass * fix some additional snip references in getting started
This commit is contained in:
parent
413df8e0f4
commit
59f53e1a22
289 changed files with 45 additions and 45 deletions
|
|
@ -0,0 +1,380 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2017 Lightbend Inc. <http://www.lightbend.com>
|
||||
*/
|
||||
|
||||
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._
|
||||
import docs.persistence.proto.FlightAppModels
|
||||
|
||||
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 =>
|
||||
// use generated protobuf serializer
|
||||
seatReserved(FlightAppModels.SeatReserved.parseFrom(bytes))
|
||||
case _ =>
|
||||
throw new IllegalArgumentException("Unable to handle manifest: " + manifest)
|
||||
}
|
||||
|
||||
override def toBinary(o: AnyRef): Array[Byte] = o match {
|
||||
case s: SeatReserved =>
|
||||
FlightAppModels.SeatReserved.newBuilder
|
||||
.setRow(s.row)
|
||||
.setLetter(s.letter)
|
||||
.setSeatType(s.seatType.code)
|
||||
.build().toByteArray
|
||||
}
|
||||
|
||||
// -- fromBinary helpers --
|
||||
|
||||
private def seatReserved(p: FlightAppModels.SeatReserved): SeatReserved =
|
||||
SeatReserved(p.getLetter, p.getRow, seatType(p))
|
||||
|
||||
// handle missing field by assigning "Unknown" value
|
||||
private def seatType(p: FlightAppModels.SeatReserved): SeatType =
|
||||
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
|
||||
override def manifest(event: Any): String = V2
|
||||
|
||||
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 {
|
||||
case V1 => rename(json, "code", "seatNr")
|
||||
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))
|
||||
}
|
||||
|
||||
def rename(json: JsObject, from: String, to: String): JsObject = {
|
||||
val value = json.fields(from)
|
||||
val withoutOld = json.fields - from
|
||||
JsObject(withoutOld + (to -> value))
|
||||
}
|
||||
|
||||
}
|
||||
//#rename-plain-json
|
||||
}
|
||||
|
||||
object SimplestCustomSerializer {
|
||||
|
||||
//#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
|
||||
override def toBinary(obj: AnyRef): Array[Byte] = obj match {
|
||||
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
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//#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))
|
||||
case UserDetailsChanged(name, address) =>
|
||||
EventSeq(
|
||||
UserNameChanged(name),
|
||||
UserAddressChanged(address))
|
||||
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 {
|
||||
val utf8 = Charset.forName("UTF-8")
|
||||
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 {
|
||||
case _ => o.toString.getBytes(utf8) // example serialization
|
||||
}
|
||||
|
||||
override def fromBinary(bytes: Array[Byte], manifest: String): AnyRef =
|
||||
manifest match {
|
||||
case m if SkipEventManifestsEvents.contains(m) =>
|
||||
EventDeserializationSkipped
|
||||
|
||||
case other => new String(bytes, utf8)
|
||||
}
|
||||
}
|
||||
//#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.
|
||||
}
|
||||
|
||||
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]")
|
||||
}
|
||||
}
|
||||
//#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
|
||||
Loading…
Add table
Add a link
Reference in a new issue