org.javers.core.Javers Maven / Gradle / Ivy
package org.javers.core;
import org.javers.core.changelog.ChangeProcessor;
import org.javers.core.commit.Commit;
import org.javers.core.commit.CommitMetadata;
import org.javers.core.diff.Change;
import org.javers.core.diff.Diff;
import org.javers.core.diff.changetype.PropertyChange;
import org.javers.core.json.JsonConverter;
import org.javers.core.metamodel.object.CdoSnapshot;
import org.javers.core.metamodel.object.GlobalId;
import org.javers.core.metamodel.property.Property;
import org.javers.core.metamodel.type.EntityType;
import org.javers.core.metamodel.type.JaversType;
import org.javers.core.metamodel.type.ValueObjectType;
import org.javers.repository.jql.*;
import org.javers.shadow.Shadow;
import java.lang.reflect.Type;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.stream.Stream;
/**
* A JaVers instance.
* Should be constructed by {@link JaversBuilder} provided with your domain model configuration.
*
*
* For example, to deeply compare two objects
* or two arbitrary complex object graphs, call:
*
* Javers javers = JaversBuilder.javers().build();
* Diff diff = javers.compare(oldVersion, currentVersion);
*
*
* @see http://javers.org/documentation
* @author bartosz walacik
*/
public interface Javers {
/**
* Persists a current state of a given domain object graph
* in JaVers repository.
*
*
* JaVers applies commit() to given object and all objects navigable from it.
* You can capture a state of an arbitrary complex object graph with a single commit() call.
*
* @see http://javers.org/documentation/repository-examples
* @param author current user
* @param currentVersion standalone object or handle to an object graph
*/
Commit commit(String author, Object currentVersion);
/**
* Variant of {@link #commit(String, Object)} with commitProperties.
*
* You can pass arbitrary commit properties and
* use them in JQL to search for snapshots or changes.
*
* @see QueryBuilder#withCommitProperty(String, String)
* @param commitProperties for example ["channel":"web", "locale":"pl-PL"]
*/
Commit commit(String author, Object currentVersion, Map commitProperties);
/**
* Async version of {@link #commit(String, Object)}
*
*
* Important! Async Javers commits work seamlessly with MongoDB.
* If you are using SQL repository — take extra care about transaction management.
*
* @param executor ExecutorService to be used to process commit() asynchronously.
*/
CompletableFuture commitAsync(String author, Object currentVersion, Executor executor);
/**
* Async version of {@link #commit(String, Object, Map)}
*
*
* @param executor ExecutorService to be used to process commit() asynchronously
*/
CompletableFuture commitAsync(String author, Object currentVersion, Map commitProperties,
Executor executor);
/**
* Marks given object as deleted.
*
*
* Unlike {@link Javers#commit(String, Object)}, this method is shallow
* and affects only given object.
*
*
* This method doesn't delete anything from JaVers repository.
* It just persists 'terminal snapshot' of a given object.
*
* @param deleted object to be marked as deleted (Entity or Value Object)
*/
Commit commitShallowDelete(String author, Object deleted);
/**
* Variant of {@link #commitShallowDelete(String, Object)} with commitProperties.
*
*
* See {@link #commit(String, Object, Map)} for commitProperties description.
*/
Commit commitShallowDelete(String author, Object deleted, Map commitProperties);
/**
* The same like {@link #commitShallowDelete(String,Object)}
* but deleted object is selected using globalId
*/
Commit commitShallowDeleteById(String author, GlobalIdDTO globalId);
/**
* Variant of {@link #commitShallowDeleteById(String, GlobalIdDTO)} with commitProperties.
*
*
* See {@link #commit(String, Object, Map)} for commitProperties description.
*/
Commit commitShallowDeleteById(String author, GlobalIdDTO globalId, Map commitProperties);
/**
* Deep compare
*
* JaVers core function,
* deeply compares two arbitrary complex object graphs.
*
*
* To calculate a diff, just provide two versions of the
* same domain object.
*
* The domain object can be a root of an Aggregate, tree root
* or any node in a domain object graph from where all other nodes are navigable.
*
*
* Both oldVersion
and currentVersion
* should be mapped to {@link EntityType} or {@link ValueObjectType},
* see
* Domain model mapping.
*
* Flat collection compare
* You can also pass object collections here (List, Sets or Maps),
* but in this case, JaVers calculates flat collection diff only.
* Because it's impossible to determine type of raw collection items, JaVers maps them as Values
* and compares using {@link Object#equals(Object)}.
* So if you need to deep compare, wrap collections in some Value Objects.
*
* Misc
* compare()
function is used for ad-hoc objects comparing.
* In order to use data auditing feature, call {@link #commit(String, Object)}.
*
*
* Diffs can be converted to JSON with {@link JsonConverter#toJson(Object)} or pretty-printed with {@link Diff#prettyPrint()}
*
*
* @param oldVersion Old version of a domain object, an instance of {@link EntityType} or
* {@link ValueObjectType}
* , nullable
* @param currentVersion Current version of a domain object, nullable
*
* @see
* http://javers.org/documentation/diff-examples
*/
Diff compare(Object oldVersion, Object currentVersion);
/**
* Deeply compares two top-level collections.
*
*
* Introduced due to the lack of possibility to statically
* determine type of collection items when two top-level collections are passed as references to
* {@link #compare(Object, Object)}.
*
*
* Usage example:
*
* List<Person> oldList = ...
* List<Person> newList = ...
* Diff diff = javers.compareCollections(oldList, newList, Person.class);
*
*
* @see
* Compare top-level collections example
*/
Diff compareCollections(Collection oldVersion, Collection currentVersion, Class itemClass);
/**
* Initial diff is a kind of snapshot of a given object graph.
*
* @deprecated Use {@link Javers#compare(Object, Object)} passing null as the first parameter.
*/
@Deprecated
Diff initial(Object newDomainObject);
/**
* Queries a JaversRepository for {@link Shadow}s.
* Shadows are historical version of domain objects
* which are restored from persisted snapshots.
*
*
* For example, to get latest 5 Shadows of "bob" Person, call:
*
*
*
* List<Shadow> shadows = javers.findShadows(
* QueryBuilder.byInstanceId("bob", Person.class).limit(5).build() );
*
*
* Since Shadows are instances of your domain classes,
* you can use them directly in your application:
*
*
*
* assert shadows.get(0).get() instanceof Person.class;
*
*
* Paging & limit
* Use {@link QueryBuilder#skip(int)} and {@link QueryBuilder#limit(int)} for paging Shadows.
*
* An underlying Snapshots query uses its own limit — {@link QueryBuilder#snapshotQueryLimit(Integer)}.
* Since one Shadow might be reconstructed from many Snapshots, when snapshotQueryLimit()
is hit,
* Javers repeats a given Shadow query to load a next frame of Shadows until required limit is reached.
*
*
* Returned list of Shadow graphs is always complete (according to the selected {@link ShadowScope})
* but the whole operation can trigger a few DB queries.
*
*
* Query scopes
* By default, Shadow queries are run in the Shallow scope, which is the fastest one
* .
* To eagerly load all referenced objects use one of the wider scopes, Commit-deep or Deep+ :
*
*
* {@link QueryBuilder#withScopeCommitDeep()}
* {@link QueryBuilder#withScopeDeepPlus(int)}
*
*
* We recommend the Deep+ scope as a good start (see {@link ShadowScope}).
*
*
* Query scopes example
*
* To understand Shadow query scopes, you need to understand how JaVers commit works.
* Remember that JaVers reuses existing snapshots and creates a fresh one
* only if a given object is changed.
* The way how objects are committed affects shadow query results.
*
*
* For example, let's say we have four Entities in the object graph, joined by references:
*
*
* // E1 -> E2 -> E3 -> E4
* def e4 = new Entity(id:4)
* def e3 = new Entity(id:3, ref:e4)
* def e2 = new Entity(id:2, ref:e3)
* def e1 = new Entity(id:1, ref:e2)
*
*
* In the first scenario, our four entities are committed in three commits:
*
* In Shallow scope, referenced entities are not loaded.
* But they all can be loaded using Deep+3 scope.
*
*
*
*given:
* javers.commit("author", e4) // commit 1.0 with e4 snapshot
* javers.commit("author", e3) // commit 2.0 with e3 snapshot
* javers.commit("author", e1) // commit 3.0 with snapshots of e1 and e2
*
*when: 'shallow scope query'
* def shadows = javers.findShadows(QueryBuilder.byInstanceId(1, Entity)
* .build())
* def shadowE1 = shadows.get(0).get()
*
*then: 'only e1 is loaded'
* shadowE1 instanceof Entity
* shadowE1.id == 1
* shadowE1.ref == null
*
*when: 'commit-deep scope query'
* shadows = javers.findShadows(QueryBuilder.byInstanceId(1, Entity)
* .withScopeCommitDeep().build())
* shadowE1 = shadows.get(0).get()
*
*then: 'only e1 and e2 are loaded, both was committed in commit 3.0'
* shadowE1.id == 1
* shadowE1.ref.id == 2
* shadowE1.ref.ref == null
*
*when: 'deep+1 scope query'
* shadows = javers.findShadows(QueryBuilder.byInstanceId(1, Entity)
* .withScopeDeepPlus(1).build())
* shadowE1 = shadows.get(0).get()
*
*then: 'only e1 + e2 are loaded'
* shadowE1.id == 1
* shadowE1.ref.id == 2
* shadowE1.ref.ref == null
*
*when: 'deep+3 scope query'
* shadows = javers.findShadows(QueryBuilder.byInstanceId(1, Entity)
* .withScopeDeepPlus(3).build())
* shadowE1 = shadows.get(0).get()
*
*then: 'all object are loaded'
* shadowE1.id == 1
* shadowE1.ref.id == 2
* shadowE1.ref.ref.id == 3
* shadowE1.ref.ref.ref.id == 4
*
*
* In the second scenario, our four entities are committed in the single commit:
*
* Shallow scope works in the same way as in the first example,
* but commit-deep scope is enough to load the full graph.
*
*
*
*given:
* javers.commit("author", e1) //commit 1.0 with snapshots of e1, e2, e3 and e4
*
*when: 'commit-deep scope query'
* shadows = javers.findShadows(QueryBuilder.byInstanceId(1, Entity)
* .withScopeCommitDeep().build())
* shadowE1 = shadows.get(0).get()
*
*then: 'all object are loaded'
* shadowE1.id == 1
* shadowE1.ref.id == 2
* shadowE1.ref.ref.id == 3
* shadowE1.ref.ref.ref.id == 4
*
*
* Performance & Profiling
*
* Each Shadow query executes one or more DB queries (Snapshot queries).
* The number of executed DB queries depends on: selected {@link ShadowScope},
* complexity of your object graphs, and obviously on {@link Stream#limit(long)}.
*
*
* If you are having performance issues, start from checking execution statistics of your query,
* available in {@link JqlQuery#streamStats()}.
* Also, the stats are printed in {@link JqlQuery#toString()}, for example:
*
*
*
* def query = QueryBuilder.byInstanceId(1, Entity).withScopeDeepPlus(1).build()
* def shadows = javers.findShadows(query)
* println 'executed query: ' + query
*
*
* Output:
*
* executed query: JqlQuery {
* IdFilterDefinition{ globalId: 'org.javers.core.examples.JqlExample$Entity/1' }
* QueryParams{ aggregate: 'true', limit: '100' }
* shadowScope: DEEP_PLUS
* ShadowStreamStats{
* executed in millis: '7'
* DB queries: '2'
* snapshots loaded: '2'
* SHALLOW snapshots: '1'
* DEEP_PLUS snapshots: '1'
* gaps filled: '1'
* gaps left!: '1'
* Shadow stream frame queries: '1'
* }
* }
*
*
* For quick win — try to reduce the {@link ShadowScope}.
*
*
* More detailed stats can be obtained by setting the
* org.javers.JQL
logger to DEBUG
:
*
*
*<logger name="org.javers.JQL" level="DEBUG"/>
*
*
* @return Returns a list of latest Shadows ordered in reverse chronological order
* The size of the list is limited by {@link QueryBuilder#limit(int)}.
* @param type of a domain object
* @see ShadowScope
* @see JaversCore#findShadows(JqlQuery)
*/
List> findShadows(JqlQuery query);
/**
* The streamed version of {@link #findShadows(JqlQuery)}.
*
*
* The main difference is that the returned stream is lazy loaded and subsequent frame queries
* are executed gradually, during the stream consumption.
*
*
* @return A lazy loaded stream of latest Shadows ordered in reverse chronological order.
* Terminated stream if nothing found. The size of the stream is limited by
* {@link QueryBuilder#limit(int)}.
* @param type of a domain object
* @see #findShadows(JqlQuery)
*/
Stream> findShadowsAndStream(JqlQuery query);
/**
* Queries a JaversRepository for change history (diff sequence) of a given class, object or property.
* Returns the list of Changes.
* There are various types of changes. See {@link Change} class hierarchy.
* {@link Changes} can be easily traversed using {@link Changes#groupByCommit()} and {@link Changes#groupByObject()}.
*
*
* Querying for Entity changes by instance Id
*
* For example, to get change history of last 5 versions of "bob" Person, call:
*
* javers.findChanges( QueryBuilder.byInstanceId("bob", Person.class).limit(5).build() );
*
*
* Last "salary" changes of "bob" Person:
*
* javers.findChanges( QueryBuilder.byInstanceId("bob", Person.class).withChangedProperty("salary").build() );
*
*
* Querying for ValueObject changes
*
* Last changes on Address ValueObject owned by "bob" Person:
*
* javers.findChanges( QueryBuilder.byValueObjectId("bob", Person.class, "address").build() );
*
*
* Last changes on Address ValueObject owned by any Person:
*
* javers.findChanges( QueryBuilder.byValueObject(Person.class, "address").build() );
*
*
* Last changes on nested ValueObject
* (when Address is a ValueObject nested in PersonDetails ValueObject):
*
* javers.findChanges( QueryBuilder.byValueObject(Person.class, "personDetails/address").build() );
*
*
* Querying for any object changes by its class
*
* Last changes on any object of MyClass.class:
*
* javers.findChanges( QueryBuilder.byClass(MyClass.class).build() );
*
*
* Last "myProperty" changes on any object of MyClass.class:
*
* javers.findChanges( QueryBuilder.byClass(Person.class).withChangedProperty("myProperty").build() );
*
*
* @return A list of Changes ordered in reverse chronological order.
* Empty if nothing found.
* @see http://javers.org/documentation/jql-examples
*/
Changes findChanges(JqlQuery query);
/**
* Queries JaversRepository for object Snapshots.
* Snapshot is a historical state of a domain object captured as the property->value Map.
*
*
* For example, to get latest Snapshots of "bob" Person, call:
*
* javers.findSnapshots( QueryBuilder.byInstanceId("bob", Person.class).limit(5).build() );
*
*
* For more query examples, see {@link #findChanges(JqlQuery)} method.
*
* Use the same JqlQuery to get changes, snapshots and shadows views.
*
* @return A list ordered in reverse chronological order. Empty if nothing found.
* @see http://javers.org/documentation/jql-examples
*/
List findSnapshots(JqlQuery query);
/**
* Latest snapshot of a given Entity instance.
*
*
* For example, to get the current state of Bob
, call:
*
* javers.getLatestSnapshot("bob", Person.class);
*
*
* Returns Optional#EMPTY if an instance is not versioned.
*/
Optional getLatestSnapshot(Object localId, Class entity);
/**
* Historical snapshot of a given Entity instance.
*
*
* For example, to get the historical state of Bob
at 2017-01-01, call:
*
* javers.getHistoricalSnapshot("bob", Person.class, LocalDateTime.of(2017,01,01));
*
*
* Returns Optional#EMPTY if an instance is not versioned.
*
* @since 3.4
*/
Optional getHistoricalSnapshot(Object localId, Class entity, LocalDateTime effectiveDate);
/**
* If you are serializing JaVers objects like
* {@link Commit}, {@link Change}, {@link Diff} or {@link CdoSnapshot} to JSON, use this JsonConverter.
*
*
* For example:
*
* javers.getJsonConverter().toJson(changes);
*
*/
JsonConverter getJsonConverter();
/**
* Generic purpose method for processing a changes list.
* After iterating over given list, returns data computed by
* {@link org.javers.core.changelog.ChangeProcessor#result()}.
*
* It's more convenient than iterating over changes on your own.
* ChangeProcessor frees you from if + inctanceof boilerplate.
*
*
* Additional features:
* - when several changes in a row refers to the same Commit, {@link ChangeProcessor#onCommit(CommitMetadata)}
* is called only for first occurrence
* - similarly, when several changes in a row affects the same object, {@link ChangeProcessor#onAffectedObject(GlobalId)}
* is called only for first occurrence
*
*
* For example, to get pretty change log, call:
*
* List<Change> changes = javers.calculateDiffs(...);
* String changeLog = javers.processChangeList(changes, new SimpleTextChangeLog());
* System.out.println( changeLog );
*
*
* @see org.javers.core.changelog.SimpleTextChangeLog
*/
T processChangeList(List changes, ChangeProcessor changeProcessor);
/**
* Use JaversTypes, if you want to:
* - describe your class in the context of JaVers domain model mapping,
* - use JaVers Reflection API to conveniently access your object properties
* (instead of awkward java.lang.reflect API).
*
*
*
* Class describe example.
* You can pretty-print JaversType of your class and
* check if mapping is correct.
*
* class Person {
* @Id int id;
* @Transient String notImportantField;
* String name;
* }
*
*
* Calling
*
* System.out.println( javers.getTypeMapping(Person.class).prettyPrint() );
*
*
* prints:
*
* EntityType{
* baseType: org.javers.core.examples.Person
* managedProperties:
* Field int id; //declared in: Person
* Field String name; //declared in: Person
* idProperty: login
* }
*
*
* Property access example.
* You can list object property values using {@link Property} abstraction.
*
* Javers javers = JaversBuilder.javers().build();
* ManagedType jType = javers.getTypeMapping(Person.class);
* Person person = new Person("bob", "Uncle Bob");
*
* System.out.println("Bob's properties:");
* for (Property p : jType.getPropertyNames()){
* Object value = p.get(person);
* System.out.println( "property:" + p.getName() + ", value:" + value );
* }
*
*
* prints:
*
* Bob's properties:
* property:login, value:bob
* property:name, value:Uncle Bob
*
*/
T getTypeMapping(Type userType);
/**
* Returns {@link Property} which underlies given {@link PropertyChange}
*
* @since 1.4.1
*/
Property getProperty(PropertyChange propertyChange);
CoreConfiguration getCoreConfiguration();
}