-
Notifications
You must be signed in to change notification settings - Fork 30
Add possibility to unpersist data one generation younger #71
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,7 +26,7 @@ package object migrations { | |
| * creating Migrator[T, V2], etc. Its migration will be the identity | ||
| * function so calling its migrate function will not have any effect. | ||
| */ | ||
| def from[T, V <: V1: VersionInfo]: Migrator[T, V] = new Migrator[T, V](Map(Version.numberFor[V] → identityMigration[T])) | ||
| def from[T, V <: V1: VersionInfo]: Migrator[T, V] = new Migrator[T, V](Map(Version.numberFor[V] → identityMigration[T]), None) | ||
| } | ||
|
|
||
| package migrations { | ||
|
|
@@ -36,40 +36,71 @@ package migrations { | |
|
|
||
| /** | ||
| * A `Migrator[R, V]` can migrate raw values of type R from older | ||
| * versions to version `V` by applying a specific `Migration[R]` to it. | ||
| * versions to version `V` or from version one generation younger | ||
| * than `V` back to `V` by applying a specific `Migration[R]` to it. | ||
| * | ||
| * You can create instances of `Migrator[R, V]` by using | ||
| * a small type-safe DSL consisting of two parts: the | ||
| * `from[R, V1]` function will create a | ||
| * `Migrator[R, V1]` and then you can use the | ||
| * `to[V](migration: Migration[R])` function to build | ||
| * instances that can migrate multiple versions. | ||
| * `backFrom[V]` can be used to define migration from version | ||
| * one generation younger than current `Migrator` version. | ||
| * This functionality can be useful when you want to do | ||
| * e.g. rolling update in clustered application - first | ||
| * deploy to all nodes app that still persists events with current | ||
| * version but is able to read also newer events, then deploy | ||
| * app that actually saves data in new format. | ||
| * Note that `backFrom[V]` is effective only when | ||
| * it is called as the last one in call chain when defining | ||
| * `Migrator[R, V]` | ||
| * | ||
| * @example Using the json implementation: | ||
| * {{{ | ||
| * val p = persister[CartCreated, V3]("cart-created", | ||
| * from[JsValue, V1] | ||
| * .to[V2](_.update('cart / 'items / * / 'price ! set[Int](1000))) | ||
| * .to[V3](_.update('timestamp ! set[Long](System.currentTimeMillis - 3600000L))) | ||
| * .backFrom[V4](_.update('cart / 'items / * / 'name ! set[String]("unknown"))) | ||
| * ) | ||
| * }}} | ||
| * | ||
| * @tparam R The type of raw data being migrated. In the JSON implementation this would be `JsValue`. | ||
| * @tparam V The "current" version of this Migrator, i.e. it can migrate values from V1 to this version or any version in between. | ||
| * @tparam V The "current" version of this Migrator, i.e. it can migrate values from V1 to this version or any version in between | ||
| * and optionally from next version back to this one. | ||
| */ | ||
| class Migrator[R, V <: Version: VersionInfo] private[stamina] (migrations: Map[Int, Migration[R]] = Map.empty) { | ||
| def canMigrate(fromVersion: Int): Boolean = migrations.contains(fromVersion) | ||
| class Migrator[R, V <: Version : VersionInfo] private[stamina](migrations: Map[Int, Migration[R]] = Map.empty, backwardMigration: Option[Migration[R]] = None) { | ||
| def canMigrate(fromVersion: Int): Boolean = migrations.contains(fromVersion) || (backwardMigration.isDefined && fromVersion == Version.numberFor[V] + 1) | ||
|
|
||
| def migrate(value: R, fromVersion: Int): R = { | ||
| migrations.get(fromVersion).map(_.apply(value)).getOrElse( | ||
| throw UndefinedMigrationException(fromVersion, Version.numberFor[V]) | ||
| ) | ||
| val thisVersion = Version.numberFor[V] | ||
| if (fromVersion <= thisVersion) { | ||
| migrations.get(fromVersion).map(_.apply(value)).getOrElse( | ||
| throw UndefinedMigrationException(fromVersion, thisVersion) | ||
| ) | ||
| } else if (fromVersion == thisVersion + 1) { | ||
| backwardMigration.map(_.apply(value)).getOrElse( | ||
| throw UndefinedMigrationException(fromVersion, thisVersion) | ||
| ) | ||
| } else { | ||
| throw UndefinedMigrationException(fromVersion, thisVersion) | ||
| } | ||
| } | ||
|
|
||
| def to[NextV <: Version: VersionInfo](migration: Migration[R])(implicit isNextAfter: IsNextVersionAfter[NextV, V]) = { | ||
| def to[NextV <: Version : VersionInfo](migration: Migration[R])(implicit isNextAfter: IsNextVersionAfter[NextV, V]): Migrator[R, NextV] = { | ||
| new Migrator[R, NextV]( | ||
| migrations.mapValues(_ && migration) + (Version.numberFor[NextV] → identityMigration[R]) | ||
| migrations.mapValues(_ && migration) + (Version.numberFor[NextV] → identityMigration[R]), | ||
| None | ||
| ) | ||
| } | ||
|
|
||
| def backFrom[NextV <: Version : VersionInfo](migration: Migration[R])(implicit isNextAfter: IsNextVersionAfter[NextV, V]): Migrator[R, V] = { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about renaming this method to While this: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| new Migrator[R, V]( | ||
| migrations, | ||
| Some(migration) | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| package stamina.migrations | ||
|
|
||
| import stamina._ | ||
|
|
||
| class MigratorSpec extends StaminaSpec { | ||
|
|
||
| val mV3WithBackwardMigration: Migrator[String, V3] = | ||
| from[String, V1] | ||
| .to[V2](_ + "V2") | ||
| .to[V3](_ + "V3") | ||
| .backFrom[V4](_.replace("V4", "")) | ||
|
|
||
| val mV3WithIgnoredBackwardMigration: Migrator[String, V3] = | ||
| from[String, V1] | ||
| .to[V2](_ + "V2") | ||
| .backFrom[V3](_ + "this should not be added") | ||
| .to[V3](_ + "V3") | ||
|
|
||
| val mV1WithBackwardMigration: Migrator[String, V1] = | ||
| from[String, V1] | ||
| .backFrom[V2](_.replace("V2", "")) | ||
|
|
||
| "Migrator with backward migration" should { | ||
| "be able to migrate" when { | ||
| "migration is from V1" in { | ||
| mV3WithBackwardMigration.canMigrate(1) shouldBe true | ||
| } | ||
|
|
||
| "migration is from V2" in { | ||
| mV3WithBackwardMigration.canMigrate(2) shouldBe true | ||
| } | ||
|
|
||
| "migration is from V3 (identity)" in { | ||
| mV3WithBackwardMigration.canMigrate(3) shouldBe true | ||
| } | ||
|
|
||
| "migration is from V4 (backward migration)" in { | ||
| mV3WithBackwardMigration.canMigrate(4) shouldBe true | ||
| } | ||
| } | ||
|
|
||
| "not be able to migrate" when { | ||
| "migration is from V5" in { | ||
| mV3WithBackwardMigration.canMigrate(5) shouldBe false | ||
| } | ||
| } | ||
|
|
||
| "migrate forward" when { | ||
| "migration is from V1" in { | ||
| mV3WithBackwardMigration.migrate("V1", 1) shouldBe "V1V2V3" | ||
| } | ||
|
|
||
| "migration is from V2" in { | ||
| mV3WithBackwardMigration.migrate("V1V2", 2) shouldBe "V1V2V3" | ||
| } | ||
|
|
||
| "migration is from V3" in { | ||
| mV3WithBackwardMigration.migrate("V1V2V3", 3) shouldBe "V1V2V3" | ||
| } | ||
| } | ||
|
|
||
| "migrate backward" when { | ||
| "migration is from V4" in { | ||
| mV3WithBackwardMigration.migrate("V1V2V3V4", 4) shouldBe "V1V2V3" | ||
| } | ||
| } | ||
| } | ||
|
|
||
| "Migrator with ignored backward migration" should { | ||
| "be able to migrate" when { | ||
| "migration is from V1" in { | ||
| mV3WithIgnoredBackwardMigration.canMigrate(1) shouldBe true | ||
| } | ||
|
|
||
| "migration is from V2" in { | ||
| mV3WithIgnoredBackwardMigration.canMigrate(2) shouldBe true | ||
| } | ||
|
|
||
| "migration is from V3 (identity)" in { | ||
| mV3WithIgnoredBackwardMigration.canMigrate(3) shouldBe true | ||
| } | ||
| } | ||
|
|
||
| "not be able to migrate" when { | ||
| "migration is from V4" in { | ||
| mV3WithIgnoredBackwardMigration.canMigrate(4) shouldBe false | ||
| } | ||
| } | ||
|
|
||
| "migrate" when { | ||
| "migration is from V1" in { | ||
| mV3WithIgnoredBackwardMigration.migrate("V1", 1) shouldBe "V1V2V3" | ||
| } | ||
|
|
||
| "migration is from V2" in { | ||
| mV3WithIgnoredBackwardMigration.migrate("V1V2", 2) shouldBe "V1V2V3" | ||
| } | ||
|
|
||
| "migration is from V3" in { | ||
| mV3WithIgnoredBackwardMigration.migrate("V1V2V3", 3) shouldBe "V1V2V3" | ||
| } | ||
| } | ||
| } | ||
|
|
||
| "Migrator V1 with backward migration" should { | ||
| "be able to migrate" when { | ||
| "migration is from V2" in { | ||
| mV1WithBackwardMigration.canMigrate(2) shouldBe true | ||
| } | ||
| } | ||
|
|
||
| "migrate" when { | ||
| "migration is from V2" in { | ||
| mV1WithBackwardMigration.migrate("V1V2", 2) shouldBe "V1" | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -45,13 +45,22 @@ package object json { | |
| * and unpersist version 1. Use this function to produce the initial persister | ||
| * for a new domain class/event/entity. | ||
| */ | ||
| def persister[T: RootJsonFormat: ClassTag](key: String): JsonPersister[T, V1] = new V1JsonPersister[T](key) | ||
| def persister[T: RootJsonFormat: ClassTag](key: String): JsonPersister[T, V1] = new V1JsonPersister[T](key, from[V1]) | ||
|
|
||
| /** | ||
| * Creates a JsonPersister[T, V1], i.e. a JsonPersister that will only persist | ||
| * version 1 and can unpersist from both versions 1 and 2. | ||
| * Provided Migrator should be able to migrate values from version 2 back to | ||
| * version 1, it can be achieved by defining it like | ||
| * `from[V1].backFrom[V2](identity)`. | ||
| */ | ||
| def persister[T: RootJsonFormat: ClassTag](key: String, migrator: JsonMigrator[V1]): JsonPersister[T, V1] = new V1JsonPersister[T](key, migrator) | ||
|
|
||
| /** | ||
| * Creates a JsonPersister[T, V] where V is a version greater than V1. | ||
| * It will always persist instances of T to version V but it will use the specified | ||
| * JsonMigrator[V] to migrate any values older than version V to version V before | ||
| * unpersisting them. | ||
| * JsonMigrator[V] to migrate any values older or or one generation younger | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Typo: The word |
||
| * than version V to version V before unpersisting them. | ||
| */ | ||
| def persister[T: RootJsonFormat: ClassTag, V <: Version: VersionInfo: MigratableVersion](key: String, migrator: JsonMigrator[V]): JsonPersister[T, V] = new VnJsonPersister[T, V](key, migrator) | ||
|
|
||
|
|
@@ -69,7 +78,9 @@ package json { | |
| s"""JsonPersister[${implicitly[ClassTag[T]].runtimeClass.getSimpleName}, V${currentVersion}](key = "${key}") cannot unpersist data with key "${p.key}" and version ${p.version}.""" | ||
| } | ||
|
|
||
| private[json] class V1JsonPersister[T: RootJsonFormat: ClassTag](key: String) extends JsonPersister[T, V1](key) { | ||
| private[json] class V1JsonPersister[T: RootJsonFormat: ClassTag](key: String, migrator: JsonMigrator[V1]) extends JsonPersister[T, V1](key) { | ||
| override def canUnpersist(p: Persisted): Boolean = p.key == key && migrator.canMigrate(p.version) | ||
|
|
||
| def persist(t: T): Persisted = Persisted(key, currentVersion, toJsonBytes(t)) | ||
| def unpersist(p: Persisted): T = { | ||
| if (canUnpersist(p)) fromJsonBytes[T](p.bytes) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's better to use the word "newer" instead of "younger" to describe Versions with greater numbers than the older ones. It's just a vocabulary clarification, as we had misunderstanding discussing this.