DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations
  1. DZone
  2. Coding
  3. Frameworks
  4. Integrating Bi-Temporal Data in Spring Boot Applications

Integrating Bi-Temporal Data in Spring Boot Applications

The following example demonstrates how to use BarbelHisto to enable bi-temporal data in a Spring Boot application.

Niklas Schlimm user avatar by
Niklas Schlimm
CORE ·
Mar. 14, 19 · Tutorial
Like (1)
Save
Tweet
Share
10.42K Views

Join the DZone community and get the full member experience.

Join For Free

The following example demonstrates how to use BarbelHisto to enable bi-temporal data in a Spring Boot application. There are two fundamental alternatives to integrateBarbelHisto into Spring Boot: using BarbelHisto as a helper class in your services or using the BarbelHisto event listener persistence within Spring Boot services. The example in this article demonstrates the simple helper class alternative. The event listener alternative is described on the Project Barbel website.

The Spring Boot example application of this article can be found here in the examples repository.

Let's get started. Image title

Customer Model

First, we need an example POJO like this one:

public class Customer implements Bitemporal {

    @Id
    private String id;

    @DocumentId
    private String clientId;

    // version stamp
    private BitemporalStamp bitemporalStamp;

    private String firstName;
    private String lastName;
    private String street;
    private String city;
    private String postalcode;

    // constructor and accessors ...

}

The  @Id annotation is a spring annotation that uniquely identifies an object in the data source you use. We will use a MongoDB repository in this example. The @DocumentId is a BarbelHisto annotation to identify the functional identifier of the business object. A certain customer is uniquely identified by his corresponding clientId. BarbelHisto will group versions of a customer in a document journal using the document id.

Notice that we use BarbelMode.BITEMPORAL in this setup, so the Customer implements the Bitemporal interface. The BitemporalStamp will contain the version data. When users of BarbelHisto use custom persistence, it can be more convenient to use BarbelHisto in the  BarbelMode.BITEMPORAL. No proxying magic will be applied to any objects. And objects are stored as they are, just with an additional BitemporalStamp. This mode is very explicit and straightforward.

Defining Converters

We need two converters because Spring Boot is claiming conversion issues with ZonedDateTime. Use converters like this one in your application:

public class ZonedDateTimeReadConverter implements Converter<Date, ZonedDateTime> {
    @Override
    public ZonedDateTime convert(Date date) {
        return date.toInstant().atZone(ZoneOffset.UTC);
    }
}
public class ZonedDateTimeWriteConverter implements Converter<ZonedDateTime, Date> {
    @Override
    public Date convert(ZonedDateTime zonedDateTime) {
        return Date.from(zonedDateTime.toInstant());
    }
}

We demonstrate how to register these converters in Spring Boot later.

The Customer Repository

In the example, we're using a straight Spring MongoDB repository like this one:

public interface CustomerRepository extends MongoRepository<Customer, String> {

public List<Customer> findByLastName(String lastName);

public List<Bitemporal> findByClientId(String clientId);

public List<Bitemporal> findByClientIdAndBitemporalStampRecordTimeState(String clientId, BitemporalObjectState state);

}

The method findByClientId draws the complete version data (i.e. the document journal) for a given clientId. The findByClientIdAndBitemporalStampRecordTimeState draws active or inactive versions using the BitemporalStamp that we've applied to the Customer object.

The Customer Service Using BarbelHisto as Helper Class

Here is the service implementation that uses BarbelHisto as helper class for bi-temporal data.

@Component
public class CustomerService {

    @Autowired
    private CustomerRepository customerRepository;

    public void saveCustomer(Customer customer, LocalDate from, LocalDate until) {

        // (1) create BarbelHisto helper instance
        BarbelHisto<Customer> bitemporalHelper = BarbelHistoBuilder.barbel().withMode(BarbelMode.BITEMPORAL).build();

        // (2) load active records of the current Customer journal
        bitemporalHelper.load(customerRepository.findByClientIdAndBitemporalStampRecordTimeState(customer.getClientId(),
                BitemporalObjectState.ACTIVE));

        // (3) make a bitemporal update
        BitemporalUpdate<Customer> update = bitemporalHelper.save(customer, from, until);

        // (4) replace inactivated versions
        update.getInactivations().stream().forEach(i -> customerRepository.save(i));

        // (5) prepare inserts: clear IDs of new version records
        update.getInserts().stream().forEach(d -> d.setId(null));

        // (5) perform inserts of new version data
        customerRepository.insert(update.getInserts());

    }

}

