Validar objetos con Spring Validation, ejemplo registros de jOOQ

Escrito por el , actualizado el .
blog-stack java planeta-codigo programacion
Comentarios

jOOQ es una alternativa a Hibernate que devuelve la base de datos a primer plano en una aplicación. Por la importancia en cualquier aplicación de los datos propone que la base de datos sea la única fuente de la verdad. Para los modelos es patente al generarse a partir del esquema de la base de datos para las validaciones se puede hacer con restricciones de integridad pero si queremos conocer los errores detalladamente que se produzcan puede que deseemos usar Spring Validation.

Java

En la documentación de jOOQ no he encontrado nada referente a cómo realizar validaciones antes de guardar los datos en la base de datos. Pero con la herramienta de generación se puede indicar que las clases Java generadas se anoten con las anotaciones de validación de la especificación JSR-303 según las restricciones encontradas en la base de datos. Las clases contendrán validaciones básicas como @NotNull o @Size pero no he visto nada más allá de estas simples validaciones. Si tenemos validaciones dependientes entre campos o más complejas como una expresión regular necesitaremos algo más.

Si queremos ser puristas las validaciones deberíamos hacerlas en la base de datos usando constraints impidiendo de esta manera que se guarden datos inválidos independientemente de la aplicación o microservicio que intente guardar algo en la base de datos. Sin embargo, realizando solo las validaciones en la base de datos puede que perdamos qué campo o campos son erróneos y los motivos por los que son erróneos, información que seguramente nos interese para indicar detalladamente los datos no válidos al usuario permitiéndole corregirlos.

Usando jOOQ como alternativa a Hibernate deberemos realizar las validaciones nosotros de alguna forma. jOOQ aboga por que la base de datos sea la única fuente de la verdad, claramente es así para los modelos o records de datos que se generan a partir de las tablas y la relaciones entre ellas. Deberemos tener en cuenta los problemas de tener dos fuentes de la verdad o de realizar las validaciones a nivel de la aplicación en vez de en la base de datos pero si así lo deseamos podemos usar Spring Validation.

Con Spring Validation tenemos diferentes formas de realizar las validaciones, dos de ellas son con las anotaciones de la especificación de validación JSR-303 o implementando una clase de la interfaz Validator. En el siguiente ejemplo se muestra una combinación de ambos, un Validator que valida primeramente las anotaciones de JSR-303 para una clase record de jOOQ y posteriormente unas validaciones adicionales propias de la aplicación u en otro caso para campos campos dependientes. A través de las clases DataBinder y el SpringValidationAdapter validaremos un objeto con las anotaciones de javax.validation y las propias de Spring además de adaptar los errores a la interfaz Errors con la que inspeccionaremos los errores. Perfectamente podemos usar únicamente los Validator de Spring sin tener en cuenta las anotaciones de javax.validation, nótese también que podemos implementar múltiples validadores de Spring con diferentes criterios de validación.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package io.github.picodotdev.blogbitix.springboot.validator;
 
import java.time.LocalDateTime;

import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

import io.github.picodotdev.blogbitix.springboot.jooq.tables.interfaces.IEmployee;

@Component
public class EmployeeValidator implements Validator {
 
    public boolean supports(Class<?> clazz) {
        return IEmployee.class.isAssignableFrom(clazz);
    }
 
    public void validate(Object target, Errors errors) {
        if (!supports(target.getClass())) {
            return;
        }
        IEmployee o = (IEmployee) target;
        if (o.getBirthday() != null && o.getBirthday().isAfter(LocalDateTime.now())) {
            errors.rejectValue("birthday", "invalid", new Object[]{ o.getBirthday() }, "birthday cannot be after now");
        }
    }
}

En un determinado servicio donde implementaremos la lógica de negocio realizaremos la validación de forma explícita antes de guardar el registro, para ello inyectaremos en el servicio la referencia al validador de Spring con la anotación Autowired y usaremos uno de sus métodos sobrecargados validate para que compruebe las validaciones que haya definidas para ese registro.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
package io.github.picodotdev.blogbitix.springboot.service;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import org.jooq.DSLContext;
import org.jooq.Record;
import org.springframework.validation.DataBinder;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;

import io.github.picodotdev.blogbitix.springboot.jooq.Tables;
import io.github.picodotdev.blogbitix.springboot.jooq.tables.records.DepartmentRecord;
import io.github.picodotdev.blogbitix.springboot.jooq.tables.records.EmployeeRecord;
import io.github.picodotdev.blogbitix.springboot.misc.RecordContainer;
import io.github.picodotdev.blogbitix.springboot.validator.EmployeeValidator;

