Monday, October 31, 2016

What’s New in Speedment 3.0

Spire standing in front of a sign that says Forest

If you have followed my blog you know that I have been involved in the open-source project Speedment for a while. During the summer and fall I have worked a lot with finishing up the next big 3.0.0 release of the toolkit. In this post I will showcase some of the cool new features we have built into the platform and also explain how you can get started!

New Module System

The biggest change from the previous version of Speedment, and the one that took us most time to get right, is the new module system. If you have been following the progress of the new JDK 9 project Jigsaw, you will recognize this subject. Previously, Speedment consisted of one big artifact called com.speedment:speedment. In addition to this, we had a few minor projects like the speedment-maven-plugin and speedment-archetypes that made the tool easier to use. There was several issues with this design. First, it was very tedious to develop in it since we often needed to rebuild the entire thing multiple times each day and every build could take minutes. It was also not very plugin-friendly since a plugin had to depend on the entire code base, even if it only modified a small group of classes.

In 3.0 however, com.speedment is actually a multi-module pom-project with a clear build order. Inside it are groups of artifacts, also realized as multi-module projects, that separate artifacts based on when they are needed. We now have the following artifact groups:

  1. common-parent contains artifacts that are mature, reusable in a number of situations and that doesn’t have any dependencies (except on our own lightweight logging framework). Here you will find some of the core utilities of Speedment like MapStream and CodeGen.
  2. runtime-parent contains artifacts that are required by the end-user during the runtime of their application. We wanted to separate these into their own group to make sure that the final jar of the user’s app has as small footprint as possible.
  3. generator-parent contains artifacts related to the code generation and database analyzation parts of Speedment. These classes doesn’t require a graphical environment which is useful if you want to use Speedment as a general purpose code generator in a non-graphical environment.
  4. tool-parent contains all the artifacts used by the graphical Speedment tool. Here we put all our home-brewed JavaFX-components as well as resources like icons used by the UI.
  5. build-parent is a meta group that contains various artifacts that we build simply to make Speedment easier to use for the end user. Here we for an example have a number of shaded artifacts that you can use when you deploy your application on a server and the Maven Plugin that users use to launch Speedment as a Maven goal.
  6. plugins-parent is a whole new group where we put official plugins for Speedment that doesn’t quite fit into the general framework but that many users request. This makes it possible for us to automatically rebuild them in the general build cycle, making sure they are always up-to-date with the latest changes in the platform.
  7. archetypes-parent is a group of all the official Maven Archetypes for Speedment. This was previously a separate project but has now been lifted into the main project so that they can be automatically reinstalled every time Speedment is built.

All these groups are built in the same order as specified above. This makes it much easier to keep dependencies single-directional and the overall design of the system more comprehensive.

So how do I use it?

The beautiful thing is that you barely have to change a thing! We automatically build an artifact that is called com.speedment:runtime that you can depend on in your project. It contains transitive dependencies to the exact set of artifacts that are required to run Speedment.


<dependency>
    <groupId>com.speedment</groupId>
    <artifactId>runtime</artifactId>
    <version>3.0.1</version>
    <type>pom</type>
</dependency>

When it is time for deployment, you simply replace this dependency with com.speedment:runtime-deploy and you will get a shaded jar with all the Speedment-stuff bundled together and ready to ship!


<dependency>
    <groupId>com.speedment</groupId>
    <artifactId>runtime-deploy</artifactId>
    <version>3.0.1</version>
</dependency>

For more details about the new release, go to this official GitHub page and fork it!

Monday, October 24, 2016

Database CRUD Operations in Java 8 Streams

Spire and Duke in front of the letters CRUD

The biggest obstacle to overcome when starting out with a new tool is to get your head around how to do the little things. By now you might feel confident in how the new Java 8 Stream API works, but you might not have used it for database querying yet. To help you get started creating, modifying and reading from your SQL database using the Stream API, I have put together this quick start. Hopefully it can help you take your streams to the next level!

Background

Speedment is an Open Source toolkit that can be used to generate java entities and managers for communicating with a database. Using a graphical tool you connect to your database and generate a complete ORM tailored to represent your domain model. But Speedment is not only a code generator but also a runtime that plugs into your application and makes it possible to translate your Java 8 streams into optimized SQL queries. That is the part that I will focus on in this article.

Generate Code

To begin using Speedment in a Maven project, add the following lines to your pom.xml-file. In this example I am using MySQL, but you can use PostgreSQL or MariaDB as well. Connectors to proprietary databases like Oracle are available for enterprise customers.

pom.xml

<properties>
  <speedment.version>3.0.1</speedment.version>
  <db.groupId>mysql</db.groupId>
  <db.artifactId>mysql-connector-java</db.artifactId>
  <db.version>5.1.39</db.version>
</properties>

<dependencies>
  <dependency>
    <groupId>com.speedment</groupId>
    <artifactId>runtime</artifactId>
    <version>${speedment.version}</version>
    <type>pom</type>
  </dependency>
        
  <dependency>
    <groupId>${db.groupId}</groupId>
    <artifactId>${db.artifactId}</artifactId>
    <version>${db.version}</version>
  </dependency>
</dependencies>

<build>
  <plugins>
    <plugin>
      <groupId>com.speedment</groupId>
      <artifactId>speedment-maven-plugin</artifactId>
      <version>${speedment.version}</version>

      <dependencies>
        <dependency>
          <groupId>${db.groupId}</groupId>
          <artifactId>${db.artifactId}</artifactId>
          <version>${db.version}</version>
        </dependency>
      </dependencies>
    </plugin>
  </plugins>
</build>

You now have access to a number of new Maven Goals that make it easier to use the toolkit. The launch the Speedment UI, execute:

mvn speedment:tool

This will guide you through the process of connecting to the database and configure the code generation. The simplest way in the beginning is you just run along with the default settings. Once you press “Generate”, Speedment will analyze your database metadata and fill your project with new sources like entity and manager classes.

Initialize Speedment

Once you have generated your domain model, setting up Speedment is easy. Create a new Main.java-file and add the following lines. All the classes you see are generated so their names will depend on the names of your database schemas, tables and columns.

Main.java

public class Main {
  public static void main(String... param) {
    final HaresApplication app = new HaresApplicationBuilder()
      .withPassword("password")
      .build();
  }
}

The code above creates a new application instance using a generated builder pattern. The builder makes it possible to set any runtime configuration details like database passwords.

Once we have an app instance, we can use it to get access to the generated managers. In this case, I have four tables in the database; “hare”, “carrot”, “human” and “friend”. (You can see the entire database definition here).


final CarrotManager carrots = app.getOrThrow(CarrotManager.class);
final HareManager hares     = app.getOrThrow(HareManager.class);
final HumanManager humans   = app.getOrThrow(HumanManager.class);
final FriendManager friends = app.getOrThrow(FriendManager.class);

These managers can now be used to perform all our CRUD operations.

Create Entities

Creating entities is very straight forward. We use the generated implementation of our entities, set the values we want for columns and then persist it to the data source.


hares.persist(
  new HareImpl()
    .setName("Harry")
    .setColor("Gray")
    .setAge(8)
);

The persist method returns a (potentially) new instance of Hare where auto-generated keys like “id” has been set. If we want to use Harry after persisting him we should therefore use the instance returned by persist.


final Hare harry = hares.persist(
  new HareImpl()
    .setName("Harry")
    .setColor("Gray")
    .setAge(8)
);

If the persistence fails, for an example if a foreign key or a unique constraint fails, a SpeedmentException is thrown. We should check for this and show an error if something prevented us from persisting the hare.


try {
  final Hare harry = hares.persist(
    new HareImpl()
      .setName("Harry")
      .setColor("Gray")
      .setAge(8)
  );
} catch (final SpeedmentException ex) {
  System.err.println(ex.getMessage());
  return;
}

Read Entities

The coolest feature in the Speedment runtime is the ability to stream over data in your database using Java 8 Streams. “Why is that so cool?” you might ask yourself. “Even Hibernate have support for streaming nowadays!”

The beautiful thing with Speedment streams is that they take intermediary and terminating actions into consideration when constructing the stream. This means that if you add a filter to the stream after it has been created, it will still be taken into consideration when building the SQL statement.

Here is an example. We want to count the total number of hares in the database.


final long haresTotal = hares.stream().count();
System.out.format("There are %d hares in total.%n", haresTotal);

The SQL query that will be generated is the following:


