Cómo Spring Data Jdbc une tablas

En esta publicación, veremos cómo Spring Data Jdbc crea consultas SQL para recuperar entidades relacionadas.



La publicación está diseñada para programadores novatos y no contiene cosas muy complicadas.





Los invito a todos al día de demostración del curso online “Java Developer. Profesional " . Como parte del evento, te contaré en detalle sobre el programa del curso, así como también responderé tus preguntas.





Una gran parte de la solución como Hibernate se usa porque esto es muy conveniente para trabajar con objetos anidados.



Por ejemplo, hay una clase RecordPackage, uno de los campos de esta clase es una colección de objetos secundarios (o anidados): registros.



Si usa Jdbc, tendrá que escribir mucho código de rutina. A pocas personas les gusta, que es en parte la razón por la que usan Hiberhate.



En Hibernate, puede llamar a un método a la vez para obtener un RecordPackage con todos sus registros secundarios.



Por un lado, quiero usar un método para obtener todo el objeto y, por otro lado, no quiero meterme con el monstruo Hibernate.



Spring Data Jdbc le permite obtener lo mejor de estos dos mundos (o al menos algo aceptable).



Considere dos casos:



  • relación uno a muchos
  • relación uno a uno




Son estas conexiones las que se encuentran con mayor frecuencia en la práctica.



El código completo de ejemplos se puede encontrar en GitHub, aquí daré solo el mínimo.

En primer lugar, vale la pena señalar que Spring Data Jdbc no es una herramienta mágica para resolver ningún problema. Ciertamente tiene sus inconvenientes y limitaciones.

Sin embargo, para una serie de tareas típicas, esta es una solución perfectamente adecuada.



Relación uno a muchos



Como ejemplo real, puede considerar: el encabezado de un paquete de algunos datos y las líneas de datos incluidas en este paquete. Por ejemplo, el archivo es un paquete y las líneas de archivo son líneas de datos que se incluyen en ese paquete.



La estructura de las tablas es la siguiente:



create table record_package
(
    record_package_id bigserial    not null
        constraint record_package_pk primary key,
    name              varchar(256) not null
);

create table record
(
    record_id         bigserial    not null
        constraint record_pk primary key,
    record_package_id bigint       not null,
    data              varchar(256) not null
);

alter table record
    add foreign key (record_package_id) references record_package;




dos tablas: record_package(encabezado de un paquete determinado) y record(registros incluidos en el paquete).

Cómo se muestra esta relación en el código java:



@Table("record_package")
public class RecordPackage {
    @Id
    private final Long recordPackageId;
    private final String name;

    @MappedCollection(idColumn = "record_package_id")
    private final Set<Record> records;
….
}




Aquí estamos interesados ​​en definir una relación de uno a varios. Esto está codificado mediante anotación @MappedCollection.



Esta anotación tiene dos parámetros:



idColumn : el campo mediante el cual se realiza la conexión

; keyColumn : el campo mediante el cual se ordenan los registros en la tabla secundaria.



Vale la pena mencionar este pedido por separado. En este ejemplo, no nos importa en qué orden se insertarán los registros secundarios en la tabla de registros, pero en algunos casos puede ser importante. Para tal orden, la tabla de registros tendrá un campo como record_no, y este es el campo que deberá escribirse en la keyColumn de la anotación MappedCollection. Cuando ejecute insertar, Spring Data Jdbc generará los valores de este campo. Además de la anotación, Establecer <registro >deberá reemplazarse con Lista<Record >, que es bastante lógico y comprensible. La secuencia explícitamente especificada de filas secundarias se tendrá en cuenta al formar la selección, pero volveremos a esto más adelante.



Entonces, hemos identificado las conexiones y estamos listos para probarlo.



Creamos entidades relacionadas y las obtenemos de la base:



  var record1 = new Record("r1");
  var record2 = new Record("r2");
  var record3 = new Record("r3");

   var recordPackage = new RecordPackage( "package", Set.of(record1, record2, record3));
   var recordPackageSaved = repository.save(recordPackage);

   var recordPackageLoaded = repository.findById(recordPackageSaved.getRecordPackageId());




Tenga en cuenta que solo necesitamos llamar a un método repository.findByIdpara obtener una instancia RecordPackagecon una colección completa de registros.



Por supuesto, estamos interesados ​​en qué tipo de consulta SQL se ejecutó para obtener los registros de la colección anidada.



Comparado con Hibernate, Spring Data Jdbc es bueno por su simplicidad. Se puede depurar fácilmente para revelar los puntos principales.



Después de una pequeña investigación en el paquete org.springframework.data.jdbc.core.convert , encontramos la clase DefaultDataAccessStrategy . Esta clase es responsable de generar consultas SQL basadas en la información de la clase. Ahora en esta clase estamos interesados ​​en el método
Iterable <Objeto> findAllByPath




O más precisamente la línea:



String findAllByProperty = sql(actualType) 
    .getFindAllByProperty(identifier, path.getQualifierColumn(), path.isOrdered());


Aquí, la consulta SQL requerida se recupera de la caché interna.



En nuestro caso, se ve así:



SELECT "record"."data" AS "data", "record"."record_id" AS "record_id", "record"."record_package_id" AS "record_package_id" 
FROM "record" 
WHERE "record"."record_package_id" = :record_package_id


Todo es claro y predecible.



¿Cómo se vería si usáramos el orden de registros en la tabla secundaria? Obviamente, se requeriría ordenar por.



Pasemos a la clase BasicRelationalPersistentProperty del paquete org.springframework.data.relational.core.mapping. Esta clase tiene un método que determina si agregar orden a la consulta o no.



	
public boolean isOrdered() {
  return isListLike();
}


y



private boolean isListLike() {
  return isCollectionLike() && !Set.class.isAssignableFrom(this.getType());
}




isCollectionLike verifica que realmente tenemos una "colección" (incluida una matriz).

Y desde la condición ! Set.class.isAssignableFrom (this.getType ()); queda claro que Set no se usa por casualidad, sino para excluir una clasificación innecesaria. Y algún día usaremos List intencionalmente para habilitar la clasificación.



Creo que uno a muchos es más o menos claro, pasemos al siguiente caso.



Relación uno a uno



Digamos que tenemos tal estructura.



create table info_main
(
    info_main_id bigserial    not null
        constraint info_pk primary key,
    main_data    varchar(256) not null
);

create table info_additional
(
    info_additional_id bigserial    not null
        constraint additional_pk primary key,
    info_main_id       bigint       not null,
    additional_data    varchar(256) not null
);

alter table info_additional
    add foreign key (info_main_id) references info_main;


Hay una tabla con información básica sobre un determinado objeto (info_main) y hay información adicional (info_additional).



¿Cómo se puede representar esto en el código?



@Table("info_main")
public class InfoMain {
    @Id
    private final Long infoMainId;
    private final String mainData;

    @MappedCollection(idColumn = "info_main_id")
    private final InfoAdditional infoAdditional;
}




A primera vista, parece el primer caso de uno a muchos, pero hay una diferencia. Esta vez, el niño es realmente un objeto, no una colección como en el caso anterior.



El código de prueba tiene este aspecto:



  var infoAdditional = new InfoAdditional("InfoAdditional");

  var infoMain = new InfoMain("mainData", infoAdditional);

  var infoMainSaved = repository.save(infoMain);
  var infoMainLoaded = repository.findById(infoMainSaved.getInfoMainId());


Veamos qué expresión sql se genera esta vez. Para hacer esto, desenterraremos el método findById en la ubicación:



Paquete org.springframework.data.jdbc.core.convert clase DefaultDataAccessStrategy . Ya estamos familiarizados con esta clase, ahora estamos interesados ​​en el método.



public <T> T findById(Object id, Class<T> domainType)




Vemos que la siguiente solicitud se recupera de la caché:



SELECT "info_main"."main_data" AS "main_data", "info_main"."info_main_id" AS "info_main_id", "infoAdditional"."info_main_id" AS "infoadditional_info_main_id", "infoAdditional"."additional_data" AS "infoadditional_additional_data", "infoAdditional"."info_additional_id" AS "infoadditional_info_additional_id" 
FROM "info_main" 
LEFT OUTER JOIN "info_additional" "infoAdditional" 
ON "infoAdditional"."info_main_id" = "info_main"."info_main_id" 
WHERE "info_main"."info_main_id" = :id




En este momento, la unión externa izquierda nos conviene, pero ¿y si no? ¿Cómo obtengo una unión interna?

La creación de uniones funcionales se encuentra en el paquete org.springframework.data.jdbc.core.convert , clase SqlGenerator , método:
private SelectBuilder.SelectWhere selectBuilder(Collection<SqlIdentifier> keyColumns)




Estamos interesados ​​en este fragmento:



		
for (Join join : joinTables) {
  baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId);
}




Si necesita unir tablas, hay una opción solo con una combinación externa izquierda.

Parece que aún no se puede realizar la unión interna.



Conclusión



Hemos considerado dos casos más típicos de cómo puede unir tablas en Spring Data Jdbc.

En principio, la funcionalidad que tenemos actualmente es bastante adecuada para resolver problemas prácticos, aunque existen limitaciones no críticas.



El texto completo del ejemplo se puede encontrar aquí .



Y aquí hay una versión en video de esta publicación.






All Articles