Colunas Definidas em Tempo de Execução Com asentinel-orm

Asentinel-orm é uma ferramenta ORM leve construída sobre o Spring JDBC, particularmente JdbcTemplate. Assim, possui a maioria das características que se esperaria de um ORM básico, como geração de SQL, carregamento preguiçoso, etc.

Ao aproveitar o JdbcTemplate, significa que permite participação em transações gerenciadas pelo Spring e pode ser facilmente integrado em qualquer projeto que já utilize o JdbcTemplate como meio de interação com o banco de dados.

Desde 2015, asentinel-orm tem sido usado com sucesso em várias aplicações e continuamente melhorado conforme exigido pelas necessidades de negócio. No verão de 2024, tornou-se oficialmente um projeto de código aberto, o que acreditamos que acelerará sua evolução e aumentará o número de contribuidores.

Neste artigo, é construída uma aplicação de exemplo para destacar várias características-chave do ORM:

  • Configuração simples
  • Modelagem de entidades de domínio direta via anotações personalizadas
  • Escrita fácil e execução segura de declarações SQL simples
  • Geração automática de declarações SQL
  • Esquema dinâmico (entidades são enriquecidas com atributos adicionais em tempo de execução, persistidos e lidos sem alterações de código)

Aplicação

Configuração

  • Java 21
  • Spring Boot 3.4.0
  • asentinel-orm 1.70.0
  • Banco de dados H2

Configuração

Para interagir com o asentinel-orm e aproveitar suas funcionalidades, é necessário uma instância de OrmOperations .

Conforme declarado no JavaDoc, esta é a interface central para realizar operações ORM e não é pretendido nem necessário que seja especificamente implementada no código do cliente.

A aplicação de exemplo inclui o código de configuração para criar um bean desse tipo.

Java

 

OrmOperations tem duas super interfaces:

  • SqlBuilderFactory – cria instâncias de SqlBuilder  que podem ser posteriormente utilizadas para criar consultas SQL. SqlBuilder é capaz de gerar automaticamente partes da consulta, como por exemplo a seleção das colunas. A cláusula where, a cláusula order by, outras condições e as colunas reais podem ser adicionadas utilizando métodos da classe SqlBuilder também. Na próxima parte desta seção, um exemplo de consulta gerada por SqlBuilder é mostrado.
  • Updater – usado para salvar entidades em suas respectivas tabelas de banco de dados. Pode realizar inserções ou atualizações dependendo se a entidade é recém-criada ou já existente. Existe uma interface de estratégia chamada NewEntityDetector , que é usada para determinar se uma entidade é nova. Por padrão, o SimpleNewEntityDetector  é utilizado.

Todas as consultas geradas pelo ORM são executadas usando uma instância de SqlQueryTemplate , que ainda precisa de um JdbcOperations/JdbcTemplate para funcionar. Eventualmente, todas as consultas alcançam o bom e velho JdbcTemplate através do qual são executadas enquanto participam de transações do Spring, assim como qualquer execução direta de JdbcTemplate ..

Construções e lógica SQL específicas do banco de dados são fornecidas por meio de implementações da interface JdbcFlavor, que são injetadas na maioria dos beans mencionados acima. Neste artigo, como um banco de dados H2 é utilizado, uma implementação de H2JdbcFlavor é configurada.

A configuração completa do ORM como parte da aplicação de exemplo é OrmConfig.

Implementação

O modelo de domínio experimental exposto pela aplicação de exemplo é simples e consiste em duas entidades – fabricantes de carros e modelos de carros. Representando exatamente o que seus nomes denotam, a relação entre eles é óbvia: um fabricante de carros pode possuir vários modelos de carros.

Além de seu nome, o fabricante de carros é enriquecido com atributos (colunas) que são inseridos pelo usuário da aplicação dinamicamente em tempo de execução. O caso de uso exemplificado é direto:

  • O usuário é solicitado a fornecer os nomes e tipos desejados para os atributos dinâmicos
  • Um par de fabricantes de carros é criado com valores concretos para os atributos dinâmicos previamente adicionados e então
  • As entidades são carregadas de volta descritas pelos atributos iniciais e definidos em tempo de execução

As entidades iniciais são mapeadas usando as tabelas de banco de dados abaixo:

SQL

 

As classes de domínio correspondentes são decoradas com anotações específicas do ORM para configurar os mapeamentos para as tabelas de banco de dados acima. 

Java

 

Java

 

Algumas considerações:

  • @Table – mapeia (associa) a classe a uma tabela de banco de dados
  • @PkColumn – mapeia o id (identificador único) para a chave primária da tabela
  • @Column – mapeia um membro da classe para uma coluna da tabela 
  • @Child – define o relacionamento com outra entidade
  • Membros anotados com @Child – configurados para serem carregados de forma preguiçosa
  • Coluna da tabela type – mapeada para um campo enumCarType