SELECT COUNT(*) FROM hares.hare;

The terminating operation was a .count() so Speedment knows that it is a SELECT COUNT(...)-statement that is to be created. It also knows that the primary key for the “hare” table is the column “id”, which makes it possible to reduce the entire statement sent to the database down into this.

A more complex example might be to find the number of hares that has a name that ends with the letters “rry” and an age greater or equal to 5. That can be written as this:


final long complexTotal = hares.stream()
  .filter(Hare.NAME.endsWith("rry"))
  .filter(Hare.AGE.greaterOrEqual(5))
  .count();

We use the predicate builders generated to us by Speedment to define the filters. This make it possible for us to analyze the stream programmatically and reduce it down to the following SQL statement:


SELECT COUNT(id) FROM hares.hare
WHERE hare.name LIKE CONCAT("%", ?)
AND hare.age >= 5;

If we add an operation that Speedment can’t optimize to the stream, it will be resolved just like any Java 8 stream. We are never limited to the use of the generated predicate builders, it just makes the stream more efficient.


final long inefficientTotal = hares.stream()
  .filter(h -> h.getName().hashCode() == 52)
  .count();

This would produce the following extremely inefficient statement, but it will still work.


SELECT id,name,color,age FROM hares.hare;

Update Entities

Updating existing entities are done very similar to how we read and persist entities. Changes made to a local copy of an entity will not affect the database until we call the update()-method in the manager.

In this case we take the hare Harry created earlier and we want to change his color to brown:


harry.setColor("brown");
final Hare updatedHarry = hares.update(harry);

The manager returns a new copy of the hare if the update is accepted, so we should continue using that instance after this point. Just like in the “create”-example, the update might fail. Maybe color was defined as a “unique” column and a “brown” hare already existed. In that case, a SpeedmentException is thrown.

We can also update multiple entities at the same time by combining it with a stream. Say that we want to make all hares named “Harry” brown. In that case, we do this:


hares.stream()
  .filter(Hare.NAME.equal("Harry"))
  .map(Hare.COLOR.setTo("Brown"))
  .forEach(hares.updater()); // Updates remaining elements in the Stream

We should also wrap it in a try-catch to make sure we warn the user if a constraint failed.


try {
  hares.stream()
    .filter(Hare.NAME.equal("Harry"))
    .map(Hare.COLOR.setTo("Brown"))
    .forEach(hares.updater());
} catch (final SpeedmentException ex) {
  System.err.println(ex.getMessage());
  return;
}

Removing Entities

The last CRUD operation we need to know is how to remove entities from the database. This is almost identical to the “update”. Say that we want to remove all hares older than 10 years. We then do this:


try {
  hares.stream()
    .filter(Hare.AGE.greaterThan(10))
    .forEach(hares.remover()); // Removes remaining hares
} catch (final SpeedmentException ex) {
  System.err.println(ex.getMessage());
  return;
}

Summary

In this article you have learned how to set up Speedment in a Maven project and how to create, update, read and delete entities from a database using Java 8 Streams. This is only a small subset of all the things you can do with Speedment, but it serves as a good introduction to start getting your hands dirty. More examples and more advanced use-cases can be found on the GitHub-page.

Until next time!

Tuesday, October 11, 2016

Event-Sourcing and CQRS in Practise

Sauna.png

Anyone that has tried to implement a fully ACID compliant system knows that there are a lot of considerations you have to do. You need to make sure database entities can be freely created, modified and deleted without the risk of errors, and in most cases, the solution will be at the cost of performance. One methodology that can be used to get around this is to design the system based on a series of events rather than mutable states. This is generally called Event Sourcing.

In this article I will showcase a demo application that uses the Open Source toolkit Speedment to rapidly get a scalable event-sourced database application up and running. Full source code for the example is available here.

What is Event Sourcing?

In a typical relational database system you store the state of an entity as a row in a database. When the state changes, the application modifies the row using an UPDATE or a DELETE-statement. A problem with this method is that it adds a lot of requirements on the database when it comes to making sure that no row is changed in a way that puts the system in an illegal state. You don’t want anyone to withdraw more money than they have in their account or bid on an auction that has already been closed.

