Data Integrity in NoSQL and Java Applications Using Bean Validation
Learn more about integrating bean validation in a NoSQL database while maintaining data integrity.
Join the DZone community and get the full member experience.
Join For FreeThe 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:
- A driver must have a license
- A driver must be older than eighteen and younger than one hundred fifty years old.
- A valid email contact
- At least one car to parking, however, it the storage maximum of five vehicles.
- To insurance policy, the car cannot be more expensive than one million dollars and not cheap to don't justify the system, less costly than one hundred dollars.
- The packing insurance works only with the dollar, thus, just the value in American currency.
- All car has a unique plate that is a sequence of three letters and then four numbers, e.g. ABC-1234
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:
A driver by the name
A driver by the car's plate
A driver by the color and the model both order by the most expensive one.
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.
Opinions expressed by DZone contributors are their own.
Comments