Obtener datos de múltiples tablas con jOOQ

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

jOOQ no proporciona la misma transparencia de acceso a una base de datos relacional que un ORM. Para validar los datos podemos usar Spring Validation y para obtener datos de múltiples tablas lo que comento en este artículo. Usando jOOQ podemos exprimir todo el potencial del lenguaje SQL, comprobación de tipos por el compilador de argumentos y resultados, usar la base de datos como única fuente de la verdad, diferentes formas de acceso a la base de datos usando el patrón Active Record, directamente SQL, …

Java

Una de las facilidades que proporciona la librería de persistencia Hibernate usada ampliamente en aplicaciones Java como buen ORM es la navegación de las relaciones a través de los métodos del modelo de objetos de forma transparente a las consultas SQL que se necesiten lanzar a la base de datos para ir obteniendo los resultados. Sin embargo, pensar únicamente en este modelo orientado a objetos o abusar de él sin tener en cuenta el número de consultas SQL que estamos realizando al modelo relacional provocará que la aplicación sea lenta, poco eficiente y sobrecargue el servidor de base de datos. Como ejemplo usando un ORM es habitual provocar un problema de N+1 o 1+N que deberemos detectar y corregir.

Por el contrario jOOQ se postula como alternativa a Hibernate para proporcionar persistencia en base de datos relacionales. Se basa en proporcionar un acceso usando un DSL más cercano al lenguaje SQL de la base de datos en vez de proporcionar una capa de abstracción para el modelo de objetos, la forma de acceder a la base de datos es muy flexible pudiéndose emplear para generar consultas SQL en forma de String que lanzaremos con JDBC o con la clase JdbcTemplate de Spring. Las SQL son construidas con una API en forma de DSL o usando el patrón ActiveRecord con la posibilidad de que el compilador realice la validación de tipos tanto para los parámetros como para los resultados.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Main.java
System.out.printf("Number employees: %d%n", service.countEmployees());
System.out.printf("Number departments: %d%n", service.countDepartments());

// DefaultServiceImpl.java
@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);
}

Sin embargo, aunque jOOQ permite también navegar las relaciones entre las entidades implementadas con el patrón ActiveRecord puede sucedernos que se nos presente el mismo problema 1+N de Hibernate si por ejemplo obtenemos una lista departamentos con 1 SQL y posteriormente 1 consulta más para obtener los empleados según se itera cada departamento en un bucle, en total 1+N consultas para los departamentos y sus empleados. Como sería el siguiente caso.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Main.java
System.out.println();
System.out.println("# Relations (1+N)");
DepartmentRecord department = service.findDepartment(1l);
List<EmployeeDepartmentRecord> eds = department.fetchChildren(Keys.DEPARTMENT_ID);
for (EmployeeDepartmentRecord ed : eds) {
    EmployeeRecord employee = ed.fetchParent(Keys.EMPLOYEE_ID);    
    System.out.printf("%s %s%n", employee.getName(), department.getName());
}

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

Si sabemos que vamos a necesitar una entidad y las relacionadas, como en un departamento y sus empleados, es mejor obtener todos los datos en una única consulta. Una de las formas en que jOOQ devuelve resultados es a través de objetos Record que representa a los datos de resultado de la SQL, por otro lado jOOQ genera un objeto Record por cada tabla de la base de datos. Si en una consulta necesitamos únicamente los datos de una tabla podemos obtener los datos en el ActiveRecord que jOOQ genera para esa tabla. Si queremos obtener datos de múltiples tablas deberemos emplear otra forma, por ejemplo, podemos recoger los resultados en un objeto de tipo Record genérico y posteriormente extraer los datos a los diferentes Record concretos de la aplicación.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// Main.java
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());
});

// DefaultAppService.java
@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;
    });
}

El objeto RecordContainer es el siguiente, con una propiedad por cada posible Record que pudiese recuperar. Solo necesitaremos crear uno que incluya una propiedad con todos los posibles Record que necesitemos.

 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
package io.github.picodotdev.blogbitix.springboot.misc;

import io.github.picodotdev.blogbitix.springboot.jooq.tables.records.DepartmentRecord;
import io.github.picodotdev.blogbitix.springboot.jooq.tables.records.EmployeeRecord;

public class RecordContainer {

    private EmployeeRecord employee;
    private DepartmentRecord department;

    public RecordContainer() {        
    }
    
    public EmployeeRecord getEmployee() {
        return employee;
    }
    
    public void setEmployee(EmployeeRecord employee) {
        this.employee = employee;
    }
    
    public String getEmployeeName() {
        return employee.getName();
    }

    public DepartmentRecord getDepartment() {
        return department;
    }

    public void setDepartment(DepartmentRecord department) {
        this.department = department;
    }
    
    public String getDepartmentName() {
        return department.getName();
    }
}

Las clases completas Main.java y AppServiceImpl.java son las siguientes.

 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);
        for (EmployeeDepartmentRecord ed : eds) {
            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
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;
            });
        }
}

La salida en la terminal de esta aplicación al iniciarse es la siguiente:

 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

jOOQ presenta varias cosas interesantes sobre Hibernate, ya es en una alternativa con un enfoque diferente, con ideas interesantes, algunas ventajas y el tiempo dirá si se convierte en el nuevo estándar para la persistencia en las aplicaciones Java. Otra de las cosas comunes que necesitaremos en una aplicación es validar los objetos Record, una posibilidad es usando Spring Validation.

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.