+ per plugin scoped adapters + could be swapped during runtime +per EventAdapter now has manifest and is configurable ai la serializers + json examples in docs + including "completely manual" example in case one wants to add metadata TO the persisted event + better error reporting when misconfigured bindings + manifest is handled by in memory plugin - did not check if it works with LevelDB plugin yet > TODO: json example uses Gson, as that's simplest to do, can we use +per allows 1:n adapters, multiple adapters can be bound to 1 class
233 lines
No EOL
7.1 KiB
Scala
233 lines
No EOL
7.1 KiB
Scala
/*
|
|
* Copyright (C) 2015 Typesafe Inc. <http://www.typesafe.com>
|
|
*/
|
|
|
|
package docs.persistence
|
|
|
|
import akka.actor.{ ExtendedActorSystem, Props }
|
|
import akka.persistence.journal.{ EventAdapter, EventSeq }
|
|
import akka.persistence.{ PersistentActor, RecoveryCompleted }
|
|
import akka.testkit.{ AkkaSpec, TestProbe }
|
|
import com.google.gson.{ Gson, JsonElement }
|
|
|
|
import scala.collection.immutable
|
|
|
|
class PersistenceEventAdapterDocSpec(config: String) extends AkkaSpec(config) {
|
|
|
|
def this() {
|
|
this("""
|
|
akka.persistence.snapshot-store.plugin = "akka.persistence.snapshot-store.local"
|
|
|
|
//#event-adapters-config
|
|
akka.persistence.journal {
|
|
inmem {
|
|
event-adapters {
|
|
tagging = "docs.persistence.MyTaggingEventAdapter"
|
|
user-upcasting = "docs.persistence.UserUpcastingEventAdapter"
|
|
item-upcasting = "docs.persistence.ItemUpcastingEventAdapter"
|
|
}
|
|
|
|
event-adapter-bindings {
|
|
"docs.persistence.Item" = tagging
|
|
"docs.persistence.TaggedEvent" = tagging
|
|
"docs.persistence.v1.Event" = [user-upcasting, item-upcasting]
|
|
}
|
|
}
|
|
}
|
|
//#event-adapters-config
|
|
|
|
|
|
akka.persistence.journal {
|
|
auto-json-store {
|
|
class = "akka.persistence.journal.inmem.InmemJournal" # reuse inmem, as an example
|
|
|
|
event-adapters {
|
|
auto-json = "docs.persistence.MyAutoJsonEventAdapter"
|
|
}
|
|
|
|
event-adapter-bindings {
|
|
"docs.persistence.DomainEvent" = auto-json # to journal
|
|
"com.google.gson.JsonElement" = auto-json # from journal
|
|
}
|
|
}
|
|
|
|
manual-json-store {
|
|
class = "akka.persistence.journal.inmem.InmemJournal" # reuse inmem, as an example
|
|
|
|
event-adapters {
|
|
manual-json = "docs.persistence.MyManualJsonEventAdapter"
|
|
}
|
|
|
|
event-adapter-bindings {
|
|
"docs.persistence.DomainEvent" = manual-json # to journal
|
|
"com.google.gson.JsonElement" = manual-json # from journal
|
|
}
|
|
}
|
|
}
|
|
""")
|
|
}
|
|
|
|
"MyAutomaticJsonEventAdapter" must {
|
|
"demonstrate how to implement a JSON adapter" in {
|
|
val p = TestProbe()
|
|
|
|
val props = Props(new PersistentActor {
|
|
override def persistenceId: String = "json-actor"
|
|
override def journalPluginId: String = "akka.persistence.journal.auto-json-store"
|
|
|
|
override def receiveRecover: Receive = {
|
|
case RecoveryCompleted => // ignore...
|
|
case e => p.ref ! e
|
|
}
|
|
|
|
override def receiveCommand: Receive = {
|
|
case c => persist(c) { e => p.ref ! e }
|
|
}
|
|
})
|
|
|
|
val p1 = system.actorOf(props)
|
|
val m1 = Person("Caplin", 42)
|
|
val m2 = Box(13)
|
|
p1 ! m1
|
|
p1 ! m2
|
|
p.expectMsg(m1)
|
|
p.expectMsg(m2)
|
|
|
|
val p2 = system.actorOf(props)
|
|
p.expectMsg(m1)
|
|
p.expectMsg(m2)
|
|
}
|
|
}
|
|
|
|
"MyManualJsonEventAdapter" must {
|
|
"demonstrate how to implement a JSON adapter" in {
|
|
val p = TestProbe()
|
|
|
|
val props = Props(new PersistentActor {
|
|
override def persistenceId: String = "json-actor"
|
|
override def journalPluginId: String = "akka.persistence.journal.manual-json-store"
|
|
|
|
override def receiveRecover: Receive = {
|
|
case RecoveryCompleted => // ignore...
|
|
case e => p.ref ! e
|
|
}
|
|
|
|
override def receiveCommand: Receive = {
|
|
case c => persist(c) { e => p.ref ! e }
|
|
}
|
|
})
|
|
|
|
val p1 = system.actorOf(props)
|
|
val m1 = Person("Caplin", 42)
|
|
val m2 = Box(13)
|
|
p1 ! m1
|
|
p1 ! m2
|
|
p.expectMsg(m1)
|
|
p.expectMsg(m2)
|
|
|
|
val p2 = system.actorOf(props)
|
|
p.expectMsg(m1)
|
|
p.expectMsg(m2)
|
|
}
|
|
}
|
|
}
|
|
|
|
trait DomainEvent
|
|
case class Person(name: String, age: Int) extends DomainEvent
|
|
case class Box(length: Int) extends DomainEvent
|
|
|
|
case class MyTaggingJournalModel(payload: Any, tags: immutable.Set[String])
|
|
|
|
//#identity-event-adapter
|
|
class MyEventAdapter(system: ExtendedActorSystem) extends EventAdapter {
|
|
override def manifest(event: Any): String =
|
|
"" // when no manifest needed, return ""
|
|
|
|
override def toJournal(event: Any): Any =
|
|
event // identity
|
|
|
|
override def fromJournal(event: Any, manifest: String): EventSeq =
|
|
EventSeq.single(event) // identity
|
|
}
|
|
//#identity-event-adapter
|
|
|
|
/**
|
|
* This is an example adapter which completely takes care of domain<->json translation.
|
|
* It allows the journal to take care of the manifest handling, which is the FQN of the serialized class.
|
|
*/
|
|
class MyAutoJsonEventAdapter(system: ExtendedActorSystem) extends EventAdapter {
|
|
private val gson = new Gson
|
|
|
|
override def manifest(event: Any): String =
|
|
event.getClass.getCanonicalName
|
|
|
|
override def toJournal(event: Any): Any = gson.toJsonTree(event)
|
|
|
|
override def fromJournal(event: Any, manifest: String): EventSeq = EventSeq.single {
|
|
event match {
|
|
case json: JsonElement =>
|
|
val clazz = system.dynamicAccess.getClassFor[Any](manifest).get
|
|
gson.fromJson(json, clazz)
|
|
}
|
|
}
|
|
}
|
|
|
|
class MyUpcastingEventAdapter(system: ExtendedActorSystem) extends EventAdapter {
|
|
override def manifest(event: Any): String = ""
|
|
|
|
override def toJournal(event: Any): Any = ???
|
|
|
|
override def fromJournal(event: Any, manifest: String): EventSeq = ???
|
|
}
|
|
|
|
/**
|
|
* This is an example adapter which completely takes care of domain<->json translation.
|
|
* It manually takes care of smuggling through the manifest even if the journal does not do anything in this regard,
|
|
* which is the case in this example since we're persisting to the inmem journal, which does nothing in terms of manifest.
|
|
*/
|
|
class MyManualJsonEventAdapter(system: ExtendedActorSystem) extends EventAdapter {
|
|
|
|
private val gson = new Gson
|
|
|
|
override def manifest(event: Any): String = event.getClass.getCanonicalName
|
|
|
|
override def toJournal(event: Any): Any = {
|
|
val out = gson.toJsonTree(event).getAsJsonObject
|
|
|
|
// optionally can include manifest in the adapted event.
|
|
// some journals will store the manifest transparently
|
|
out.addProperty("_manifest", manifest(event))
|
|
|
|
out
|
|
}
|
|
|
|
override def fromJournal(event: Any, m: String): EventSeq = event match {
|
|
case json: JsonElement =>
|
|
val manifest = json.getAsJsonObject.get("_manifest").getAsString
|
|
|
|
val clazz = system.dynamicAccess.getClassFor[Any](manifest).get
|
|
EventSeq.single(gson.fromJson(json, clazz))
|
|
}
|
|
}
|
|
|
|
class MyTaggingEventAdapter(system: ExtendedActorSystem) extends EventAdapter {
|
|
override def manifest(event: Any): String = ""
|
|
|
|
override def fromJournal(event: Any, manifest: String): EventSeq = event match {
|
|
case j: MyTaggingJournalModel => EventSeq.single(j)
|
|
}
|
|
|
|
override def toJournal(event: Any): Any = {
|
|
event match {
|
|
case Person(_, age) if age >= 18 => MyTaggingJournalModel(event, tags = Set("adult"))
|
|
case Person(_, age) => MyTaggingJournalModel(event, tags = Set("minor"))
|
|
case _ => MyTaggingJournalModel(event, tags = Set.empty)
|
|
}
|
|
}
|
|
}
|
|
|
|
object v1 {
|
|
trait Event
|
|
trait UserEvent extends v1.Event
|
|
trait ItemEvent extends v1.Event
|
|
} |