In an event-sourced system, we take a different approach to this. Instead of storing the state of an entity in the database, you store the series of changes that led to that state. An event is immutable once it is created, meaning that you only have to implement two operations, CREATE and READ. If an entity is updated or removed, that is realized using the creation of an “update” or “remove” event.

An event sourced system can easily be scaled up to improve performance, as any node can simply download the event log and replay the current state. You also get better performance due to the fact that writing and querying is handled by different machines. This is referred to as CQRS (Command-Query Responsibility Segregation). As you will see in the examples, we can get an eventually consistent materialized view up and running in a very little time using the Speedment toolkit.

The Bookable Sauna

To showcase the workflow of building an event sourced system we will create a small application to handle the booking of a shared sauna in a housing complex. We have multiple tenants interested in booking the sauna, but we need to guarantee that the shy tenants never accidentally double-book it. We also want to support multiple saunas in the same system.

To simplify the communication with the database, we are going to use the Speedment toolkit. Speedment is a java tool that allows us to generate a complete domain model from the database and also makes it easy to query the database using optimized Java 8 streams. Speedment is available under the Apache 2-license and there are a lot of great examples for different usages on the Github page.

Step 1: Define the Database Schema

The first step is to define our (MySQL) database. We simply have one table called “booking” where we store the events related to booking the sauna. Note that a booking is an event and not an entity. If we want to cancel a booking or make changes to it, we will have to publish additional events with the changes as new rows. We are not allowed to modify or delete a published row.


CREATE DATABASE `sauna`;

CREATE TABLE `sauna`.`booking` (
  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `booking_id` BIGINT NOT NULL,
  `event_type` ENUM('CREATE', 'UPDATE', 'DELETE') NOT NULL,
  `tenant` INT NULL,
  `sauna` INT NULL,
  `booked_from` DATE NULL,
  `booked_to` DATE NULL,
  PRIMARY KEY (`id`)
);

The “id” column is an increasing integer that is assigned automatically every time a new event is published to the log. The “booking_id” tells us which booking we are referring to. If two events share the same booking id, they refer to the same entity. We also have an enum called “event_type” that describes which kind of operation we were trying to do. After that comes the information that belongs to the booking. If a column is NULL, we will consider that as unmodified compared to any previous value.

Step 2: Generating Code using Speedment

The next step is to generate code for the project using Speedment. Simply create a new maven project and add the following code to the pom.xml-file.

pom.xml

<properties>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  <maven.compiler.source>1.8</maven.compiler.source>
  <maven.compiler.target>1.8</maven.compiler.target>
  <speedment.version>3.0.0-EA2</speedment.version>
  <mysql.version>5.1.39</mysql.version>
</properties>

<build>
  <plugins>
    <plugin>
      <groupId>com.speedment</groupId>
      <artifactId>speedment-maven-plugin</artifactId>
      <version>${speedment.version}</version>

      <dependencies>
        <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
          <version>${mysql.version}</version>
        </dependency>
      </dependencies>
    </plugin>
  </plugins>
</build>

<dependencies>
  <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>${mysql.version}</version>
  </dependency>

  <dependency>
    <groupId>com.speedment</groupId>
    <artifactId>runtime</artifactId>
    <version>${speedment.version}</version>
    <type>pom</type>
  </dependency>
</dependencies>

If you build the project, a new maven goal called speedment:tool should appear in the IDE. Run it to launch the Speedment user interface. In there, connect to the Sauna database and generate code using the default settings. The project should now be populated with source files.

Tip: If you make changes to the database, you can download the new configuration using the speedment:reload-goal and regenerate sources using speedment:generate. No need to relaunch the tool!

Step 3: Creating the Materialized View

The materialized view is a component that regularly polls the database to see if any new rows have been added, and if so, downloads and merges them into the view in the correct order. Since the polling sometimes can take a lot of time, we want this process to run in a separate thread. We can accomplish that with a java Timer and TimerTask.

Polling the database? Really? Well, an important thing to take into consideration is that it is only the server that will poll the database, not the clients. This gives us very good scalability since we can have a handful of servers polling the database that in turn serve hundreds of thousands of tenants. Compare this with a regular system where every client would request a resource from the server, that in turn contacts the database.

BookingView.java

public final class BookingView {

  ...

