Data Integrity in NoSQL and Java Applications Using Bean Validation

BV logo

The NoSQL databases are famous because they are schemaless. That means a developer does need to care about a pre-defined type as it does on a relational database; thus, it makes it easier for developers to create an application. However, it has a considerable price once there is no structure that is not trivial to validate it on the database. To ensure integrity, the solution should work on the server side. There is a specification and framework in the Java world that is smoother and annotation-driven: bean validation. This post will cover how to integrate bean validation in a NoSQL database.

Bean Validation, JSR 380, is a specification which ensures that the properties of a Class match specific criteria, using annotations such as @NotNull, @Min, and @Max, also create its annotation and validation. It is worth noting bean validation does not, necessarily, mean an anemic model; in other words, a developer can use it with good practices to get data integrity.

The post will create a sample demo using MongoDB as a database and a Maven Java SE application with Eclipse JNoSQL once this framework is the first Jakarta EE specification to explore the integration between a NoSQL database and Bean Validation. The form will register the driver and a car in a smooth car app; therefore, the rules are:

As the first step, it will add the maven dependencies library. This project requires the Eclipse JNoSQL modules: Mapping layer, communication layer beyond, the communication MongoDB driver and the blade to active validation using Bean validation. The Jakarta EE dependency, in other words, the API and any implementation to CDI 2.0, Bean Validation, JSON-B, and JSON-P. Furthermore, money-API represent money.

<dependencies>
    <dependency>
        <groupId>org.jnosql.artemis</groupId>
        <artifactId>artemis-document</artifactId>
        <version>${project.version}</version>
    </dependency>
    <dependency>
        <groupId>org.jnosql.artemis</groupId>
        <artifactId>artemis-validation</artifactId>
        <version>${project.version}</version>
    </dependency>
    <dependency>
        <groupId>org.jnosql.diana</groupId>
        <artifactId>mongodb-driver</artifactId>
        <version>${project.version}</version>
    </dependency>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-validator</artifactId>
        <version>6.0.16.Final</version>
    </dependency>
    <dependency>
        <groupId>org.glassfish</groupId>
        <artifactId>javax.el</artifactId>
        <version>3.0.0</version>
    </dependency>
    <dependency>
        <groupId>org.javamoney.moneta</groupId>
        <artifactId>moneta-core</artifactId>
        <version>1.3</version>
    </dependency>
    <dependency>
        <groupId>org.jboss.weld.se</groupId>
        <artifactId>weld-se-shaded</artifactId>
        <version>${weld.se.core.version}</version>
    </dependency>
    <dependency>
        <groupId>javax.json</groupId>
        <artifactId>javax.json-api</artifactId>
        <version>${javax.json.version}</version>
    </dependency>
    <dependency>
        <groupId>javax.json.bind</groupId>
        <artifactId>javax.json.bind-api</artifactId>
        <version>${json.b.version}</version>
    </dependency>
    <dependency>
        <groupId>org.eclipse</groupId>
        <artifactId>yasson</artifactId>
        <version>${json.b.version}</version>
    </dependency>
    <dependency>
        <groupId>org.glassfish</groupId>
        <artifactId>javax.json</artifactId>
        <version>${javax.json.version}</version>
    </dependency>
</dependencies>


Bean validation has several constraints native. This means a developer can check if the value is blank, the size of either a number or a collection if the value is an email, and so on. Furthermore, it has the option to create a custom constraint that is useful. Once there is no support for validation, you can use the Money-API and system to check the minimum value, maximum value, and the currency accepted on the project.