This is the minimal setup when you'd use BarbelHisto. In the example, the service uses the BitemporalUpdate return value of the save-Operation to perform the updates to the underlying MongoCollection. There are two kinds of records returned in a BitemporalUpdate instance. 'Inactivations' are existing (stamped) records in your data source that were inactivated by the considered update to BarbelHisto. 'Inserts' on the other hand are new active versions. Lets go through this step-by-step:

  1. The service creates an instance of BarbelHisto  in BarbelMode.BITEMPORAL  

  2. It loads the current active(!) records of the Customer  journal to BarbelHisto  (here journal with the clientId "1234").

  3. Then the service performs the update against BarbelHisto  helper instance.

    Notice, that step 3 causes some active records to be inactivated and new active records are created for the new effective periods. We need to forward those changes to the backend MongoCollection now. Currently, only the BarbelHisto helper knows about the changes. We can propagate the changes using the BitemporalUpdate return value of the save-operation.

  4. First save inactivated records. The state of these existing(!) records changed. By performing a save operation the corresponding backend records are updated, i.e. inactivated.

  5. As a last step, insert the new active records created by the bi-temporal update. An insert needs to be prepared. The new active records were copied from existing ones, therefore they carry an old ID in the  Customer.id field. Hence, before we insert these records into the MongoCollection, we need to clear the IDs. Afterwards records can be inserted as new records.

This is what you need to do in your service when you use BarbelHisto as a helper in your backend services.

The Spring Boot Configuration

Let's look at the Spring Boot configuration to put all the above stuff together.

@SpringBootApplication
public class BarbelHistoHelperIntegrationApplication extends MongoConfigurationSupport implements CommandLineRunner {

    @Autowired
    private CustomerService service;

    @Autowired
    private CustomerRepository repository;


    public static void main(String[] args) throws Exception {
        SpringApplication.run(BarbelHistoHelperIntegrationApplication.class, args);
    }

    @Override
    protected String getDatabaseName() {
        return "test";
    }

    @Override
    public MongoCustomConversions customConversions() {
        final List<Converter<?, ?>> converters = new ArrayList<Converter<?, ?>>();
        converters.add(new ZonedDateTimeReadConverter());
        converters.add(new ZonedDateTimeWriteConverter());
        return new MongoCustomConversions(converters);
    }

    @Override
    public void run(String... args) throws Exception {

        Customer customer = new Customer("1234", "Alice", "Smith", "Some Street 10", "Houston", "77001");

        // save a couple of customers
        service.saveCustomer(customer, LocalDate.now(), EffectivePeriod.INFINITE);
        service.saveCustomer(customer, LocalDate.now().plusDays(10), EffectivePeriod.INFINITE);
        service.saveCustomer(customer, LocalDate.now().plusDays(20), EffectivePeriod.INFINITE);

        // validate the state of the journal
        Assert.isTrue(repository.findByClientIdAndBitemporalStampRecordTimeState("1234", BitemporalObjectState.ACTIVE).size() == 3, "must contain 3 active records");
        Assert.isTrue(repository.findByClientIdAndBitemporalStampRecordTimeState("1234", BitemporalObjectState.INACTIVE).size() == 2, "must contain 2 inactive records");

        service.saveCustomer(customer, LocalDate.now().minusDays(1), EffectivePeriod.INFINITE);

        // validate the state of the journal
        Assert.isTrue(repository.findByClientIdAndBitemporalStampRecordTimeState("1234", BitemporalObjectState.ACTIVE).size() == 1, "must contain 1 active records");
        Assert.isTrue(repository.findByClientIdAndBitemporalStampRecordTimeState("1234", BitemporalObjectState.INACTIVE).size() == 5, "must contain 5 inactive records");

        System.out.println(repository.findAll().toString());
     }
}

The converters for ZonedDateTime are registered in line 22. Notice that the run method performs some updates using the CustomerService just as the demo here. Anything else is just straightforward Spring Boot MongoDB configuration.

As you can see, you can integrate BarbelHisto fairly easy into any kind of Spring boot application. The advantage is that you don't have to put any effort or headache into creating a bullet-proof bitemporal audit trail because BarbelHisto will cover that subject for you. 

Looking forward to your feedback and any thoughts!

Spring Framework Spring Boot application Data (computing)

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • AWS CodeCommit and GitKraken Basics: Essential Skills for Every Developer
  • Required Knowledge To Pass AWS Certified Solutions Architect — Professional Exam
  • Microservices Testing
  • Cucumber.js Tutorial With Examples For Selenium JavaScript

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: