JaVers needs funding to sustain. You can sponsor JaVers development via GitHub Sponsors or Open Collective.

JaVers Repository Configuration

If you’re going to use JaVers as a data audit framework you should configure JaversRepository.

The purpose of JaversRepository is to store JaVers commits in your database, alongside your domain data. JSON format is used for serializing your data. This approach significantly simplifies the construction of JaversRepository. The hardest work — mapping domain objects to persistent format (JSON) — is done by javers-core. This common JSON format is used by many JaversRepository implementations.

In runtime, JaVers commit holds a list of domain object snapshots and a list of changes (a diff). Only snapshots are persisted in a database. When JaVers commit is being read from a database, snapshots are deserialized from JSON and the diff is re-calculated by comparing snapshot pairs.

By default, JaVers comes with in-memory repository implementation. It’s perfect for testing, but for production environment you need something real.

Choose JaversRepository

First, choose proper JaversRepository implementation. Currently, JaVers supports the following databases: MongoDB, H2, PostgreSQL, MySQL, MariaDB, Oracle, Microsoft SQL Server and Redis.

In JaVers 5.2.4, we have added experimental support for Amazon DocumentDB which claims to be almost fully compatible with MongoDB.

Hint. If you are using Spring Boot, just add one of our Spring Boot starters for Spring Data and let them automatically configure and boot a JaVers instance with proper JaversRepository implementation.

MongoDB

Dependency
If you’re using MongoDB, choose MongoRepository.

Add javers-persistence-mongo module to your classpath:

compile 'org.javers:javers-persistence-mongo:7.8.0'

Check Maven Central for other build tools snippets.

Usage
The idea of configuring MongoRepository is simple, just provide a working Mongo client.

import org.javers.repository.mongo.MongoRepository;
import com.mongodb.MongoClient;
import com.mongodb.client.MongoDatabase;

...

//by default, use the same database connection
//which you are using for your primary database
MongoDatabase mongoDb = new MongoClient( "localhost" ).getDatabase("test");

MongoRepository mongoRepository = new MongoRepository(mongoDb);
Javers javers = JaversBuilder.javers().registerJaversRepository(mongoRepository).build();

Here’s the Spring Config example for MongoRepository.

Schema
JaVers creates two collections in MongoDB:

  • jv_head_id — one document with the last CommitId,
  • jv_snapshots — domain object snapshots. Each document contains snapshot data and commit metadata.

Amazon DocumentDB

Configuration is the same as for MongoDB, but you should use this factory method to create a repository instance:

MongoRepository documentDBrepository =
        MongoRepository.mongoRepositoryWithDocumentDBCompatibility(mongoDb);

SQL databases

Dependency
Add javers-persistence-sql module to your classpath:

compile 'org.javers:javers-persistence-sql:7.8.0'

Check Maven Central for other build tools snippets.

Overview

JaVers uses it’s own, lightweight abstraction layer over various SQL dialects.

The following SQL database types are supported: H2, PostgreSQL, MySQL/MariaDB, Oracle and Microsoft SQL Server.

For testing, you can setup JaversSqlRepository as follows:

import org.javers.repository.sql.JaversSqlRepository;
import java.sql.Connection;
import java.sql.DriverManager;
... //

final Connection dbConnection = DriverManager.getConnection("jdbc:h2:mem:test");

ConnectionProvider connectionProvider = new ConnectionProvider() {
    @Override
    public Connection getConnection() {
        //suitable only for testing!
        return dbConnection;
    }
};

JaversSqlRepository sqlRepository = SqlRepositoryBuilder
        .sqlRepository()
        .withSchema("my_schema") //optionally, provide the schame name
        .withConnectionProvider(connectionProvider)
        .withDialect(DialectName.H2).build();
Javers javers = JaversBuilder.javers().registerJaversRepository(sqlRepository).build();

To setup JaversSqlRepository you need to provide three things: an SQL dialect name, a ConnectionProvider implementation and a JDBC driver on your classpath.

In the following table, there is a summary of all supported SQL databases with corresponding dialect names.

You should provide a proper JDBC driver version on your classpath, which works bests for you (these versions are only a suggestion, we use them in JaVers integration tests) . Probably it would be the same version which you already use for your application’s database.

Open source databases

Database name DialectName JDBC driver
PostgreSQL POSTGRES org.postgresql:postgresql:42.2.5
MariaDB MYSQL org.mariadb.jdbc:mariadb-java-client:2.2.3
H2 H2 com.h2database:h2:1.4.187
Oracle ORACLE commercial
MySQL MYSQL mysql:mysql-connector-java:8.0.15
Microsoft SQL Server MSSQL commercial

ConnectionProvider