Lucky for this article, the Money-API has a project that carries several utilitarian classes to make it more comfortable when using this API with Jakarta EE: The Midas project. So, it will extract the validations class from that project.

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Target( { METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = {MonetaryAmountAcceptedValidator.class})
@Documented
public @interface CurrencyAccepted {

    String message() default "{org.javamoney.midas.constraints.currencyAccepted}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

    String[] currencies() default "";

    String[] currenciesFromLocales() default "";
}

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Target( { METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = MonetaryAmountMaxValidator.class)
@Documented
public @interface MonetaryMax {

    String message() default "{org.javamoney.midas.constraints.monetaryMax}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    String value();
}

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Target( { METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = MonetaryAmountMinValidator.class)
@Documented
public @interface MonetaryMin {

    String message() default "{org.javamoney.midas.constraints.monetaryMin}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    String value();
}


The validation annotation has several critical annotations, and it includes the Constraint. It marks an annotation as being a bean validation constraint. The element validatedBy specifies the classes implementing the limitation. The post won't go more in-depth on all the annotations within the bean validation constraint, but you can use this link to get more details.

import javax.money.CurrencyUnit;
import javax.money.MonetaryAmount;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.ArrayList;
import java.util.List;

import static java.util.Collections.binarySearch;
import static java.util.Collections.sort;

public class MonetaryAmountAcceptedValidator implements ConstraintValidator<CurrencyAccepted, MonetaryAmount>{


    private final List<CurrencyUnit> currencies = new ArrayList<>();

    @Override
    public void initialize(CurrencyAccepted constraintAnnotation) {
        CurrencyReaderConverter reader = new CurrencyReaderConverter(constraintAnnotation);
        currencies.addAll(reader.getCurrencies());
        sort(currencies);
    }

    @Override
    public boolean isValid(MonetaryAmount value,
                           ConstraintValidatorContext context) {
        if (value == null) {
            return true;
        }
        return containsCurrency(value.getCurrency());
    }

    private boolean containsCurrency(CurrencyUnit value) {
        return binarySearch(currencies, value) >= 0;
    }
}


import javax.money.MonetaryAmount;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.math.BigDecimal;

public class MonetaryAmountMaxValidator implements ConstraintValidator<MonetaryMax, MonetaryAmount>{

   private BigDecimal number;

   @Override
   public void initialize(MonetaryMax constraintAnnotation) {
      number = new BigDecimal(constraintAnnotation.value());
   }

   @Override
   public boolean isValid(MonetaryAmount value,
                          ConstraintValidatorContext context) {
      if (value == null) {
         return true;
      }
      return value.isLessThanOrEqualTo(value.getFactory().setNumber(number).create());
   }
}

import javax.money.MonetaryAmount;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.math.BigDecimal;

public class MonetaryAmountMinValidator implements ConstraintValidator<MonetaryMin, MonetaryAmount>{
   private BigDecimal number;

   @Override
   public void initialize(MonetaryMin constraintAnnotation) {
      number = new BigDecimal(constraintAnnotation.value());
   }

   @Override
   public boolean isValid(MonetaryAmount value,
                          ConstraintValidatorContext context) {
      if (value == null) {
         return true;
      }
      return value.isGreaterThanOrEqualTo(value.getFactory().setNumber(number).create());
   }
}


With the validation annotation ready, the next step is to create model entities with the information about the driver and the car. To point out, MongoDB does not have native support to Money-API, and to solve this problem, it uses the @Convert annotation followed by an AttributeConverter implementation that uses the same concept of a convention that JPA has. It will be hidden on this post so don't get out of scope, but the GitHub repository has all the details.

import org.jnosql.artemis.Column;
import org.jnosql.artemis.Convert;
import org.jnosql.artemis.Entity;
import org.jnosql.artemis.demo.se.parking.converter.MonetaryAmountConverter;
import org.jnosql.artemis.demo.se.parking.validation.CurrencyAccepted;
import org.jnosql.artemis.demo.se.parking.validation.MonetaryMax;
import org.jnosql.artemis.demo.se.parking.validation.MonetaryMin;

import javax.money.MonetaryAmount;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import java.util.function.Supplier;

@Entity
public class Car {

    @Column
    @NotNull
    @Pattern(regexp = "[A-Z]{3}-[0-9]{4}", message = "Invalid car plate")
    private String plate;

    @Column
    @NotNull
    @MonetaryMin(value = "100", message = "There is not car cheap like that")
    @MonetaryMax(value = "1000000", message = "The parking does not support fancy car")
    @CurrencyAccepted(currencies = "USD", message = "The car price must work with USD")
    @Convert(MonetaryAmountConverter.class)
    private MonetaryAmount price;

    @Column
    @NotBlank
    private String model;

    @Column
    @NotBlank
    private String color;

    //hidden

}

import org.jnosql.artemis.Column;
import org.jnosql.artemis.Convert;
import org.jnosql.artemis.Entity;
import org.jnosql.artemis.Id;
import org.jnosql.artemis.demo.se.parking.converter.ObjectIdConverter;

import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Email;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

@Entity
public class Driver {


    @Id
    @Convert(ObjectIdConverter.class)
    private String id;

    @NotBlank(message = "Name cannot be null")
    @Column
    private String name;

    @AssertTrue(message = "A driver must have a license")
    @Column
    private boolean license;


    @Min(value = 18, message = "Age should not be less than 18")
    @Max(value = 150, message = "Age should not be greater than 150")
    @Column
    private int age;

    @Email(message = "Email should be valid")
    @NotNull
    @Column
    private String email;


    @Size(min = 1, max = 5, message = "It must have one car at least")
    @NotNull
    @Column
    private List<Car> cars;

  //hidden
}


The model is ready to persist on the MongoDB. To make it natural, the project will use the repository interface concept to use either query by a method and a query annotation to retrieve the information:

import org.jnosql.artemis.Param;
import org.jnosql.artemis.Query;
import org.jnosql.artemis.Repository;

import java.util.List;
import java.util.Optional;

public interface DriverRepository extends Repository<Driver, String> {

    Optional<Driver> findByName(String name);

    @Query("select * from Driver where cars.plate = @plate")
    Optional<Driver> findByPlate(@Param("plate") String name);

    @Query("select * from Driver where cars.color = @color order by cars.price.value desc")
    List<Driver> findByColor(@Param("color") String color);

    @Query("select * from Driver where cars.model = @model order by cars.price.value desc")
    List<Driver> findByModel(@Param("model") String color);

}


On the DriverRepository, beyond the query by method feature, there is a Query annotation that executes a standard query through the Eclipse JNoSQL query. There is this article that covers more about this topic. With the repository done, the last step on configuration is to create a connection to a MongoDB driver using the MongoDB implementation of DocumentCollectionManager.

import org.jnosql.diana.api.document.DocumentCollectionManager;
import org.jnosql.diana.api.document.DocumentCollectionManagerFactory;
import org.jnosql.diana.mongodb.document.MongoDBDocumentConfiguration;

import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Produces;

@ApplicationScoped
public class MongoDBProducer {

    private static final String COLLECTION = "developers";

    private MongoDBDocumentConfiguration configuration;

    private DocumentCollectionManagerFactory managerFactory;

    @PostConstruct
    public void init() {
        configuration = new MongoDBDocumentConfiguration();
        managerFactory = configuration.get();
    }


    @Produces
    public DocumentCollectionManager getManager() {
        return managerFactory.get(COLLECTION);

    }

}


Now, we just left the client creation to consume the entities and persist on the database. To this smooth project, the client will have a pure executable class. The first client class will try to endure an entity with several invalid data.


import javax.enterprise.inject.se.SeContainer;
import javax.enterprise.inject.se.SeContainerInitializer;

public class App {


    public static void main(String[] args) {

        try (SeContainer container = SeContainerInitializer.newInstance().initialize()) {
            DriverRepository repository = container.select(DriverRepository.class).get();
            //an invalid driver it will return an exception
            repository.save(new Driver());

        }
    }

    private App() {
    }
}


The result is a constraint exception from bean validation:

Caused by: javax.validation.ConstraintViolationException: age: Age should not be less than 18, name: Name cannot be null, cars: must not be null, license: A driver must have a license, email: must not be null


The second and last piece of code on this article will create valid data, persist, and then execute the retrieve method through the DriverRepository interface.

import org.javamoney.moneta.Money;

import javax.enterprise.inject.se.SeContainer;
import javax.enterprise.inject.se.SeContainerInitializer;
import javax.money.CurrencyUnit;
import javax.money.Monetary;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;

public class App1 {


    public static void main(String[] args) {

        try (SeContainer container = SeContainerInitializer.newInstance().initialize()) {
            DriverRepository repository = container.select(DriverRepository.class).get();
            CurrencyUnit usd = Monetary.getCurrency(Locale.US);

            Car ferrari = Car.builder().withPlate("BRL-1234")
                    .withModel("812 Superfast")
                    .withColor(Color.RED)
                    .withPrice(Money.of(315_000, usd))
                    .build();

            Car mustang = Car.builder().withPlate("OTA-7659")
                    .withModel("812 Superfast")
                    .withColor(Color.RED)
                    .withPrice(Money.of(55_000, usd))
                    .build();

            Driver michael = Driver.builder().withAge(35)
                    .withCars(Arrays.asList(ferrari))
                    .withEmail("michael@ferrari.com")
                    .withLicense(true)
                    .withName("Michael Schumacher").build();

            Driver rubens = Driver.builder().withAge(25)
                    .withCars(Arrays.asList(mustang))
                    .withEmail("rubens@mustang.com")
                    .withLicense(true)
                    .withName("Rubens Barrichello").build();

            repository.save(michael);
            repository.save(rubens);

            System.out.println("Find by Color");
            repository.findByColor(Color.RED.get()).forEach(System.out::println);
            System.out.println("Find by Model");
            repository.findByModel("812 Superfast").forEach(System.out::println);
            System.out.println("Find by Name");
            repository.findByName("Rubens Barrichello").ifPresent(System.out::println);
            System.out.println("Find by Plate");
            repository.findByPlate("BRL-1234").ifPresent(System.out::println);

        }
    }

    private App1() {
    }
}


This post demonstrates a successful strategy for maintaining data integrity on a Java application with NoSQL, bean validation, and Eclipse JNoSQL. It is worthwhile to mention that the integration using the first Jakarta EE specification will work in any communication driver. Therefore, in several NoSQL databases that do not have any kind of validation, and even MongoDB which has some constraints, it does not support multiple validations as smoothly as bean validation. The complete source solution is in this GitHub repository.

 

 

 

 

Top