DEV Community

Cover image for Reactive Programming with Java Spring, R2DBC and MariaDB
Rob Hedgpeth
Rob Hedgpeth

Posted on • Updated on

Reactive Programming with Java Spring, R2DBC and MariaDB

Whether or not you've been actively developing within one of the JVM languages or not, you've likely at least heard of the concept of reactive programming by now. If not, I certainly encourage you to do a little research into the whole idea of declarative programming using data streams.

In this article we'll take a look at the (very) brief background of not only what Reactive Relational Database Connectivity (R2DBC) is but why it exists, and then dive into the code to see how the new MariaDB R2DBC connector can be used to create fully reactive, Java Spring (boot and data) applications!

We're going to walking through the process of creating a simple todo application, but if you'd prefer to jump directly into the code you can find it here.

It all starts with Reactive Streams

While I'm not going to give you the entire backstory and technical details behind reactive programming, it's important that we go over a few of the high level points before diving to all the R2DBC hoopla.

For starters, it's important to know (or recall, depending on your current circumstance) that reactive programming boils down to the idea of using asynchronous data streams.

A data stream is basically what it sounds like, a stream of data. But what does that really mean exactly?

A stream is a sequence of ongoing events ordered in time. There are three types of events; value, error, completed signal.

Alt Text

While we could certainly dive even deeper into the plethora of use cases for data streams, that's a little out of scope for this walk-through (I promise all of this is culminating to a walk-through). Instead we're going to focus on that the structure of a data stream is what brings the concept of reactive streams to life.

Ok, ok, I know what you're thinking; "Damn, this dude is throwing around the word reactive more than a Midwesterner roaming through a crowd using the word 'ope'". Bear with me, there's a point to all this madness. I promise!

Reactive Streams is simply a standard for asynchronous stream processing using the idea of non-blocking back pressure.

"Seriously, Rob! Back pressure?! Are you just making things up now? I think I'm getting back pressure just reading this nonsense."

  1. No, I'm not just messing with you.
  2. You can think of back pressure as a way of the receiving side (of a data stream) telling the sending side when they are ready for more information and how much to send.

Alt Text

Back pressure is an important concept because it's the foundation of which the reactive streams initiative is built on.

Keeping out of the gritty details, the key takeaway here is that Reactive Streams is a set of specifications, or rules, used to define the way asynchronous streams are managed.

TL;DR - reactive streams creates a standard approach.

The following (very high level) diagram shows the interactions (to manage, yep, you guessed it, data streams!) between publishers and subscribers.

Alt Text

For more information on reactive streams, why specification came about, the interfaces involved and analysis on use cases, I encourage you to take a look at the this write-up by John Thompson.

For our purposes, we're interested in the specification itself, specifically how it's utilized with Reactive Relational Database Connectivity (R2DBC).

R2DBC to the rescue

R2DBC started as an experiment and proof of concept to enable integration of SQL databases into systems that use reactive programming models. It's necessary because of limitations of the Java Database Connectivity (JDBC) API.

According to the http://r2dbc.io:

"R2DBC specifies a service-provider interface (SPI) that is intended to be implemented by driver vendors and used by client libraries. By using the R2DBC SPI, applications written in a JVM programming language can run SQL statements and retrieve results by using an underlying data source."

And if you haven't guess by now, the R2DBC SPI is based on Reactive Streams and uses the concepts of publishers and subscribers to allow non-blocking back-pressure-aware data access. Ah, yes, we've come full circle!

TL;DR - R2DBC is a new connectivity specification that supports reactive interactions all the way to the database level.

MariaDB Corporation recently released their R2DBC connector implementation. It's completely open-source and follows the R2DBC 0.8.1 specifications.

This is where you ceremoniously crack your knuckles. It's time to code!

Requirements

Before jumping into code, you're going to need to make sure you have a few things on your machine:

Getting Started with MariaDB

Let's assume you've either never used MariaDB or don't currently have an instance running on your machine. No problem! It only takes a couple of minutes, using a Docker container, to get MariaDB up and running. If you've already got an instance running feel free to skip down to the part where we create a new schema.

To pull the MariaDB Server image and spin up a container simply open a terminal window and run the following:

$ docker run -p 3306:3306 -d --name mariadb -eMARIADB_ROOT_PASSWORD=Password123! mariadb/server:10.4 
Enter fullscreen mode Exit fullscreen mode

The previous command will spin up a MariaDB Server container that you can connect to and communicate with using the MariaDB client.

There are many SQL clients available out in the wild. For simplicity's sake, I've chosen to demonstrate how to use the official MariaDB Client, but certainly feel free to use whatever client you prefer.

Connect to your MariaDB instance by executing the following command in a terminal window.

Alt Text

You should see something like the following, which means you've successfully connected to the MariaDB instance!

Next, create a new database for our TODO application.

