All examples are runnable. Checkout our github repository:
git clone https://github.com/javers/javers.git
cd javers
Run examples as unit tests:
./gradlew javers-core:test --tests JqlExample
Overview
JQL (JaVers Query Language) is a simple, fluent API which allows you to query JaversRepository for changes of a given class, object or property.
It’s not such a powerful language like SQL because it’s the abstraction over native languages used by concrete JaversRepository implementations (like SQL, MongoDB).
In the example below, we show all types of JQL queries. We use Groovy and Spock as these languages are far more readable for BDD-style tests than Java.
Data history views
Data history can be fetched from JaversRepository using javers.find*()
methods in one of three views:
Shadows, Changes, and Snapshots.
- Shadow is a historical version of a domain object restored from a snapshot.
- Change represents an atomic difference between two objects.
- Snapshot is a historical state of a domain object captured as the
property:value
map.
List of Examples
There are three find*()
methods:
- findShadows() for the Shadows view,
- findChanges() for the Changes view,
- findSnapshots() for the Snapshots view.
There are four types of queries:
- query for Entity changes by Instance Id,
- query for ValueObject changes,
- query by object’s class,
- query for any object changes.
Queries can have one or more optional filters:
- changed property,
- limit,
- skip,
- author,
- commitProperty,
- commitDate,
- commitId,
- snapshot version,
- child ValueObjects,
- initial Changes.
JQL can adapt when you refactor your domain classes:
- refactoring Entities with
@TypeName
, - free refactoring of ValueObjects.
Find methods
All find*()
methods understand JQL so you can use the same JqlQuery to get Changes, Shadows and Snapshots views.
Querying for Shadows
Shadows (see Shadow.java
) offer the most natural view on data history.
Thanks to JaVers magic, you can see historical versions of your domain objects
reconstructed from Snapshots.
Since Shadows are instances of your domain classes, you can use them easily in your application. Moreover, the JQL engine strives to rebuild original object graphs.
See how it works — JqlExample.groovy
:
def "should query for Shadows of an object"() {
given:
def javers = JaversBuilder.javers().build()
def bob = new Employee(name: "bob",
salary: 1000,
primaryAddress: new Address("London"))
javers.commit("author", bob) // initial commit
bob.salary = 1200 // changes
bob.primaryAddress.city = "Paris" //
javers.commit("author", bob) // second commit
when:
List<Shadow<Employee>> shadows = javers.findShadows(
QueryBuilder.byInstance(bob).build())
then:
assert shadows.size() == 2
Employee bobNew = shadows[0].get() // Employee shadows are instances
Employee bobOld = shadows[1].get() // of Employee.class
bobNew.salary == 1200
bobOld.salary == 1000
bobNew.primaryAddress.city == "Paris" // Employee shadows are linked
bobOld.primaryAddress.city == "London" // to Address Shadows
shadows[0].commitMetadata.id.majorId == 2
shadows[1].commitMetadata.id.majorId == 1
}
Shadow Scopes
Shadow reconstruction comes with one limitation — the query scope. Shadows inside the scope are loaded eagerly. References to Shadows outside the scope are simply nulled. There is no Hibernate-style lazy loading.
There are four scopes.
The wider the scope, the more object shadows are loaded to the resulting graph
(and the more database queries are executed).
Scopes are defined in the
ShadowScope
enum.
-
Shallow — the defult scope — Shadows are created only from snapshots selected directly in the main JQL query.
-
Child-value-object — JaVers loads all child Value Objects owned by selected Entities. Since 3.7.5, this scope is implicitly enabled for all Shadow queries and can’t be disabled.
-
Commit-deep — Shadows are created from all snapshots saved in commits touched by the main query.
-
Deep+ — JaVers tries to restore full object graphs with (possibly) all objects loaded.
The following example shows how the scopes work — JqlExample.groovy
:
def "should query for Shadows with different scopes"(){
given:
def javers = JaversBuilder.javers().build()
// /-> John -> Steve
// Bob
// \-> #address
def steve = new Employee(name: 'steve')
def john = new Employee(name: 'john', boss: steve)
def bob = new Employee(name: 'bob', boss: john, primaryAddress: new Address('London'))
javers.commit('author', steve) // commit 1.0 with snapshot of Steve
javers.commit('author', bob) // commit 2.0 with snapshots of Bob, Bob#address and John
bob.salary = 1200 // the change
javers.commit('author', bob) // commit 3.0 with snapshot of Bob
when: 'shallow scope query'
def shadows = javers.findShadows(QueryBuilder.byInstance(bob).build())
Employee bobShadow = shadows[0].get() //get the latest version of Bob
then:
assert shadows.size() == 2 //we have 2 shadows of Bob
assert bobShadow.name == 'bob'
// referenced entities are outside the query scope so they are nulled
assert bobShadow.boss == null
// child Value Objects are always in scope
assert bobShadow.primaryAddress.city == 'London'
when: 'commit-deep scope query'
shadows = javers.findShadows(QueryBuilder.byInstance(bob)
.withScopeCommitDeep().build())
bobShadow = shadows[0].get()
then:
assert bobShadow.boss.name == 'john' // John is inside the query scope, so his
// shadow is loaded and linked to Bob
assert bobShadow.boss.boss == null // Steve is still outside the scope
assert bobShadow.primaryAddress.city == 'London'
when: 'deep+2 scope query'
shadows = javers.findShadows(QueryBuilder.byInstance(bob)
.withScopeDeepPlus(2).build())
bobShadow = shadows[0].get()
then: 'all objects are loaded'
assert bobShadow.boss.name == 'john'
assert bobShadow.boss.boss.name == 'steve' // Steve is loaded thanks to deep+2 scope
assert bobShadow.primaryAddress.city == 'London'
}
If you want to be 100% sure that Shadow reconstruction didn’t hide some details — use Snapshots or Changes view.
Read more about Shadow query scopes, profiling, and runtime statistics in
the Javers.findShadows()
javadoc.
Querying for Changes
The Changes view (see Changes.java
)
is the list of atomic differences between subsequent versions of a domain object.
Since JaVers stores only Snapshots of domain objects,
Changes are recalculated by the JQL engine as the diff between subsequent Snapshots loaded from the JaversRepository.
There are three main types of Changes:
NewObject
— when an object is committed to the JaversRepository for the first time,ObjectRemoved
— when an object is deleted from the JaversRepository,PropertyChange
— most common — a changed property of an object (field or getter).
PropertyChange has the following subtypes:
ContainerChange
— list of changed items in Set, List or Array,MapChange
— list of changed Map entries,ReferenceChange
— changed Entity reference,ValueChange
— changed Primitive or Value.
See how it works — JqlExample.groovy
:
def "should query for Changes made on any object"() {
given:
def javers = JaversBuilder.javers().build()
def bob = new Employee(name: "bob",
salary: 1000,
primaryAddress: new Address("London"))
javers.commit("author", bob) // initial commit
bob.salary = 1200 // changes
bob.primaryAddress.city = "Paris" //
javers.commit("author", bob) // second commit
when:
Changes changes = javers.findChanges( QueryBuilder.anyDomainObject().build() )
println changes.prettyPrint()
then:
def lastCommitChanges = changes.groupByCommit()[0].changes
assert lastCommitChanges.size() == 2
ValueChange salaryChange = lastCommitChanges.find{it.propertyName == "salary"}
ValueChange cityChange = lastCommitChanges.find{it.propertyName == "city"}
assert salaryChange.left == 1000
assert salaryChange.right == 1200
assert cityChange.left == "London"
assert cityChange.right == "Paris"
}
the query result:
Changes:
Commit 2.00 done by author at 16 Mar 2021, 22:04:09 :
* changes on Employee/bob :
- 'primaryAddress.city' changed: 'London' -> 'Paris'
- 'salary' changed: '1000' -> '1200'
Commit 1.00 done by author at 16 Mar 2021, 22:04:09 :
* new object: Employee/bob
- 'name' = 'bob'
- 'primaryAddress.city' = 'London'
- 'salary' = '1000'
Querying for Snapshots
Snapshot (see CdoSnapshot.java
)
is the historical state of a domain object captured as the property-value map.
Snapshots are raw data stored in the JaversRepository. When an object is committed, JaVers makes a Snapshot of its state and persists it. Under the hood, JaVers reuses Snapshots and creates a new one, only when a given object is changed (i.e., is changed compared to the last persisted Snapshot). It allows you to save a significant amount of repository space.
JaVers fetches snapshots in reversed chronological order. So if you set the limit to 10, you will get a list of the 10 latest Snapshots.
def "should query for Snapshots of an object"(){
given:
def javers = JaversBuilder.javers().build()
def bob = new Employee(name: "bob",
salary: 1000,
age: 29,
boss: new Employee("john"))
javers.commit("author", bob) // initial commit
bob.salary = 1200 // changes
bob.age = 30 //
javers.commit("author", bob) // second commit
when:
def snapshots = javers.findSnapshots( QueryBuilder.byInstance(bob).build() )
then:
assert snapshots.size() == 2
assert snapshots[0].commitMetadata.id.majorId == 2
assert snapshots[0].changed == ["salary", "age"]
assert snapshots[0].getPropertyValue("salary") == 1200
assert snapshots[0].getPropertyValue("age") == 30
// references are dehydrated
assert snapshots[0].getPropertyValue("boss").value() == "Employee/john"
assert snapshots[1].commitMetadata.id.majorId == 1
assert snapshots[1].getPropertyValue("salary") == 1000
assert snapshots[1].getPropertyValue("age") == 29
assert snapshots[1].getPropertyValue("boss").value() == "Employee/john"
}
Query types
JqlQueries are created by the following methods:
QueryBuilder.byInstanceId()
— query for Entity instance changes,QueryBuilder.byValueObjectId()
andQueryBuilder.byValueObject()
— query for ValueObject changes,QueryBuilder.byClass()
— query by objects’ class,QueryBuilder.anyDomainObject()
— query for any object changes.
All examples are in JqlExample.groovy
.
Querying for Entity changes by Instance Id
This query selects changes made on concrete Entity instance. The query accepts two mandatory parameters:
Object localId
— expected Instance Id,Class entityClass
— expected Entity class.
Here is the Groovy spec:
def "should query for Entity changes by instance Id"() {
given:
def javers = JaversBuilder.javers().build()
javers.commit("author", new Employee(name:"bob", age:30, salary:1000) )
javers.commit("author", new Employee(name:"bob", age:31, salary:1200) )
javers.commit("author", new Employee(name:"john",age:25) )
when:
Changes changes = javers.
findChanges( QueryBuilder.byInstanceId("bob", Employee.class).build() )
println changes.prettyPrint()
then:
assert changes.size() == 6
}
the query result:
Changes:
Commit 2.00 done by author at 16 Mar 2021, 22:04:10 :
* changes on Employee/bob :
- 'age' changed: '30' -> '31'
- 'salary' changed: '1000' -> '1200'
Commit 1.00 done by author at 16 Mar 2021, 22:04:10 :
* new object: Employee/bob
- 'age' = '30'
- 'name' = 'bob'
- 'salary' = '1000'
Querying for Value Object changes
This query selects changes made on a concrete Value Object (so a Value Object owned by a concrete Entity instance) or changes made on all Value Objects owned by any instance of a given Entity.
When querying for Value Objects, you should keep in mind that Value Objects, by definition, don’t have their own identifiers. We identify them using the owning Entity Instance Id and the property name. So in this case, the property name serves as a sort of path.
Let’s see how it works:
def "should query for ValueObject changes by owning Entity instance and class"() {
given:
def javers = JaversBuilder.javers().build()
javers.commit("author", new Employee(name:"bob", postalAddress: new Address(city:"Paris")))
javers.commit("author", new Employee(name:"bob", primaryAddress: new Address(city:"London")))
javers.commit("author", new Employee(name:"bob", primaryAddress: new Address(city:"Paris")))
javers.commit("author", new Employee(name:"lucy", primaryAddress: new Address(city:"New York")))
javers.commit("author", new Employee(name:"lucy", primaryAddress: new Address(city:"Washington")))
when:
println "query for ValueObject changes by owning Entity instance Id"
Changes changes = javers
.findChanges( QueryBuilder.byValueObjectId("bob",Employee.class,"primaryAddress").build())
println changes.prettyPrint()
then:
assert changes.size() == 2
when:
println "query for ValueObject changes by owning Entity class"
changes = javers
.findChanges( QueryBuilder.byValueObject(Employee.class,"primaryAddress").build())
println changes.prettyPrint()
then:
assert changes.size() == 4
}
the query result:
query for ValueObject changes by owning Entity instance Id
Changes:
Commit 3.00 done by author at 16 Mar 2021, 22:04:10 :
* changes on Employee/bob :
- 'primaryAddress.city' changed: 'London' -> 'Paris'
Commit 2.00 done by author at 16 Mar 2021, 22:04:10 :
* changes on Employee/bob :
- 'primaryAddress.city' = 'London'
query for ValueObject changes by owning Entity class
Changes:
Commit 5.00 done by author at 16 Mar 2021, 22:04:10 :
* changes on Employee/lucy :
- 'primaryAddress.city' changed: 'New York' -> 'Washington'
Commit 4.00 done by author at 16 Mar 2021, 22:04:10 :
* changes on Employee/lucy :
- 'primaryAddress.city' = 'New York'
Commit 3.00 done by author at 16 Mar 2021, 22:04:10 :
* changes on Employee/bob :
- 'primaryAddress.city' changed: 'London' -> 'Paris'
Commit 2.00 done by author at 16 Mar 2021, 22:04:10 :
* changes on Employee/bob :
- 'primaryAddress.city' = 'London'
Querying for any object changes by class
The only mandatory parameter of this query is a class. It selects objects regardless of theirs JaversType.
This query is useful for selecting Unbounded Value Objects (Value Objects without an owning Entity) and also for Value Objects when we don’t care about the owning Entity and path.
In the example, we show how to query for changes made on Value Objects owned by two different Entities.
def "should query for Object changes by its class"() {
given:
def javers = JaversBuilder.javers().build()
javers.commit("me", new DummyUserDetails(id:1, dummyAddress: new DummyAddress(city: "London")))
javers.commit("me", new DummyUserDetails(id:1, dummyAddress: new DummyAddress(city: "Paris")))
javers.commit("me", new SnapshotEntity(id:2, valueObjectRef: new DummyAddress(city: "Rome")))
javers.commit("me", new SnapshotEntity(id:2, valueObjectRef: new DummyAddress(city: "Palma")))
javers.commit("me", new SnapshotEntity(id:2, intProperty:2))
when:
Changes changes = javers.findChanges( QueryBuilder.byClass(DummyAddress.class).build() )
then:
println changes.prettyPrint()
assert changes.size() == 4
}
the query result:
Changes:
Commit 4.00 done by me at 16 Mar 2021, 22:04:10 :
* changes on org.javers.core.model.SnapshotEntity/2 :
- 'valueObjectRef.city' changed: 'Rome' -> 'Palma'
Commit 3.00 done by me at 16 Mar 2021, 22:04:10 :
* changes on org.javers.core.model.SnapshotEntity/2 :
- 'valueObjectRef.city' = 'Rome'
Commit 2.00 done by me at 16 Mar 2021, 22:04:10 :
* changes on org.javers.core.model.DummyUserDetails/1 :
- 'dummyAddress.city' changed: 'London' -> 'Paris'
Commit 1.00 done by me at 16 Mar 2021, 22:04:10 :
* changes on org.javers.core.model.DummyUserDetails/1 :
- 'dummyAddress.city' = 'London'
Querying for any domain object changes
This query is a kind of a shotgun approach. It accepts no parameters. It selects all objects regardless of theirs JaversType or class.
The query is useful for selecting any snapshots or changes that were created by a given author or have some other common properties set during commit.
In the example, we show how to query for changes made on any domain object.
def "should query for any domain object changes"() {
given:
def javers = JaversBuilder.javers().build()
javers.commit("author", new Employee(name:"bob", age:30) )
javers.commit("author", new Employee(name:"bob", age:31) )
javers.commit("author", new DummyUserDetails(id:1, someValue:"old") )
javers.commit("author", new DummyUserDetails(id:1, someValue:"new") )
when:
Changes changes = javers.findChanges( QueryBuilder.anyDomainObject().build() )
then:
println changes.prettyPrint()
assert changes.size() == 8
}
the query result:
Changes:
Commit 4.00 done by author at 16 Mar 2021, 22:04:10 :
* changes on org.javers.core.model.DummyUserDetails/1 :
- 'someValue' changed: 'old' -> 'new'
Commit 3.00 done by author at 16 Mar 2021, 22:04:10 :
* new object: org.javers.core.model.DummyUserDetails/1
- 'id' = '1'
- 'someValue' = 'old'
Commit 2.00 done by author at 16 Mar 2021, 22:04:10 :
* changes on Employee/bob :
- 'age' changed: '30' -> '31'
Commit 1.00 done by author at 16 Mar 2021, 22:04:10 :
* new object: Employee/bob
- 'age' = '30'
- 'name' = 'bob'
Query filters
For each query you can add one or more optional filters: changed property, limit, skip, author, commitProperty, commitDate, commitId, snapshot version, child ValueObjects and initial Changes.
All examples are in JqlExample.groovy
.
Changed property filter
Optional parameter for all queries. Use it to filter query results to changes made on a concrete property.
In the example, we show how to query for Employee’s salary changes, while ignoring changes made on other properties.
def "should query for changes (and snapshots) with property filter"() {
given:
def javers = JaversBuilder.javers().build()
javers.commit("me", new Employee(name:"bob", age:30, salary:1000) )
javers.commit("me", new Employee(name:"bob", age:31, salary:1100) )
javers.commit("me", new Employee(name:"bob", age:31, salary:1200) )
when:
def query = QueryBuilder.byInstanceId("bob", Employee.class)
.withChangedProperty("salary").build()
Changes changes = javers.findChanges(query)
then:
println changes.prettyPrint()
assert changes.size() == 3
assert javers.findSnapshots(query).size() == 3
}
the query result:
Changes:
Commit 3.00 done by me at 16 Mar 2021, 22:04:10 :
* changes on Employee/bob :
- 'salary' changed: '1100' -> '1200'
Commit 2.00 done by me at 16 Mar 2021, 22:04:10 :
* changes on Employee/bob :
- 'salary' changed: '1000' -> '1100'
Commit 1.00 done by me at 16 Mar 2021, 22:04:10 :
* changes on Employee/bob :
- 'salary' = '1000'
Limit filter
Use the limit parameter to set the maximum number of Snapshots or Shadows loaded from a JaversRepository. Choose a reasonable limit to improve performance of your queries. By default, the limit is set to 100. It’s optional for all queries.
There are four JQL find*()
methods, and the limit parameter affects all of them,
but in a different way:
-
Javers.findSnapshots()
— the limit works intuitively. It’s the maximum size of the returned list. It’s applied directly to the underlying database query. On SQL database, it limits the number of records loaded from thejv_snapshots
table. On MongoDB, it limits the number of documents loaded from thejv_snapshots
collection. -
Javers.findChanges()
— the limit is applied to the Snapshots query, which underlies the Changes query. The size of the returned list can be greater than limit, because, typically a difference between any two Snapshots consists of many atomic Changes. -
Javers.findShadows()
— the limit is applied to Shadows, it limits the size of the returned list. The underlying Snapshots query uses its own limit —QueryBuilder.snapshotQueryLimit()
. Since one Shadow might be reconstructed from many Snapshots, whensnapshotQueryLimit()
is hit, Javers repeats a given Shadow query to load a next frame of Shadows until required limit is reached. -
Javers.findShadowsAndStream()
— the limit works like infindShadows()
, it limits the size of the returned stream. The main difference is that the stream is lazy loaded and subsequent frame queries are executed gradually, during the stream consumption.
In the following example we set the limit parameter to 2, and we load Bob’s Snapshots and Changes. Only the last 2 Snapshots are loaded which means 4 Changes:
def "Snapshots limit in findChanges and findShadows"() {
given:
def javers = JaversBuilder.javers().build()
def bob = new Employee("Bob", 9_000, "ScrumMaster")
bob.age = 20
10.times {
bob.salary += 1_000
bob.age += 1
javers.commit("author", bob)
}
def query = QueryBuilder.byInstanceId("Bob", Employee).limit(2).build()
when: "findSnapshots - 2 latest snapshots are loaded and returned"
List<CdoSnapshot> snapshots = javers.findSnapshots(query)
snapshots.each {println(it)}
then:
snapshots.size() == 2
when: "findChanges - two latest snapshots are loaded, 4 changes are returned"
Changes changes = javers.findChanges(query)
println changes.prettyPrint()
then:
changes.size() == 4
}
output:
Snapshot{commit:10.00, id:Employee/Bob, version:10, state:{age:30, name:Bob, position:ScrumMaster, salary:19000, subordinates:[]}}
Snapshot{commit:9.00, id:Employee/Bob, version:9, state:{age:29, name:Bob, position:ScrumMaster, salary:18000, subordinates:[]}}
Changes:
Commit 10.00 done by author at 15 Mar 2021, 22:51:16 :
* changes on Employee/Bob :
- 'age' changed: '29' -> '30'
- 'salary' changed: '18000' -> '19000'
Commit 9.00 done by author at 15 Mar 2021, 22:51:16 :
* changes on Employee/Bob :
- 'age' changed: '28' -> '29'
- 'salary' changed: '17000' -> '18000'
Then, we can use the limit parameter to load the latest 2 Shadows of Bob. In this case, each Bob’s Shadow is reconstructed from 3 Snapshots, because Bob has 2 addresses which are Value Objects:
def "Shadows limit in findShadows and findShadowsAndStream"() {
given:
def javers = JaversBuilder.javers().build()
def bob = new Employee("Bob", 9_000, "ScrumMaster")
bob.primaryAddress = new Address("London")
bob.postalAddress = new Address("Paris")
3.times {
bob.salary += 1_000
bob.primaryAddress.city = "London $it"
bob.postalAddress.city = "Paris $it"
javers.commit("author", bob)
}
def query = QueryBuilder.byInstanceId("Bob", Employee).limit(2).build()
when : "findShadows() - 9 snapshots are loaded, 2 Shadows are returned"
List<Employee> shadows = javers.findShadows(query)
shadows.each {println(it)}
then:
shadows.size() == 2
println("query stats: " + query)
when : "findShadowsAndStream() - 9 snapshots are loaded, 2 Shadows are returned"
Stream<Shadow<Employee>> shadowsStream = javers.findShadowsAndStream(query)
then:
shadowsStream.count() == 2
}
output:
Shadow{it=Employee{ name: 'Bob', salary: '12000', primaryAddress: 'Address{ city: 'London 2' }' }, commitMetadata=CommitMetadata{ author: 'author', util: '11 Mar 2021, 16:36:58', id: '3.00' }}
Shadow{it=Employee{ name: 'Bob', salary: '11000', primaryAddress: 'Address{ city: 'London 1' }' }, commitMetadata=CommitMetadata{ author: 'author', util: '11 Mar 2021, 16:36:58', id: '2.00' }}
query stats: JqlQuery {
IdFilterDefinition{ globalId: 'org.javers.core.examples.model.Employee/Bob' }
QueryParams{ aggregate: 'true', limit: '2' }
shadowScope: SHALLOW
ShadowStreamStats{
executed in millis: '19'
DB queries: '1'
snapshots loaded: '9'
SHALLOW snapshots: '9'
Shadow stream frame queries: '1'
}
}
Skip filter
Use the skip parameter to define the offset of the first (most recent) Snapshot or Shadow that JaVers fetches from a repository. The default skip is 0. It’s optional for all queries.
You can use skip and limit parameters together to implement pagination.
In the following example we use skip to omit the most recent Snapshots and Shadows of Bob:
def "Skip parameter in findChanges, findSnapshots, and findShadows"() {
given:
def javers = JaversBuilder.javers().build()
javers.commit( "me", new Employee(name:"bob", age:20, salary: 2000) )
javers.commit( "me", new Employee(name:"bob", age:30, salary: 3000) )
javers.commit( "me", new Employee(name:"bob", age:40, salary: 4000) )
javers.commit( "me", new Employee(name:"bob", age:50, salary: 5000) )
def query = QueryBuilder.byInstanceId("bob", Employee.class).skip(2).build()
when: "findChanges()"
Changes changes = javers.findChanges( query )
then:
println changes.prettyPrint()
assert changes.size() == 6
when: "findSnapshots()"
List<CdoSnapshot> snapshots = javers.findSnapshots( query )
then:
snapshots.each {println it}
assert snapshots.size() == 2
assert snapshots[0].getPropertyValue("salary") == 3000
when: "findShadows()"
List<Shadow<Employee>> shadows = javers.findShadows( query )
then:
shadows.each {println it}
assert shadows.size() == 2
assert shadows[0].get().salary == 3000
}
output:
Changes:
Commit 2.00 done by me at 16 Mar 2021, 22:04:10 :
* changes on Employee/bob :
- 'age' changed: '20' -> '30'
- 'salary' changed: '2000' -> '3000'
Commit 1.00 done by me at 16 Mar 2021, 22:04:10 :
* new object: Employee/bob
- 'age' = '20'
- 'name' = 'bob'
- 'salary' = '2000'
Snapshot{commit:2.00, id:Employee/bob, version:2, state:{age:30, name:bob, salary:3000, subordinates:[]}}
Snapshot{commit:1.00, id:Employee/bob, version:1, state:{age:20, name:bob, salary:2000, subordinates:[]}}
Shadow{it=Employee{ name: 'bob', salary: '3000' }, commitMetadata=CommitMetadata{ author: 'me', util: '11 Mar 2021, 17:47:44', id: '2.00' }}
Shadow{it=Employee{ name: 'bob', salary: '2000' }, commitMetadata=CommitMetadata{ author: 'me', util: '11 Mar 2021, 17:47:44', id: '1.00' }}
Author filter
Author filter is an optional parameter for all queries. It allows you to find changes (or snapshots) persisted by a particular author.
In the example, objects are committed by turns by Jim and Pam. Then we retrieve only the changes committed by Pam.
def "should query for changes (and snapshots) with author filter"() {
given:
def javers = JaversBuilder.javers().build()
javers.commit( "Jim", new Employee(name:"bob", age:29, salary: 900) )
javers.commit( "Pam", new Employee(name:"bob", age:30, salary: 1000) )
javers.commit( "Jim", new Employee(name:"bob", age:31, salary: 1100) )
javers.commit( "Pam", new Employee(name:"bob", age:32, salary: 1200) )
when:
def query = QueryBuilder.byInstanceId("bob", Employee.class).byAuthor("Pam").build()
Changes changes = javers.findChanges( query )
then:
println changes.prettyPrint()
assert changes.size() == 4
assert javers.findSnapshots(query).size() == 2
}
the query result:
Changes:
Commit 4.00 done by Pam at 21 Mar 2021, 19:36:50 :
* changes on Employee/bob :
- 'age' changed: '31' -> '32'
- 'salary' changed: '1100' -> '1200'
Commit 2.00 done by Pam at 21 Mar 2021, 19:36:50 :
* changes on Employee/bob :
- 'age' changed: '29' -> '30'
- 'salary' changed: '900' -> '1000'
CommitProperty filter
Commit property filter is an optional parameter for all queries. It allows you to find changes (or snapshots) persisted with a given commit property. Single query can specify more than one commit property. In this case, each given commit property must match with a persisted one.
In the example, objects are committed with two properties: tenant
and event
.
Then we retrieve only the changes concerning promotions within the ACME tenant.
def "should query for changes (and snapshots) with commit property filters"() {
given:
def javers = JaversBuilder.javers().build()
def bob = new Employee(name: "bob", position: "Assistant", salary: 900)
javers.commit( "author", bob, ["tenant": "ACME", "event": "birthday"] )
bob.position = "Specialist"
bob.salary = 1600
javers.commit( "author", bob, ["tenant": "ACME", "event": "promotion"] )
def pam = new Employee(name: "pam", position: "Secretary", salary: 1300)
javers.commit( "author", pam, ["tenant": "Dunder Mifflin", "event": "hire"] )
bob.position = "Saleswoman"
bob.salary = 1700
javers.commit( "author", pam, ["tenant": "Dunder Mifflin", "event": "promotion"] )
when:
def query = QueryBuilder.anyDomainObject()
.withCommitProperty("tenant", "ACME")
.withCommitProperty("event", "promotion").build()
Changes changes = javers.findChanges( query )
then:
println changes.prettyPrint()
assert changes.size() == 2
assert javers.findSnapshots(query).size() == 1
}
the query result:
Changes:
Commit 2.00 done by author at 04 Apr 2021, 20:36:53 :
* changes on Employee/bob :
- 'position' changed: 'Assistant' -> 'Specialist'
- 'salary' changed: '900' -> '1600'
Note that when you are using JaVers’ auto-audit aspect
with Spring Data CrudRepositories
you can still provide commit properties by implementing
the CommitPropertiesProvider bean.
CommitDate filter
CommitDate filter is an optional parameter for all queries.
It allows time range filtering by commitDate
(Snapshot creation timestamp).
This example requires a trick to simulate time flow.
We use FakeDateProvider
, which is stubbed to provide concrete dates as now()
.
Bob is committed six times in one-year intervals.
Then we query for changes made over a three-years period.
def "should query for changes (and snapshots) with commitDate filter"(){
given:
def fakeDateProvider = new FakeDateProvider()
def javers = JaversBuilder.javers().withDateTimeProvider(fakeDateProvider).build()
(0..5).each{ i ->
def now = ZonedDateTime.of(2015+i,01,1,0,0,0,0, ZoneId.of("UTC"))
fakeDateProvider.set( now )
def bob = new Employee(name:"bob", age:20+i)
javers.commit("author", bob)
println "comitting bob on $now"
}
when:
def query = QueryBuilder.byInstanceId("bob", Employee.class)
.from(new LocalDate(2016,01,1))
.to (new LocalDate(2018,01,1)).build()
Changes changes = javers.findChanges( query )
then:
println changes.prettyPrint()
assert changes.size() == 3
assert javers.findSnapshots(query).size() == 3
}
the output:
committing bob on 2015-01-01
committing bob on 2016-01-01
committing bob on 2017-01-01
committing bob on 2018-01-01
committing bob on 2019-01-01
committing bob on 2020-01-01
Changes:
Commit 4.00 done by author at 01 Jan 2018, 00:00:00 :
* changes on Employee/bob :
- 'age' changed: '22' -> '23'
Commit 3.00 done by author at 01 Jan 2017, 00:00:00 :
* changes on Employee/bob :
- 'age' changed: '21' -> '22'
Commit 2.00 done by author at 01 Jan 2016, 00:00:00 :
* changes on Employee/bob :
- 'age' changed: '20' -> '21'
CommitId filter
CommitId filter is an optional parameter for all queries.
It lets you to find changes (or snapshots) persisted within a particular commit.
The commit id can be supplied as a CommitId
instance or BigDecimal
.
In the example we commit three subsequent versions of two Employees and then we retrieve the changes done in the third commit only. Note that CommitId is global in the JaversRepository context (as opposed to version).
def "should query for changes (and snapshots) with commitId filter"(){
given:
def javers = JaversBuilder.javers().build()
(1..3).each {
javers.commit("author", new Employee(name:"john", age:20+it))
javers.commit("author", new Employee(name:"bob", age:20+it))
}
when:
def query = QueryBuilder.byInstanceId("bob", Employee.class )
.withCommitId( CommitId.valueOf(4) ).build()
Changes changes = javers.findChanges(query)
then:
println changes.prettyPrint()
assert changes.size() == 1
assert changes[0].left == 21
assert changes[0].right == 22
assert javers.findSnapshots(query).size() == 1
}
the query result:
Changes:
Commit 4.00 done by author at 04 Apr 2021, 20:40:01 :
* changes on Employee/bob :
- 'age' changed: '21' -> '22'
Snapshot version filter
Version filter is similar to the CommitId filter, it lets you to find changes (or snapshots) for a concrete object version.
The Snapshot version is local for each object stored in the JaversRepository (as opposed to CommitId, which is the global identifier). When an object is committed for the first time, it has version 1. In the next commit it gets version 2 and so on.
In the example we commit five versions of two Employees: john
and bob
.
Then then we retrieve the fourth version of bob
.
def "should query for changes (and snapshots) with version filter"(){
given:
def javers = JaversBuilder.javers().build()
(1..5).each {
javers.commit("author", new Employee(name: "john",age: 20+it))
javers.commit("author", new Employee(name: "bob", age: 20+it))
}
when:
def query = QueryBuilder.byInstanceId("bob", Employee.class).withVersion(4).build()
Changes changes = javers.findChanges( query )
then:
println changes.prettyPrint()
assert changes.size() == 1
assert changes[0].left == 23
assert changes[0].right == 24
assert javers.findSnapshots(query).size() == 1
}
the query result:
Changes:
Commit 8.00 done by author at 04 Apr 2021, 20:40:28 :
* changes on Employee/bob :
- 'age' changed: '23' -> '24'
ChildValueObjects filter
When this filter is enabled, all child Value Objects owned by selected Entities are included in a query scope.
ChildValueObjects
filter can be used only for Entity queries:
byInstanceId()
and byClass()
.
In the example we are creating an employee (Entity)
with two addresses (child Value Objects).
Then we are changing employee’s age and one of his addresses.
Query with childValueObjects
filter is run and both age and address changes are selected.
Since there are no other employees in our repository, byInstanceId()
and byClass()
queries return the same result.
Let’s see how it works:
def "should query for changes made on Entity and its ValueObjects by InstanceId and Class"(){
given:
def javers = JaversBuilder.javers().build()
def bob = new Employee(name:"bob", age:30, salary: 1000,
primaryAddress: new Address(city:"Paris"),
postalAddress: new Address(city:"Paris"))
javers.commit("author", bob)
bob.age = 31
bob.primaryAddress.city = "London"
javers.commit("author", bob)
when: "query by instance Id"
def query = QueryBuilder.byInstanceId("bob", Employee.class).withChildValueObjects().build()
Changes changes = javers.findChanges( query )
then:
println changes.prettyPrint()
assert changes.size() == 8
when: "query by Entity class"
query = QueryBuilder.byClass(Employee.class).withChildValueObjects().build()
changes = javers.findChanges( query )
then:
assert changes.size() == 8
}
the query result:
Changes:
Commit 2.00 done by author at 02 wrz 2021, 15:19:45 :
* changes on Employee/bob :
- 'age' changed: '30' -> '31'
- 'primaryAddress.city' changed: 'Paris' -> 'London'
Commit 1.00 done by author at 02 wrz 2021, 15:19:45 :
* new object: Employee/bob
- 'age' = '30'
- 'name' = 'bob'
- 'postalAddress.city' = 'Paris'
- 'primaryAddress.city' = 'Paris'
- 'salary' = '1000'
Results are similar when the child Value Objects filter is applied to a Snapshot query. Snapshots of changed child Value Objects are returned together with the owning Entity snapshot.
Initial Changes switch
This switch affects queries for Changes and also javers.compare()
.
Since Javers 6.0, the Initial Changes switch
is controlled on the Javers instance level
by JaversBuilder.withInitialChanges()
and it is enabled by default.
When the switch is enabled, Javers generates additional set of Initial Changes for each property of a NewObject to capture its state. Internally, Javers generates Initial Changes by comparing a virtual, totally empty object with a real NewObject.
Let’s see how the Initial Changes switch works when on and off:
def "should query for changes with/without initialChanges"() {
when:
def javers = JaversBuilder.javers().build()
javers.commit( "author", new Employee(name:"bob", age:30, salary: 1000) )
javers.commit( "author", new Employee(name:"bob", age:30, salary: 1200) )
Changes changes = javers
.findChanges( QueryBuilder.byInstanceId("bob", Employee.class).build() )
then:
println "with initialChanges:"
println changes.prettyPrint()
assert changes.size() == 5
when:
javers = JaversBuilder.javers()
.withInitialChanges(false).build() // !
javers.commit( "author", new Employee(name:"bob", age:30, salary: 1000) )
javers.commit( "author", new Employee(name:"bob", age:30, salary: 1200) )
changes = javers
.findChanges( QueryBuilder.byInstanceId("bob", Employee.class).build() )
then:
println "without initialChanges:"
println changes.prettyPrint()
assert changes.size() == 2
}
the query results:
with initialChanges:
Changes:
Commit 2.00 done by author at 20 Mar 2021, 16:09:28 :
* changes on Employee/bob :
- 'salary' changed: '1000' -> '1200'
Commit 1.00 done by author at 20 Mar 2021, 16:09:28 :
* new object: Employee/bob
- 'age' = '30'
- 'name' = 'bob'
- 'salary' = '1000'
without initialChanges:
Changes:
Commit 2.00 done by author at 20 Mar 2021, 16:09:28 :
* changes on Employee/bob :
- 'salary' changed: '1000' -> '1200'
Commit 1.00 done by author at 20 Mar 2021, 16:09:28 :
* new object: Employee/bob
Refactoring Entities with @TypeName
Mature persistence frameworks allow you to refactor your domain classes
without losing a connection between old (possibly removed)
and new Class versions. For example,
JPA allows you to specify @Entity
name
and Spring Data uses @TypeAlias
annotation.
JaVers has
@TypeName
annotation. Use it to give stable names for your Entities.
Type name is used as a Class identifier instead of a fully-qualified Class name.
What’s important
We encourage you to use explicit Type names for all Entities — it will make your
life easier in case of refactoring.
When an Entity has a Type name, you can rename it or move it to another package safely. Without it, refactoring may break your queries.
Simple example
Let’s consider refactoring of the Person
Entity.
After persisting some commits in JaversRepository, we decide to change the Class name.
Moreover, the renamed Class
has some properties added/removed. The second commit is persisted,
using the new Class definition: PersonRefactored
.
Old class:
@TypeName("Person")
class Person {
@Id
int id
String name
Address address
}
New class:
@TypeName("Person")
class PersonRefactored {
@Id
int id
String name
String city
}
Since @TypeName
annotation was engaged from the very beginning,
our JQL just works. See the following Spock test:
def '''should allow Entity class name change
when both old and new class use @TypeName annotation'''()
{
given:
def javers = JaversBuilder.javers().build()
javers.commit('author', new Person(id:1, name:'Bob'))
when: '''Refactoring happens here, Person.class is removed,
new PersonRefactored.class appears'''
javers.commit('author', new PersonRefactored(id:1, name:'Uncle Bob', city:'London'))
def changes =
javers.findChanges( QueryBuilder.byInstanceId(1, PersonRefactored.class).build() )
println changes.prettyPrint()
then: 'three ValueChanges and one NewObject change is expected'
assert changes.size() == 4
changes.each { assert it.affectedGlobalId.value() == 'Person/1' }
}
Output:
Changes:
Commit 2.00 done by author at 20 Mar 2021, 15:57:13 :
* changes on Person/1 :
- 'city' = 'London'
- 'name' changed: 'Bob' -> 'Uncle Bob'
Commit 1.00 done by author at 20 Mar 2021, 15:57:13 :
* new object: Person/1
- 'name' = 'Bob'
As you can see, both Person(id:1)
and PersonRefactored(id:1)
objects share the same GlobalId — 'Person/1'
, so they match perfectly.
I forgot about @TypeName…
What if I forgot to use @TypeName, but my objects are already persisted
in a JaversRepository and I need to refactor now?
There are two possible solutions. The first is elegant but requires more work, the second is quick but somewhat dirty.
- Add @TypeName with a target name to a new class and update (manually) a database which underlies your JaversRepository.
- Add @TypeName to a new class and set typeName as a copy of an old class’ fully-qualified name.
Let’s see how the second approach works:
Old class:
class PersonSimple {
@Id
int id
String name
}
New class:
@TypeName("org.javers.core.examples.PersonSimple")
class PersonRetrofitted {
@Id
int id
String name
}
And the Spock test:
def '''should allow Entity class name change
when old class forgot to use @TypeName annotation'''()
{
given:
def javers = JaversBuilder.javers().build()
javers.commit('author', new PersonSimple(id:1, name:'Bob'))
when:
javers.commit('author', new PersonRetrofitted(id:1, name:'Uncle Bob'))
def changes =
javers.findChanges( QueryBuilder.byInstanceId(1,PersonRetrofitted.class).build() )
println changes.prettyPrint()
then: 'two ValueChange and one NewObject change is expected'
assert changes.size() == 3
with(changes[0]){
assert left == 'Bob'
assert right == 'Uncle Bob'
assert affectedGlobalId.value() == 'org.javers.core.examples.PersonSimple/1'
}
}
Output:
Changes:
Commit 2.00 done by author at 20 Mar 2021, 15:57:56 :
* changes on org.javers.core.examples.PersonSimple/1 :
- 'name' changed: 'Bob' -> 'Uncle Bob'
Commit 1.00 done by author at 20 Mar 2021, 15:57:56 :
* new object: org.javers.core.examples.PersonSimple/1
- 'name' = 'Bob'
In this case, both PersonSimple(id:1)
and PersonRetrofitted(id:1)
objects share the same GlobalId
— 'org.javers.core.examples.PersonSimple/1'
.
They match but, well, it’s not very nice to have deprecated names in new code.
Free ValueObjects refactoring
In most cases you don’t have to use @TypeName for ValueObjects. Most JQL queries will just work after refactoring. However, we still recommend to adding @TypeName. For example, querying by ValueObject class relies on it.
JaVers treats ValueObjects as property containers and doesn’t care much about their classes. This approach is known as Duck Typing, and is widely adopted by dynamic languages like Groovy.
Example
Let’s consider the refactoring of Person’s address,
which happened to be a ValueObject.
We want to change its type from EmailAddress
to HomeAddress
,
For the sake of brevity, we use the abstract Address
class
in the Person definition (owner Entity), so we don’t need to change it after the type of Address is altered.
abstract class Address {
boolean verified
Address(boolean verified) {
this.verified = verified
}
}
class EmailAddress extends Address {
String email
EmailAddress(String email, boolean verified) {
super(verified)
this.email = email
}
}
class HomeAddress extends Address {
String city
String street
HomeAddress(String city, String street, boolean verified) {
super(verified)
this.city = city
this.street = street
}
}
The Person class is the same like in the Refactoring Entities example.
The first version of Person is persisted with EmailAddress
and then
another two versions are persisted with HomeAddress
as the type:
def 'should be very relaxed about ValueObject types'(){
given:
def javers = JaversBuilder.javers().build()
javers.commit('author', new Person(id:1, address:new EmailAddress('[email protected]', false)))
javers.commit('author', new Person(id:1, address:new HomeAddress ('London','Green 50', true)))
javers.commit('author', new Person(id:1, address:new HomeAddress ('London','Green 55', true)))
when:
def changes =
javers.findChanges( QueryBuilder.byValueObjectId(1, Person.class, 'address').build() )
println changes.prettyPrint()
then: 'six ValueChanges are expected'
assert changes.size() == 6
assert changes.collect{ it.propertyName } as Set ==
['street','verified','city','email'] as Set
}
Output:
Changes:
Commit 3.00 done by author at 20 Mar 2021, 15:58:48 :
* changes on Person/1 :
- 'address.street' changed: 'Green 50' -> 'Green 55'
Commit 2.00 done by author at 20 Mar 2021, 15:58:48 :
* changes on Person/1 :
- 'address.city' = 'London'
- 'address.email' value '[email protected]' unset
- 'address.street' = 'Green 50'
- 'address.verified' changed: 'false' -> 'true'
Commit 1.00 done by author at 20 Mar 2021, 15:58:48 :
* changes on Person/1 :
- 'address.email' = '[email protected]'
As you can see, all three versions of the ValueObject address share the same GlobalId
— 'Person/1#address'
. Properties are matched by name, and their values are compared
without paying much attention to the actual Address class.