  public static BookingView create(BookingManager mgr) {
    final AtomicBoolean working = new AtomicBoolean(false);
    final AtomicLong last  = new AtomicLong();
    final AtomicLong total = new AtomicLong();
        
    final String table = mgr.getTableIdentifier().getTableName();
    final String field = Booking.ID.identifier().getColumnName();

    final Timer timer = new Timer();
    final BookingView view = new BookingView(timer);
    final TimerTask task = ...;

    timer.scheduleAtFixedRate(task, 0, UPDATE_EVERY);
    return view;
  }
}

The timer task is defined anonymously and that is where the polling logic will reside.


final TimerTask task = new TimerTask() {
  @Override
  public void run() {
    boolean first = true;

    // Make sure no previous task is already inside this block.
    if (working.compareAndSet(false, true)) {
      try {

        // Loop until no events was merged 
        // (the database is up to date).
        while (true) {

          // Get a list of up to 25 events that has not yet 
          // been merged into the materialized object view.
          final List added = unmodifiableList(
            mgr.stream()
              .filter(Booking.ID.greaterThan(last.get()))
              .sorted(Booking.ID.comparator())
              .limit(MAX_BATCH_SIZE)
              .collect(toList())
            );

          if (added.isEmpty()) {
            if (!first) {
              System.out.format(
                "%s: View is up to date. A total of " + 
                "%d rows have been loaded.%n",
                System.identityHashCode(last),
                total.get()
              );
            }

            break;
          } else {
            final Booking lastEntity = 
              added.get(added.size() - 1);

            last.set(lastEntity.getId());
            added.forEach(view::accept);
            total.addAndGet(added.size());

            System.out.format(
              "%s: Downloaded %d row(s) from %s. " + 
              "Latest %s: %d.%n", 
              System.identityHashCode(last),
              added.size(),
              table,
              field,
              Long.parseLong("" + last.get())
            );
          }

          first = false;
        }

        // Release this resource once we exit this block.
      } finally {
        working.set(false);
      }
    }
  }
};

Sometimes the merging task can take more time to complete than the interval of the timer. To avoid this causing a problem, we use an AtomicBoolean to check and make sure that only one task can execute at the same time. This is similar to a Semaphore, except that we want tasks that we don’t have time for to be dropped instead of queued since we don’t really need every task to execute, a new one will come in just a second.

The constructor and basic member methods are fairly easy to implement. We store the timer passed to the class as a parameter in the constructor so that we can cancel that timer if we ever need to stop. We also store a map that keeps the current view of all the bookings in memory.


private final static int MAX_BATCH_SIZE = 25;
private final static int UPDATE_EVERY   = 1_000; // Milliseconds

private final Timer timer;
private final Map<Long, Booking> bookings;

private BookingView(Timer timer) {
  this.timer    = requireNonNull(timer);
  this.bookings = new ConcurrentHashMap<>();
}

public Stream<Booking> stream() {
  return bookings.values().stream();
}

public void stop() {
  timer.cancel();
}

The last missing piece of the BookingView class is the accept()-method used above in the merging procedure. This is where new events are taken into consideration and merged into the view.


private boolean accept(Booking ev) {
    final String type = ev.getEventType();

    // If this was a creation event
    switch (type) {
        case "CREATE" :
            // Creation events must contain all information.
            if (!ev.getSauna().isPresent()
            ||  !ev.getTenant().isPresent()
            ||  !ev.getBookedFrom().isPresent()
            ||  !ev.getBookedTo().isPresent()
            ||  !checkIfAllowed(ev)) {
                return false;
            }

            // If something is already mapped to that key, refuse the 
            // event.
            return bookings.putIfAbsent(ev.getBookingId(), ev) == null;

        case "UPDATE" :
            // Create a copy of the current state
            final Booking existing = bookings.get(ev.getBookingId());

            // If the specified key did not exist, refuse the event.
            if (existing != null) {
                final Booking proposed = new BookingImpl();
                proposed.setId(existing.getId());

                // Update non-null values
                proposed.setSauna(ev.getSauna().orElse(
                    unwrap(existing.getSauna())
                ));
                proposed.setTenant(ev.getTenant().orElse(
                    unwrap(existing.getTenant())
                ));
                proposed.setBookedFrom(ev.getBookedFrom().orElse(
                    unwrap(existing.getBookedFrom())
                ));
                proposed.setBookedTo(ev.getBookedTo().orElse(
                    unwrap(existing.getBookedTo())
                ));

                // Make sure these changes are allowed.
                if (checkIfAllowed(proposed)) {
                    bookings.put(ev.getBookingId(), proposed);
                    return true;
                }
            }

            return false;


        case "DELETE" :
            // Remove the event if it exists, else refuse the event.
            return bookings.remove(ev.getBookingId()) != null;

        default :
            System.out.format(
                "Unexpected type '%s' was refused.%n", type);
            return false;
    }
}