ConnectionProvider serves as the source of live JDBC connections for your JaversSQLRepository. JaversSqlRepository works in passive mode, which means:

  • JaVers doesn’t create JDBC connections on its own and uses connections provided by an application (via ConnectionProvider.getConnection()).
  • JaVers philosophy is to use application’s transactions and never to call SQL commit or rollback commands on its own.

Thanks to this approach, data managed by an application (domain objects) and data managed by JaVers (object snapshots) can be saved to SQL database in one transaction.

If you’re using a transaction manager, implement a ConnectionProvider to integrate with it. For Spring users, we have out-of-the-box implementation: JpaHibernateConnectionProvider from javers-spring module. Choose this, if you’re using Spring/JPA/Hibernate stack (see JPA EntityManager integration).

If you’re not using any kind of transaction manager, implement a ConnectionProvider to return the current connection (thread-safely).

Schema

JaVers creates four tables in SQL database:

  • jv_global_id — domain object identifiers,
  • jv_commit — JaVers commits metadata,
  • jv_commit_property — commit properties,
  • jv_snapshot — domain object snapshots.

JaVers has a basic schema-create implementation. If a table is missing, JaVers simply creates it, together with a sequence and indexes. There’s no schema-update, so if you drop a column, index or sequence, it wouldn’t be recreated automatically.

Redis

Redis persistence overview

When storing JaVers snapshots in Redis, selecting the right persistence model is essential for ensuring data durability and performance. Redis offers two main persistence models:

  • RDB (Redis Database) - Periodically saves the dataset to disk. This is less resource-intensive because it doesn’t constantly write to disk but instead saves the state at certain intervals.
  • AOF (Append Only File) - Logs every write operation to disk as it happens. In case of a crash, Redis can replay the log to restore the data.

Redis allows using both RDB and AOF simultaneously. This provides the advantage of faster recovery through RDB snapshots while maintaining high durability with AOF. Depending on the system’s criticality, you may want to consider enabling both for your JaVers snapshot storage.

For more information, check the official Redis documentation.

Dependency
Add javers-persistence-redis module to your classpath:

Maven

<dependency>
    <groupId>org.javers</groupId>
    <artifactId>javers-persistence-redis</artifactId>
    <version>7.8.0</version>
</dependency>

Gradle (short)

implementation 'org.javers:javers-persistence-redis:7.8.0'

Check Maven Central for other build tools snippets.

Overview

The idea of configuring JaversRedisRepository is simple, just provide a working Jedis (Java client for Redis). You can setup JaversRedisRepository as follows:

import org.javers.repository.redis.JaversRedisRepository;
import redis.clients.jedis.JedisPool;
...

private static final String REDIS_HOST = "localhost";
private static final int REDIS_PORT = 6379;
private static final Duration REDIS_KEY_EXPIRATION_TIME = Duration.ofSeconds(3600);


final var jedisPool = new JedisPool();
final var javersRedisRepository = new JaversRedisRepository(jedisPool, REDIS_KEY_EXPIRATION_TIME);
final var javers = JaversBuilder.javers().registerJaversRepository(javersRedisRepository).build();

Schema

JaVers creates several key-value pairs in Redis, with main keys being:

  • jv_head_id — A String that holds the value of the last CommitId.
  • jv_snapshots_keys — A Set that contains keys, where each key is a reference to another key of type List. These lists contain snapshots for specific entities and their properties.
  • jv_snapshots_keys:<Entity Name> — Domain-specific sets pointing to lists containing snapshots for specific objects of that entity type.

Handling Redis Key Expiration

In Redis, when a key expires, it is automatically removed from the database. However, additional cleanup may be required if the expired key is referenced in other structures, such as sets.

CdoSnapshotKeyExpireListener

The CdoSnapshotKeyExpireListener is an event listener designed to handle key expiration events in Redis. Its primary responsibility is to ensure that expired key entries are removed from all relevant sets.

When an instance of JaversRedisRepository is created, it subscribes to keyspace events with the pattern __key*__:jv_snapshots:*. This subscription allows the CdoSnapshotKeyExpireListener to process key expiration events as they occur.

Upon receiving a key expiration event, the CdoSnapshotKeyExpireListener identifies the expired key and removes its entries from all relevant sets, such as jv_snapshots_keys and jv_snapshots_keys:<Entity Name>.

Expired Keys Cleanup

A significant challenge arises if Redis keys expire when there is no active CdoSnapshotKeyExpireListener running. In this scenario, expired key entries will remain in the sets, leading to potential data inconsistencies.

To address this challenge, we have introduced the public method cleanExpiredSnapshotsKeysSets in JaversRedisRepository that can be called to perform the necessary cleanup. This ensures that expired keys are properly removed from all relevant sets, even if the listener is not active at the time of expiration.