CREATE DATABASE todo;
Enter fullscreen mode Exit fullscreen mode

Then create a new table to store our tasks.

CREATE TABLE todo.tasks (id int, description varchar(200), completed boolean);
Enter fullscreen mode Exit fullscreen mode

Getting reactive with R2DBC

With a database instance spun up and schema created you're ready to create a fully-reactive Java application.

Create a Maven project

Start by navigating to https://start.spring.io, which will enable you to create a new Spring-based Maven project.

For this project you can enter the following criteria.

Alt Text

Next, add the following dependencies:

Alt Text

Finally, click the "GENERATE" button to create and download the project (contained within a .zip file) to a desired location on your machine.

Add the MariaDB R2DBC connector

Navigate to the location where you downloaded the new Maven project (.zip file) to, and unzip.  Then use a code editor to open the project, and open pom.xml.

Add a new dependency for MariaDB's R2DBC connector to the collection of dependencies.

<dependency>
    <groupId>org.mariadb</groupId>
    <artifactId>r2dbc-mariadb</artifactId>
    <version>0.8.2-alpha2</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

The MariaDB R2DBC connector is not available in the Spring dependencies listing because it's currently still in the alpha phase of development.

Preparing the data integration

Now that you've created a project that contains all of the dependencies you'll need, it's time to start really coding. Typically, I like to start by creating the entity (or model) classes.

Navigate to /src/main/java/com/example/todo , create a new folder called
"models", and create a new file within it named "Task.java".

Open "Task.java" and add the following code.

package com.mariadb.todo.models;

import org.springframework.data.relational.core.mapping.Table;

import lombok.Data;

// Lombok annotation that eliminates getter/setter boilerplate code
@Data 
// Annotation that will point to table "tasks" (pluralized in the database)
@Table("tasks")
public class Task {
    @Id private Integer id;
    private String description;
    private Boolean completed;
}
Enter fullscreen mode Exit fullscreen mode

Next, create a new folder called "repositories" in /src/main/java/com/mariadb/todo, and create a new filed within it named "TasksRepository.java"

Open "TasksRepository.java" and add the following code.

package com.mariadb.todo.repositories;

import com.mariadb.todo.models.Task;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;

// Registered as a Spring Repository (Component)
// Repository = a mechanism for encapsulating storage, retrieval, and search behavior which emulates a collection of objects
public interface TasksRepository extends ReactiveCrudRepository<Task, Integer> {
}
Enter fullscreen mode Exit fullscreen mode

The ReactiveCrudRepository interface provides CRUD operations on a repository for a specific type, and follows reactive paradigms and uses Project Reactor types which are built on top of Reactive Streams.

Setting up the connection configuration

As previously mentioned, in order to create a connection to a database using R2DBC you must first create a ConnectionFactory. The MariaDB implementation of the ConnectionFactory interface is called MariadbConnectionFactory and it requires a MariadbConnectionConfiguration instance, which contains a variety of
configuration details used to connect to a MariaDB database instance.

Create a new folder called "config" in /src/main/java/com/mariadb/todo, and create a new filed within it named "R2DBCConfig.java"

Open "R2DBCConfig.java" and add the following code.

package com.mariadb.todo.config;

import org.mariadb.r2dbc.MariadbConnectionConfiguration;
import org.mariadb.r2dbc.MariadbConnectionFactory;
import org.mariadb.r2dbc.SslMode;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration;
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