Para que a classe CarManufacturer suporte atributos definidos em tempo de execução (mapeados para colunas de tabela definidas em tempo de execução), uma subclasse como a abaixo é definida:

Java

 

Esta classe armazena os atributos (campos) definidos em tempo de execução em um Map. A interação entre os valores dos campos em tempo de execução e o ORM é realizada por meio da implementação da interface DynamicColumnEntity .

Java

 

  • setValue() – é usado para definir o valor da coluna definida em tempo de execução quando este é lido da tabela
  • getValue() – é usado para recuperar o valor de uma coluna definida em tempo de execução quando este é salvo na tabela

A DynamicColumn mapeia atributos definidos em tempo de execução para suas colunas correspondentes de maneira semelhante ao @Column anotação que mapeia membros conhecidos em tempo de compilação.

Ao executar a aplicação, o CfRunner é executado. O usuário é solicitado a inserir nomes e tipos para os atributos dinâmicos personalizados desejados que enriquecem a entidade CarManufacturer (para simplicidade, apenas os tipos int e varchar são suportados). 

Para cada par nome-tipo, um comando DML é executado para que as novas colunas possam ser adicionadas à tabela do banco de dados CarManufacturer . O seguinte método (declarado em CarService) realiza a operação.

Java

 

Cada atributo de entrada é registrado como um DefaultDynamicColumn, uma implementação de referência de DynamicColumn .

Uma vez que todos os atributos estão definidos, dois fabricantes de automóveis são adicionados ao banco de dados, à medida que o usuário fornece valores para cada um desses atributos.

Java

 

O método abaixo (declarado em CarService) realmente cria a entidade via ORM.

Java

 

A versão com 2 parâmetros do método OrmOperations update() é chamada, o que permite passar uma instância de UpdateSettings e comunicar ao ORM, na execução, que existem atributos definidos em tempo de execução cujos valores devem ser persistidos.

Por fim, dois modelos de carro são criados, correspondendo a um dos fabricantes de carros adicionados anteriormente.

Java

 

O método abaixo (declarado em CarService) realmente cria as entidades via ORM, desta vez usando o método update() de OrmOperations para persistir entidades sem atributos dinâmicos. Para conveniência, várias entidades são criadas em uma única chamada.

Java

 

Como último passo, um dos fabricantes criados é carregado de volta pelo seu nome usando uma consulta gerada pelo ORM.

Java

 

Algumas explicações sobre o método definido acima valem a pena ser feitas.

O método OrmOperations newSqlBuilder() cria uma instância de SqlBuilder, e como o nome sugere, isso pode ser usado para gerar consultas SQL. O método SqlBuilder select() gera a parte select from table da consulta, enquanto o restante (where, order by) deve ser adicionado. A parte de seleção da consulta pode ser personalizada passando instâncias de EntityDescriptorNodeCallback (detalhes sobre EntityDescriptorNodeCallback podem ser o assunto de um artigo futuro).

Para que o ORM saiba que o plano é selecionar e mapear colunas definidas em tempo de execução, um DynamicColumnsEntityNodeCallback precisa ser passado. Juntamente com ele, um AutoEagerLoader é fornecido para que o ORM entenda que deve carregar ansiosamente a lista de CarModels associados ao fabricante. No entanto, isso não tem relação com os atributos definidos em tempo de execução, mas demonstra como um membro filho pode ser carregado ansiosamente.

Conclusão

Embora provavelmente existam outras maneiras de trabalhar com colunas definidas em tempo de execução quando os dados são armazenados em bancos de dados relacionais, a abordagem apresentada neste artigo tem a vantagem de usar colunas de banco de dados padrão que são lidas/escritas usando consultas SQL padrão geradas diretamente pelo ORM.

Não era raro quando tivemos a chance de discutir na “comunidade” o asentinel-orm, as razões que tivemos para desenvolver tal ferramenta. Normalmente, à primeira vista, os desenvolvedores se mostraram relutantes e reservados quando se tratava de um ORM sob medida, perguntando por que não usar Hibernate ou outras implementações JPA.

No nosso caso, o principal motivo foi a necessidade de uma maneira rápida, flexível e fácil de trabalhar com às vezes um número bastante grande de atributos definidos em tempo de execução (colunas) para entidades que fazem parte do domínio de negócios. Para nós, isso se provou ser o caminho certo. As aplicações estão funcionando sem problemas em produção, os clientes estão satisfeitos com a velocidade e o desempenho alcançado, e os desenvolvedores estão confortáveis e criativos com a API intuitiva.

Como o projeto agora é de código aberto, é muito fácil para qualquer pessoa interessada dar uma olhada, formar uma opinião objetiva sobre ele e, por que não, fazer um fork, abrir um PR e contribuir.

Recursos

  • O projeto ORM de código aberto está aqui.
  • O código-fonte da aplicação de exemplo está aqui.

Source:
https://dzone.com/articles/runtime-defined-columns-with-asentinel-orm