In an event sourced system, the rules are not enforced when events are received but when they are materialized. Basically anyone can insert new events into the system as long as they do it in the end of the table. It is in this method that we choose to discard events that doesn’t follow the rules setup.

Step 4: Example Usage

In this example, we will use the standard Speedment API to insert three new bookings into the database, two that are valid and a third that intersects one of the previous ones. We will then wait for the view to update and print out every booking made.


public static void main(String... params) {
  final SaunaApplication app = new SaunaApplicationBuilder()
    .withPassword("password")
    .build();

  final BookingManager bookings = 
    app.getOrThrow(BookingManager.class);

  final SecureRandom rand = new SecureRandom();
  rand.setSeed(System.currentTimeMillis());

  // Insert three new bookings into the system.
  bookings.persist(
    new BookingImpl()
      .setBookingId(rand.nextLong())
      .setEventType("CREATE")
      .setSauna(1)
      .setTenant(1)
      .setBookedFrom(Date.valueOf(LocalDate.now().plus(3, DAYS)))
      .setBookedTo(Date.valueOf(LocalDate.now().plus(5, DAYS)))
  );

  bookings.persist(
    new BookingImpl()
      .setBookingId(rand.nextLong())
      .setEventType("CREATE")
      .setSauna(1)
      .setTenant(2)
      .setBookedFrom(Date.valueOf(LocalDate.now().plus(1, DAYS)))
      .setBookedTo(Date.valueOf(LocalDate.now().plus(2, DAYS)))
  );

  bookings.persist(
    new BookingImpl()
      .setBookingId(rand.nextLong())
      .setEventType("CREATE")
      .setSauna(1)
      .setTenant(3)
      .setBookedFrom(Date.valueOf(LocalDate.now().plus(2, DAYS)))
      .setBookedTo(Date.valueOf(LocalDate.now().plus(7, DAYS)))
  );

  final BookingView view = BookingView.create(bookings);

  // Wait until the view is up-to-date.
  try { Thread.sleep(5_000); }
  catch (final InterruptedException ex) {
    throw new RuntimeException(ex);
  }

  System.out.println("Current Bookings for Sauna 1:");
  final SimpleDateFormat dt = new SimpleDateFormat("yyyy-MM-dd");
  final Date now = Date.valueOf(LocalDate.now());
  view.stream()
    .filter(Booking.SAUNA.equal(1))
    .filter(Booking.BOOKED_TO.greaterOrEqual(now))
    .sorted(Booking.BOOKED_FROM.comparator())
    .map(b -> String.format(
      "Booked from %s to %s by Tenant %d.", 
      dt.format(b.getBookedFrom().get()),
      dt.format(b.getBookedTo().get()),
      b.getTenant().getAsInt()
    ))
    .forEachOrdered(System.out::println);

  System.out.println("No more bookings!");
  view.stop();
}

If we run it, we get the following output:

677772350: Downloaded 3 row(s) from booking. Latest id: 3.
677772350: View is up to date. A total of 3 rows have been loaded.
Current Bookings for Sauna 1:
Booked from 2016-10-11 to 2016-10-12 by Tenant 2.
Booked from 2016-10-13 to 2016-10-15 by Tenant 1.
No more bookings!

Full source code for this demo application is available on my GitHub page. There you can also find many other examples on how to use Speedment in various scenarios to rapidly develop database applications.

Summary

In this article we have developed a materialized view over a database table that evaluates events on materialization and not upon insertion. This makes it possible to spin up multiple instances of the application without having to worry about synchronizing them since they will be eventually consistent. We then finished by showing how the materialized view can be queried using the Speedment API to produce a list of current bookings.

Thank you for reading and please checkout more Speedment examples at the Github page!