Integrating JaversRedisRepository with JPA (Hibernate)

The JaversRedisRepository can be seamlessly integrated with existing JPA repositories, enabling you to store JaVers audit data in Redis while continuing to use your JPA entities and repositories for application data.

However, when working with Hibernate, it’s common for entities to be wrapped in proxies. To ensure proper handling of these proxies, you need to configure the HibernateUnproxyObjectAccessHook in your application. This can be done by setting the following property in your application configuration file:

javers:
  objectAccessHook: org.javers.hibernate.integration.HibernateUnproxyObjectAccessHook

This configuration ensures that JaVers can unproxy Hibernate entities effectively, allowing audit logging to work seamlessly with Redis and JPA.

Custom JSON serialization

JaVers is meant to support various persistence stores (MongoDB, SQL) for any kind of your data. Hence, we use JSON format to serialize your objects in a JaversRepository.

JaVers uses the Gson library which provides neat and pretty JSON representation for well known Java types. But sometimes Gson’s defaults isn’t what you like. That happens many times when dealing with Values like Date, Money or ObjectId.

Consider the org.bson.types.ObjectId class, often used as Id-property for objects persisted in MongoDB.

By default, Gson serializes ObjectId as follows:

  "id": {
      "_time": 1417358422,
      "_machine": 1904935013,
      "_inc": 1615625682,
      "_new": true
  } 

As you can see, ObjectId is serialized using its 4 internal fields. The resulting JSON is verbose and ugly. You would rather expect neat and atomic value like this:

  "id": "54789e5cfb2ca07e65130e7c"

That’s where custom JSON TypeAdapters come into play.

JSON TypeAdapters

JSON TypeAdapters allows customizing JSON serialization of your Value types.

JaVers supports two families of TypeAdapters.

  1. JaVers family, specified by the JsonTypeAdapter interface. It’s a thin abstraction over Gson native type adapters. We recommend using this family in most cases as it has a nice API and isolates you (to some extent) from low level Gson API.
  2. Gson family, useful when you’re already using Gson and have adapters implementing the com.google.gson.TypeAdapter interface. Register your adapters with JaversBuilder.registerValueGsonTypeAdapter(...).

JSON TypeAdapter example

Consider the following domain Entity:

package org.javers.core.cases.morphia;

import org.bson.types.ObjectId;
... // omitted

@Entity
public class MongoStoredEntity {
    @Id
    private ObjectId _id;

    private String name;
    ... // omitted
}

First, we need to implement the JsonTypeAdapter interface. In this case, we recommend extending the BasicStringTypeAdapter abstract class.

ObjectIdTypeAdapter.java:

package org.javers.core.examples.adapter;

import org.bson.types.ObjectId;
import org.javers.core.json.BasicStringTypeAdapter;

public class ObjectIdTypeAdapter extends BasicStringTypeAdapter {

    @Override
    public String serialize(Object sourceValue) {
        return sourceValue.toString();
    }

    @Override
    public Object deserialize(String serializedValue) {
        return new ObjectId(serializedValue);
    }

    @Override
    public Class getValueType() {
        return ObjectId.class;
    }
}

Then, our TypeAdapter should be registered in JaversBuilder, and that’s it.

See how it works in the test case — JsonTypeAdapterExample.java:

@Test
public void shouldSerializeValueToJsonWithTypeAdapter() {
    //given
    Javers javers = JaversBuilder.javers()
    .registerValueTypeAdapter(new ObjectIdTypeAdapter())
    .build();

    //when
    ObjectId id = ObjectId.get();
    MongoStoredEntity entity = new MongoStoredEntity(id, "alg1", "1.0", "name");
    javers.commit("author", entity);
    CdoSnapshot snapshot = javers.getLatestSnapshot(id, MongoStoredEntity.class).get();

    //then
    String json = javers.getJsonConverter().toJson(snapshot);
    Assertions.assertThat(json).contains(id.toString());

    System.out.println(json);
}

The output:

{
  "commitMetadata": {
    "author": "author",
    "properties": [],
    "commitDate": "2021-03-12T15:50:17.663813",
    "commitDateInstant": "2021-03-12T14:50:17.663813Z",
    "id": 1.00
  },
  "globalId": {
    "entity": "org.javers.core.cases.MongoStoredEntity",
    "cdoId": "54876f694b9d4135b0b179ec"
  },
  "state": {
    "_algorithm": "alg1",
    "_name": "name",
    "_id": "54876f694b9d4135b0b179ec",
    "_version": "1.0"
  },
  "changedProperties": [
    "_algorithm",
    "_name",
    "_id",
    "_version"
  ],
  "type": "INITIAL",
  "version": 1
}

JaVers logo small
Open source Java library available under Apache License