@Configuration
@EnableR2dbcRepositories
public class R2DBCConfig extends AbstractR2dbcConfiguration {
    @Override
    @Bean
    public MariadbConnectionFactory connectionFactory() {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        Properties props = new Properties();
        try (InputStream f = loader.getResourceAsStream("db.properties")) {
            props.load(f); 
            return new MariadbConnectionFactory(MariadbConnectionConfiguration.builder()
                .host(props.getProperty("host"))
                .port(Integer.parseInt(props.getProperty("port")))
                .username(props.getProperty("username"))
                .password(props.getProperty("password"))
                .database(props.getProperty("database"))
                .build());
        }
        catch (IOException e) {
            System.out.println(e.getMessage());
            return null;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice that R2DBCConfig retrieves the database connection details from a file called "db.properties". To add the connection configuration navigate to /src/main/java/com/mariadb/resources, create a new file called "db.properties" and add the following code.

host=127.0.0.1
port=3306
username=USERNAME_HERE
password=PASSWORD_HERE
database=todo
Enter fullscreen mode Exit fullscreen mode

Although not recommended you can add connection details directly to R2DBCConfig.java.

Create a data service

Services can be used to manage the business logic of your application. The only service, TasksService, in this application is used for validating a Task object and integrating with the TasksRepository.

Create a new folder called "services" in /src/main/java/com/mariadb/todo, and create a new filed within it named "TasksService.java"

Open "TasksService.java" and add the following code.

package com.mariadb.todo.services;

import com.mariadb.todo.models.Task;
import com.mariadb.todo.repositories.TasksRepository;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

// Registered as a Spring Service (Component)
@Service
public class TaskService {

    // Automatically instantiate (via Spring IoC) 
    @Autowired
    private TasksRepository repository;

    // 
    public Boolean isValid(final Task task) {
        if (task != null && !task.getDescription().isEmpty()) {
            return true;
        }
        return false;
    }

    // Get all records from the tasks table
    public Flux<Task> getAllTasks() {
        return this.repository.findAll();
    }

    // Save a new task record
    public Mono<Task> createTask(final Task task) {
        return this.repository.save(task);
    }

    // Update an existing task record
    @Transactional
    public Mono<Task> updateTask(final Task task) {
        return this.repository.findById(task.getId())
                .flatMap(t -> {
                    t.setDescription(task.getDescription());
                    t.setCompleted(task.getCompleted());
                    return this.repository.save(t);
                });
    }

    // Delete the task record by specified id
    @Transactional
    public Mono<Void> deleteTask(final int id){
        return this.repository.findById(id)
                .flatMap(this.repository::delete);
    }
}
Enter fullscreen mode Exit fullscreen mode

Expose API endpoints

Finally, you'll need to create a controller to expose four endpoints that can be used to perform the basic CRUD operations on your Tasks.

Create a new folder called "controllers" in /src/main/java/com/mariadb/todo, and create a new filed within it named "TasksController.java"

Open "TasksController.java" and add the following code.

package com.mariadb.todo.controllers;

import com.mariadb.todo.models.Task;
import com.mariadb.todo.services.TaskService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/api/tasks")
public class TasksController {

    @Autowired
    private TaskService service;

    @GetMapping()
    public ResponseEntity<Flux<Task>> get() {
        return ResponseEntity.ok(this.service.getAllTasks());
    }

    @PostMapping()
    public ResponseEntity<Mono<Task>> post(@RequestBody Task task) {
        if (service.isValid(task)) {
            return ResponseEntity.ok(this.service.createTask(task));
        }
        return ResponseEntity.status(HttpStatus.I_AM_A_TEAPOT).build();
    }

    @PutMapping()
    public ResponseEntity<Mono<Task>> put(@RequestBody Task task) {
        if (service.isValid(task)) {
            return ResponseEntity.ok(this.service.updateTask(task));
        }
        return ResponseEntity.status(HttpStatus.I_AM_A_TEAPOT).build();
    }

    @DeleteMapping()
    public ResponseEntity<Mono<Void>> delete(@RequestParam int id) {
        if (id > 0) {
            return ResponseEntity.ok(this.service.deleteTask(id));
        }
        return ResponseEntity.status(HttpStatus.I_AM_A_TEAPOT).build();
    }
}
Enter fullscreen mode Exit fullscreen mode

Testing it out

Now that everything has been constructed, it's time to test it out!

First, build the application.

$ mvn package
Enter fullscreen mode Exit fullscreen mode

And then run it.

$ mvn spring-boot:run
Enter fullscreen mode Exit fullscreen mode

First, start by adding a new task to your to do list.

$ curl --header "Content-Type: application/json" \
  --request POST \
  --data '{"description":"A New Task"}' \
  http://localhost:8080/api/tasks
Enter fullscreen mode Exit fullscreen mode

While you can certainly query the database directly to confirm that a new task record has been added, where's the fun in that? Back to the API!

$ curl https://localhost:8080/api/tasks
Enter fullscreen mode Exit fullscreen mode

If all goes well you should receive the following JSON response:

{ "id": 1, "description": "A New Task", "completed": false }
Enter fullscreen mode Exit fullscreen mode

Voilà, a fully reactive Java Spring application using R2DBC and MariaDB!

To view this code in its entirety check out the source here. And if you're wondering "it'd sure be nice to see an implementation with a user interface", you're in luck! You can find a fully fleshed out implementation of a TODO application using React.js and your choice of multiple API projects
(R2DBC, JDBC, Node.js and Python) that integrate directly with MariaDB here!

Just getting started

Now that you've successfully created a new Maven project using Spring Data, R2DBC and MariaDB, you have all the tools you need to get started creating fully reactive applications, utilizing the power of MariaDB, of your own!

If you have any questions, suggestions or concerns with this blog post please let me know here or reach out to me directly on Twitter at @probablyrealrob!

Thanks for taking the time to read this and happy coding!

Oldest comments (1)

Collapse
 
noorkrichen profile image
Noor Krichen • Edited

Thank you Rob for this great article.

Do you know how to establish the relations "one to many" and "many to many" with spring data mongodb reactive ?
I think that @DBRef and @DocumentReference do not work properly.