public class DefaultAppService implements AppService {

        private DSLContext context;
        private javax.validation.Validator validator;
        private List<Validator> validators;

        public DefaultAppService(DSLContext context, javax.validation.Validator validator, EmployeeValidator employeeValidator) {
            this.context = context;
            this.validator = validator;
            this.validators = new ArrayList<Validator>();
            this.validators.add(employeeValidator);
        }

        @Override
        public long countEmployees() {
            return context.selectCount().from(Tables.EMPLOYEE).fetchOneInto(Long.class);
        }

        @Override
        public long countDepartments() {
            return context.selectCount().from(Tables.DEPARTMENT).fetchOneInto(Long.class);
        }
        
        @Override
        public DepartmentRecord findDepartment(long id) {
            return context.select().from(Tables.DEPARTMENT).where(Tables.DEPARTMENT.ID.eq(id)).fetchOneInto(DepartmentRecord.class);
        }

        @Override
        public Errors validate(Object object) {
            List<Validator> supportedValidators = validators.stream().filter((Validator v) -> {
                return v.supports(object.getClass());
            }).collect(Collectors.toList());

            DataBinder binder = new DataBinder(object);
            binder.addValidators(new SpringValidatorAdapter(validator));
            binder.addValidators(supportedValidators.toArray(new Validator[0]));
            binder.validate();
            return binder.getBindingResult();
        }

        @Override
        public List<RecordContainer> findDepartmentEmployees(Long id) {
            return context.select().from(Tables.DEPARTMENT).join(Tables.EMPLOYEE_DEPARTMENT).on(Tables.DEPARTMENT.ID.eq(Tables.EMPLOYEE_DEPARTMENT.DEPARTMENT_ID))
                .join(Tables.EMPLOYEE).on(Tables.EMPLOYEE.ID.eq(Tables.EMPLOYEE_DEPARTMENT.EMPLOYEE_ID)).where(Tables.DEPARTMENT.ID.eq(id))
                .fetch((Record record) -> {
                    RecordContainer container = new RecordContainer();
                    container.setEmployee(record.into(EmployeeRecord.class));
                    container.setDepartment(record.into(DepartmentRecord.class));
                    return container;
            });
        }
        
        @Override
        public List<RecordContainer> findDepartmentsEmployees() {
            return context.select().from(Tables.DEPARTMENT).join(Tables.EMPLOYEE_DEPARTMENT).on(Tables.DEPARTMENT.ID.eq(Tables.EMPLOYEE_DEPARTMENT.DEPARTMENT_ID))
                .join(Tables.EMPLOYEE).on(Tables.EMPLOYEE.ID.eq(Tables.EMPLOYEE_DEPARTMENT.EMPLOYEE_ID))
                .fetch((Record record) -> {
                    RecordContainer container = new RecordContainer();
                    container.setEmployee(record.into(EmployeeRecord.class));
                    container.setDepartment(record.into(DepartmentRecord.class));
                    return container;
            });
        }
}

Realmente aunque el ejemplo muestra como validar un objeto de tipo Record de jOOQ lo mismo puede ser aplicado para validar cualquier otro tipo de objeto Java con Spring Validation. Las clases record generadas por jOOQ implementan una interfaz en la que se exponen los métodos get y set con las columnas de la tabla.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
/**
 * This class is generated by jOOQ
 */
package io.github.picodotdev.blogbitix.springboot.jooq.tables.interfaces;


import java.io.Serializable;
import java.time.LocalDateTime;

import javax.annotation.Generated;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;


/**
 * This class is generated by jOOQ.
 */
@Generated(
    value = {
        "http://www.jooq.org",
        "jOOQ version:3.7.0"
    },
    comments = "This class is generated by jOOQ"
)
@SuppressWarnings({ "all", "unchecked", "rawtypes" })
public interface IEmployee extends Serializable {

    /**
     * Setter for <code>JOOQ.EMPLOYEE.ID</code>.
     */
    public void setId(Long value);

    /**
     * Getter for <code>JOOQ.EMPLOYEE.ID</code>.
     */
    @NotNull
    public Long getId();

    /**
     * Setter for <code>JOOQ.EMPLOYEE.NAME</code>.
     */
    public void setName(String value);

    /**
     * Getter for <code>JOOQ.EMPLOYEE.NAME</code>.
     */
    @NotNull
    @Size(max = 256)
    public String getName();

    /**
     * Setter for <code>JOOQ.EMPLOYEE.SURNAME</code>.
     */
    public void setSurname(String value);

    /**
     * Getter for <code>JOOQ.EMPLOYEE.SURNAME</code>.
     */
    @Size(max = 256)
    public String getSurname();

    /**
     * Setter for <code>JOOQ.EMPLOYEE.BIRTHDAY</code>.
     */
    public void setBirthday(LocalDateTime value);

    /**
     * Getter for <code>JOOQ.EMPLOYEE.BIRTHDAY</code>.
     */
    public LocalDateTime getBirthday();

    // -------------------------------------------------------------------------
 // FROM and INTO
 // -------------------------------------------------------------------------

    /**
     * Load data from another generated Record/POJO implementing the common interface IEmployee
     */
    public void from(io.github.picodotdev.blogbitix.springboot.jooq.tables.interfaces.IEmployee from);

    /**
     * Copy data into another generated Record/POJO implementing the common interface IEmployee
     */
    public <E extends io.github.picodotdev.blogbitix.springboot.jooq.tables.interfaces.IEmployee> E into(E into);
}

En la salida en al terminal de la aplicación de ejemplo con Spring Boot se observa que se producen varios errores de validación para un objeto empleado, al validarlo le falta un valor para los campos id, name por restricciones de base de datos detectadas con las anotaciones @NotNull y al tener una fecha posterior a la actual también falla una validación propia de la aplicación. El campo id lo asignará jOOQ cuando se guarde en base de datos por lo que esta validación no deberemos tenerla en cuenta al guardar registros nuevos.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package io.github.picodotdev.blogbitix.springboot;

import java.time.LocalDateTime;
import java.util.List;

import org.jooq.DSLContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.validation.Errors;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;

import io.github.picodotdev.blogbitix.springboot.jooq.Keys;
import io.github.picodotdev.blogbitix.springboot.jooq.Tables;
import io.github.picodotdev.blogbitix.springboot.jooq.tables.records.DepartmentRecord;
import io.github.picodotdev.blogbitix.springboot.jooq.tables.records.EmployeeDepartmentRecord;
import io.github.picodotdev.blogbitix.springboot.jooq.tables.records.EmployeeRecord;
import io.github.picodotdev.blogbitix.springboot.misc.RecordContainer;
import io.github.picodotdev.blogbitix.springboot.service.AppService;

@SpringBootApplication
public class Main implements CommandLineRunner {

    @Autowired
    private DSLContext context;

    @Autowired
    private AppService service;

    @Override
    public void run(String... args) {
        System.out.printf("Number employees: %d%n", service.countEmployees());
        System.out.printf("Number departments: %d%n", service.countDepartments());

        System.out.println();
        System.out.println("# Relations (with 1+N problem)");
        DepartmentRecord department = service.findDepartment(1l);
        List<EmployeeDepartmentRecord> eds = department.fetchChildren(Keys.DEPARTMENT_ID);
        eds.stream().forEach(ed -> {
            EmployeeRecord employee = ed.fetchParent(Keys.EMPLOYEE_ID);
            System.out.printf("%s %s%n", employee.getName(), department.getName());
        });

        System.out.println();
        System.out.println("# Multipletables (no 1+N)");
        List<RecordContainer> data = service.findDepartmentEmployees(1l);
        data.stream().forEach((RecordContainer c) -> {
            System.out.printf("%s %s%n", c.getEmployee().getName(), c.getDepartment().getName());
        });

        System.out.println();
        System.out.println("# Validation");
        EmployeeRecord employee = context.newRecord(Tables.EMPLOYEE);
        employee.setBirthday(LocalDateTime.now().plusDays(1));
        Errors errors = service.validate(employee);
        errors.getFieldErrors().stream().forEach(error -> {
            System.out.printf("%s, %s, %s%n", error.getField(), error.getCode(), error.getArguments(), error.getRejectedValue());
        });
        errors.getGlobalErrors().stream().forEach(error -> {
            System.out.printf("%s, %s, %s%n", error.getObjectName(), error.getCode(), error.getArguments());
        });
    }

    public static void main(String[] args) throws Exception {
        SpringApplication application = new SpringApplication(Main.class);
        // application.setApplicationContextClass(AnnotationConfigApplicationContext.class);
        application.setApplicationContextClass(AnnotationConfigWebApplicationContext.class);
        SpringApplication.run(Main.class, args);
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Number employees: 2
Number departments: 2

# Relations (1+N)
Epi IT
Blas IT

# Multipletables (no 1+N)
Epi IT
Blas IT

# Validation
id, NotNull, [Ljava.lang.Object;@4052c8c2
name, NotNull, [Ljava.lang.Object;@181b8c4b
birthday, invalid, [Ljava.lang.Object;@38eb0f4d

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando el comando ./gradlew updateDatabase, ./gradlew generateModels, ./gradlew run.