Logo

dev-resources.site

for different kinds of informations.

Spring R2DBC, MariaDB, and JSON columns

Published at
12/7/2022
Categories
spring
webflux
r2dbc
json
Author
kappaj2
Categories
4 categories in total
spring
open
webflux
open
r2dbc
open
json
open
Author
7 person written this
kappaj2
open
Spring R2DBC, MariaDB, and JSON columns

Recently ran into a requirement to store JSON data in a MariaDB database column. With PostgreSQL, we have a lot of support already, but information on MariaDB (or MySQL) is very scarce.

After a bit of digging and lots and lots of reading, I managed to get the following going.
Using SpringBoot version 2.7.3, R2DBC as pulled in by the pom.xml

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-r2dbc</artifactId>
        </dependency>
Enter fullscreen mode Exit fullscreen mode

The MariaDB table is declared as:

CREATE TABLE IF NOT EXISTS `customer`.`interview_response_data` (
  `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
  `interview_id` VARCHAR(45) NOT NULL,
  `score_data` JSON NULL,
  `ocr_data` JSON NULL,
  `score_overall_status` VARCHAR(45) NULL,
  `score_overall_value` DECIMAL(3,1) NULL,
  `score_reason_message` VARCHAR(1024) NULL,
  `score_needs_review_reason` VARCHAR(255) NULL,
  `date_created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`))
ENGINE = InnoDB;
Enter fullscreen mode Exit fullscreen mode

The entity mapping is declared as:

@Table("interview_response_data")
@Data
@Builder
@ToString
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@NoArgsConstructor
@AllArgsConstructor
public class InterviewResponseDataEntity {

     @Id
     @Column(name = "id", nullable = false)
     @EqualsAndHashCode.Include
     private Long id;

     @Column(name = "interview_id", nullable = false)
     private String interviewId;

     @Enumerated(EnumType.STRING)
     @Column(name = "score_overall_status")
     private IncodeScoreStatusEnum scoreOverallStatus;

     @Column(name = "score_data")
     private JsonNode scoreData;
...
}
Enter fullscreen mode Exit fullscreen mode

The raw JSON data is coming from a call to Incode to retrieve customer OCR data.
A handy WebClient wrapper class will handle the GET call for us to retrieve different data structures.

     public <T> Mono<T> incodeGETRequest(
             final Function<UriBuilder, URI> uriBuilderFunction,
             final Class<T> modelImplementation,
             final String token) {
          return incodeWebClient
                  .get()
                  .uri(uriBuilderFunction)
                  .headers(httpHeaders -> httpHeaders.add("X-Incode-Hardware-Id", token))
                  .retrieve()
                  .onStatus(httpStatus -> (httpStatus.is4xxClientError() ||
                                  httpStatus.is5xxServerError() ||
                                  httpStatus.value() == 204),
                          clientResponse -> clientResponse.createException().flatMap(Mono::error)
                  )
                  .bodyToMono(modelImplementation);
     }
Enter fullscreen mode Exit fullscreen mode

Now we can have a service class that will retrieve the OCR data for us if it is provided with an InterviewId. This interviewId is obtained from Incode when you start the customer process. Part of that response is also a token to be used with subsequent data calls.

 @Override
     public Mono<InterviewResponseDataEntity> registerInterviewResults(String interviewId) {#edeaea

          var token = "eyJh...";

          Mono<JsonNode> scoreDataMono = incodeWebClientHandler.incodeGETRequest(builder -> builder.path(SCORE_DATA_ENDPOINT)
                          .queryParam("id", interviewId)
                          .build(),
                  JsonNode.class,
                  token
          );

          Mono<JsonNode> ocrDataMono = incodeWebClientHandler.incodeGETRequest(builder -> builder.path(OCR_DATA_ENDPOINT)
                          .queryParam("id", interviewId)
                          .build(),
                  JsonNode.class,
                  token
          );

          return scoreDataMono.zipWith(ocrDataMono)
                  .flatMap(objects -> {
                       var scoreData = objects.getT1();
                       var ocrData = objects.getT2();

                       log.info("Score data : {}", scoreData);
                       log.info("OCR Data   : {}", ocrData);

                       var interviewResponseDataEntity = new InterviewResponseDataEntity();
                       interviewResponseDataEntity.setInterviewId(interviewId);
                       interviewResponseDataEntity.setOcrData(ocrData);
                       interviewResponseDataEntity.setScoreData(scoreData);
...
return interviewResponseDataRepository.save(interviewResponseDataEntity);
Enter fullscreen mode Exit fullscreen mode

This will retrieve our JSON data, from which we can then unpack what is needed and write to the database.

Before we can call the persist, we need to write converter utility classes for the conversion to and from JsonNode.

Note the @ReadingConverter – this will be used to convert the stored data in the database column to a JsonNode.

@Slf4j
@ReadingConverter
@AllArgsConstructor
public class JsonNodeReadingConverter implements Converter<String, JsonNode> {

     private ObjectMapper objectMapper;

     @Override
     public JsonNode convert(String source) {

          if (!(StringUtils.hasLength(source) && StringUtils.hasText(source))) {
               log.debug("Database column is null");
               return null;
          }

          try {
               return objectMapper.readValue(source, JsonNode.class);
          } catch (Exception ex) {
               log.error("Error converting json column data from database", ex);
          }
          return null;
     }

}
Enter fullscreen mode Exit fullscreen mode

Note the @WritingConverter. This will be used to convert the JsonNode to a String to be written to the database column.

@Slf4j
@WritingConverter
@AllArgsConstructor
public class StringToJsonConverter implements Converter<JsonNode, String> {

     private final ObjectMapper objectMapper;

     @Override
     public String convert(JsonNode source) {

          if (source == null) {
               return null;
          }
          String response = null;
          try {
               response = objectMapper.writeValueAsString(source);
          } catch (JsonProcessingException e) {
               log.error("Error parsing JsonNode to database column", e);
          }
          return response;
     }

}
Enter fullscreen mode Exit fullscreen mode

The above converters need to be registered with Spring, so write a small utility class to handle this.

@Slf4j
@Configuration
@RequiredArgsConstructor
public class ReactiveMariaDBConfig extends AbstractR2dbcConfiguration {

     @Value("${spring.r2dbc.username}")
     private String userName;

     @Value("${spring.r2dbc.password}")
     private String password;

     @Value("${spring.r2dbc.url}")
     private String url;

     private final ObjectMapper objectMapper;

     @Bean
     @Override
     public @NonNull MariadbConnectionFactory connectionFactory() {

          //r2dbc:mariadb://127.0.0.1:3306/customer
          String host = url.substring(url.indexOf("//") + 2, url.lastIndexOf(":"));
          int port = Integer.parseInt(url.substring(url.lastIndexOf(":") + 1, url.lastIndexOf("/")));
          String database = url.substring(url.lastIndexOf("/") + 1);

          log.debug("Setting data connectionFactory [host: {}, port: {}, database: {}]", host, port, database);

          return MariadbConnectionFactory.from(MariadbConnectionConfiguration.builder()
                  .password(password)
                  .username(userName)
                  .database(database)
                  .host(host)
                  .port(port)
                  .build());
     }

     @Bean
     @Override
     public @NonNull R2dbcCustomConversions r2dbcCustomConversions() {

          List<Converter<?, ?>> converters = new ArrayList<>();
          converters.add(new JsonNodeReadingConverter(objectMapper));
          converters.add(new StringToJsonConverter(objectMapper));
          return new R2dbcCustomConversions(getStoreConversions(), converters);

     }
}
Enter fullscreen mode Exit fullscreen mode

The AbstractR2dbcConfiguration class requires us to override the ConnectionFactory method, so this is extracting the spring.r2dbc property values and then creates the connection factory.
The real workhorse here is the R2dbcCustomerConversions class. Running with a break point we can see about 22 converters already registered in the getStoreConversions() call. This now adds our two custom converters to the list.

And that is how easy it is. The fact that you need to register these converters is hidden somewhere deep! With JPA you have the following option to register the custom converter:

  @Convert(converter = JsonNodeConverter.class)
  private JsonNode data;
Enter fullscreen mode Exit fullscreen mode

This annotation, however, does not work with R2DBC at the moment. We have to do the custom registration class to get this to work.

All this work makes it possible to run JSON_VALUE select statements on the data we have in the DB. Super handy on massive JSON payloads that you want to analyze later.
Sample SQL:

SELECT id,
 JSON_VALUE(ocr_data,'$.name.fullName' ) as fullName
FROM interview_response_data;
Enter fullscreen mode Exit fullscreen mode

From my original blog posts here:
SpringKodierer.

webflux Article's
24 articles in total
Favicon
Introducing Java Library for Backend Microservice Webflux (Reactor-core)
Favicon
Making reactive applications with a Kitten Care Example
Favicon
Reactive Programming applied to Legacy Services β€” A WebFlux example
Favicon
Getting Started with Spring WebFlux
Favicon
Implementing Soft Delete in Spring WebFlux with R2DBC
Favicon
Java library for developing backend with reactive programming
Favicon
How to Run an Asynchronous Task in Spring WebFlux Without Blocking the Main Response?
Favicon
How to Run a Method Asynchronously in a Reactive Chain in Spring WebFlux?
Favicon
Spring WebFlux Retry Mechanism
Favicon
Implementing Context Propagation in Reactive Programming Projects πŸ”„
Favicon
Ability to unlearn in Software: Reactive Programming
Favicon
Create DTO using get results from repository returns duplicate values in Spring Boot Reactive WebFlux
Favicon
Spring R2DBC, MariaDB, and JSON columns
Favicon
SpringBoot WebFlux Annotation-based RestAPIs
Favicon
SpringBoot WebFlux Functional RestAPIs
Favicon
Spring Webflux testing with Mockito
Favicon
A Short Example of Real-Time Event Streaming Using Spring WebFlux
Favicon
Global Error Handling In Webflux (Reactive World)
Favicon
Spring Webflux - Reactive Java Applications - Part 2
Favicon
Building an URL Shortening API with Spring WebFlux (and a lot of supporting cast)
Favicon
Spring Webflux - Reactive Java Applications - Part 1
Favicon
Spring Webflux - Aplicaçáes reativas em Java - Parte 1
Favicon
KVision v2.0.0 - with Bootstrap 4, Spring Webflux and lots of other improvements
Favicon
Sending Multipart Form Data Using Spring WebTestClient

Featured ones: