Como Spring Data Jdbc une tabelas

Neste post, vamos dar uma olhada em como Spring Data Jdbc constrói consultas sql para recuperar entidades relacionadas.



A postagem foi projetada para programadores novatos e não contém coisas super complicadas.





Convido a todos para o dia de demonstração do curso online “Desenvolvedor Java. Profissional " . Como parte do evento, contarei detalhes sobre o programa do curso, bem como responderei suas dúvidas.





Uma parte da solução como o Hibernate é usada, porque isso é muito conveniente para trabalhar com objetos aninhados.



Por exemplo, existe uma classe RecordPackage, um dos campos desta classe é uma coleção de objetos filho (ou aninhados): registros.



Se você usar Jdbc, terá que escrever muitos códigos de rotina. Poucas pessoas gostam, em parte por isso usam o Hiberhate.



No Hibernate, você pode chamar um método de uma vez para obter um RecordPackage com todos os seus registros filhos.



Por um lado, quero usar um método para obter o objeto inteiro e, por outro lado, não quero mexer com o monstro do Hibernate.



Spring Data Jdbc permite que você obtenha o melhor dos dois mundos (ou pelo menos algo aceitável).



Considere dois casos:



  • relação um-para-muitos
  • relacionamento um para um




São essas conexões que são encontradas com mais frequência na prática.



O código completo dos exemplos pode ser encontrado no GitHub, aqui darei apenas o mínimo.

Em primeiro lugar, é importante notar que Spring Data Jdbc não é uma ferramenta mágica para resolver nenhum problema. Certamente tem suas desvantagens e limitações.

No entanto, para uma série de tarefas típicas, esta é uma solução perfeitamente adequada.



Relacionamento um para muitos



Como um exemplo real, você pode considerar: o cabeçalho de um pacote de alguns dados e as linhas de dados incluídas neste pacote. Por exemplo, arquivo é um pacote e linhas de arquivo são linhas de dados que vão para esse pacote.



A estrutura das tabelas é a seguinte:



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;




duas tabelas: record_package(cabeçalho de um determinado pacote) e record(registros incluídos no pacote).

Como essa relação é exibida no 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;
….
}




Aqui, estamos interessados ​​em definir uma relação um-para-muitos. Isso é codificado usando anotação @MappedCollection.



Essa anotação tem dois parâmetros:



idColumn - o campo pelo qual a conexão é feita

; keyColumn - o campo pelo qual os registros na tabela filho são ordenados.



Vale a pena mencionar essa ordem separadamente. Neste exemplo, não importa para nós a ordem em que os registros filhos serão inseridos na tabela de registros, mas em alguns casos pode ser importante. Para tal ordenação, a tabela de registros terá um campo como record_no, é este campo que deverá ser escrito na keyColumn da anotação MappedCollection. Quando você executa a inserção, Spring Data Jdbc irá gerar os valores deste campo. Além da anotação, Set <Record >deverá ser substituído por List<Registro >, o que é bastante lógico e compreensível. A sequência explicitamente especificada de linhas filho também será levada em consideração ao formar o select, mas voltaremos a isso mais tarde.



Portanto, identificamos as conexões e estamos prontos para testá-las.



Criamos entidades relacionadas e as obtemos da 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());




Observe que só precisamos chamar um método repository.findByIdpara obter uma instância RecordPackagecom uma coleção de registros preenchida.



Obviamente, estamos interessados ​​em que tipo de consulta sql foi executada para obter os registros da coleção aninhada.



Comparado ao Hibernate, Spring Data Jdbc é bom por sua simplicidade. Ele pode ser facilmente depurado para revelar os pontos principais.



Após uma pequena investigação no pacote org.springframework.data.jdbc.core.convert , encontramos a classe DefaultDataAccessStrategy . Esta classe é responsável por gerar consultas SQL com base nas informações da classe. Agora, nesta aula, estamos interessados ​​no método
Iterable <Object> findAllByPath




E mais precisamente a linha:



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


Aqui, a consulta SQL necessária é recuperada do cache interno.



No nosso caso, é assim:



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


Tudo é claro e previsível.



Como seria se usássemos a ordem dos registros na tabela filho? Obviamente, seria necessário fazer o pedido.



Vamos prosseguir para a classe BasicRelationalPersistentProperty do pacote org.springframework.data.relational.core.mapping. Essa classe tem um método que determina se deve ou não adicionar o pedido por à consulta.



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


e



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




isCollectionLike verifica se realmente temos uma "coleção" (incluindo uma matriz).

E da condição ! Set.class.isAssignableFrom (this.getType ()); fica claro que Set não é usado por acaso, mas para excluir classificação desnecessária. E algum dia usaremos intencionalmente List para permitir a classificação.



Acho que um-para-muitos é mais ou menos claro, vamos passar para o próximo caso.



Relacionamento um-para-um



Digamos que temos essa estrutura.



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;


Existe uma tabela com informações básicas sobre um determinado objeto (info_main) e há informações adicionais (info_additional).



Como isso pode ser representado no 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;
}




À primeira vista, parece o primeiro caso um para muitos, mas há uma diferença. Desta vez, a criança é realmente um objeto, não uma coleção como no caso anterior.



O código para teste é assim:



  var infoAdditional = new InfoAdditional("InfoAdditional");

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

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


Vamos ver qual expressão sql é gerada desta vez. Para fazer isso, vamos cavar o método findById para o local:



Package org.springframework.data.jdbc.core.convert class DefaultDataAccessStrategy . Já estamos familiarizados com esta classe, agora estamos interessados ​​no método.



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




Vemos que a seguinte solicitação é recuperada do cache:



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




No momento, a junção externa esquerda é adequada para nós, mas e se não. Como faço para obter uma junção interna?

A criação de join-s funcionais está no pacote org.springframework.data.jdbc.core.convert , classe SqlGenerator , método:
private SelectBuilder.SelectWhere selectBuilder(Collection<SqlIdentifier> keyColumns)




Estamos interessados ​​neste fragmento:



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




Se você precisar unir tabelas, existe uma opção apenas com uma junção externa esquerda.

Parece que a junção interna ainda não pode ser feita.



Conclusão



Cobrimos dois casos mais típicos de como você pode unir tabelas no Spring Data Jdbc.

Em princípio, a funcionalidade que temos agora é bastante adequada para resolver problemas práticos, embora haja limitações não críticas.



O texto completo do exemplo pode ser encontrado aqui .



E aqui está uma versão em vídeo deste post.






All Articles