asentinel-orm é uma ferramenta ORM leve construída sobre o Spring JDBC, particularmente JdbcTemplate
. Assim, possui a maioria dos recursos que se esperaria de um ORM básico, como geração de SQL, carregamento sob demanda, etc.
Ao aproveitar o JdbcTemplate
, isso significa que permite participação em transações gerenciadas pelo Spring, e pode ser facilmente integrado a qualquer projeto que já utilize JdbcTemplate
como meio de interagir com o banco de dados.
Desde 2015, asentinel-orm tem sido utilizado com sucesso em várias aplicações e continuamente melhorado conforme necessário pelas exigências do negócio. No verão de 2024, tornou-se oficialmente um projeto de código aberto, o que consideramos que acelerará sua evolução e aumentará o número de colaboradores.
Neste artigo, uma aplicação de exemplo é construída para destacar vários recursos-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 instruções SQL simples
- Geração automática de instruções SQL
- Esquema dinâmico (entidades são enriquecidas com atributos adicionais em tempo de execução, persistidas e lidas 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ária uma instância de OrmOperations
.
Conforme declarado no JavaDoc, esta é a interface central para realizar operações ORM, e não se destina nem é necessário ser implementada especificamente no código do cliente.
O aplicativo de exemplo inclui o código de configuração para criar um bean deste tipo.
public OrmOperations orm(SqlBuilderFactory sqlBuilderFactory,
JdbcFlavor jdbcFlavor, SqlQuery sqlQuery) {
return new OrmTemplate(sqlBuilderFactory, new SimpleUpdater(jdbcFlavor, sqlQuery));
}
OrmOperations
possui duas superinterfaces:
- SqlBuilderFactory – cria instâncias de
SqlBuilder
que podem ser utilizadas para criar consultas SQL. OSqlBuilder
é capaz de gerar automaticamente partes da consulta, por exemplo, a que seleciona as colunas. A cláusula where, a cláusula order by, outras condições e as colunas reais podem ser adicionadas usando métodos da classeSqlBuilder
também. Na próxima parte desta seção, um exemplo de consulta gerada porSqlBuilder
é mostrado. - Updater – usado para salvar entidades em suas respectivas tabelas de banco de dados. Ele pode realizar inserções ou atualizações, dependendo se a entidade foi recém-criada ou já existe. Uma interface de estratégia chamada
NewEntityDetector
existe, que é usada para determinar se uma entidade é nova. Por padrão, oSimpleNewEntityDetector
é utilizado.
Todas as consultas geradas pelo ORM são executadas usando uma instância de SqlQueryTemplate
, que por sua vez precisa de um JdbcOperations
/JdbcTemplate
para funcionar. Eventualmente, todas as consultas chegam ao bom e velho JdbcTemplate
através do qual são executadas enquanto participam de transações do Spring, assim como qualquer JdbcTemplate
execução direta.
Construtos e lógicas SQL específicos do banco de dados são fornecidos 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 dinamicamente pelo usuário da aplicação 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 casal de fabricantes de automóveis é criado com valores concretos para os atributos dinâmicos previamente adicionados, e então
- As entidades são carregadas de volta descritas tanto pelos atributos iniciais quanto pelos atributos definidos em tempo de execução
As entidades iniciais são mapeadas usando as tabelas de banco de dados abaixo:
CREATE TABLE CarManufacturers (
ID INT auto_increment PRIMARY KEY,
NAME VARCHAR(255)
);
CREATE TABLE CarModels(
ID INT auto_increment PRIMARY KEY,
CarManufacturer int,
NAME VARCHAR(255),
TYPE VARCHAR(15),
foreign key (CarManufacturer) references CarManufacturers(id)
);
As classes de domínio correspondentes são decoradas com anotações específicas de ORM para configurar os mapeamentos para as tabelas de banco de dados acima.
"CarManufacturers") (
public class CarManufacturer {
"id") (
private int id;
"name") (
private String name;
parentRelationType = RelationType.MANY_TO_ONE, (
fkName = CarModel.COL_CAR_MANUFACTURER,
fetchType = FetchType.LAZY)
private List<CarModel> models = Collections.emptyList();
...
}
"CarModels") (
public class CarModel {
public static final String COL_CAR_MANUFACTURER = "CarManufacturer";
"id") (
private int id;
"name") (
private String name;
"type") (
private CarType type;
fkName = COL_CAR_MANUFACTURER, fetchType = FetchType.LAZY) (
private CarManufacturer carManufacturer;
...
}
public enum CarType {
CAR, SUV, TRUCK
}
Algumas considerações:
@Table
– mapeia (associa) a classe a uma tabela de banco de dados@PkColumn
– mapeia oid
(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@Child
membros anotados – configurados para serem carregados de forma preguiçosa- coluna da tabela
type
– mapeada para um campoenum
–CarType
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:
public class CustomFieldsCarManufacturer extends CarManufacturer
implements DynamicColumnsEntity<DynamicColumn> {
private final Map<DynamicColumn, Object> customFields = new HashMap<>();
...
public void setValue(DynamicColumn column, Object value) {
customFields.put(column, value);
}
public Object getValue(DynamicColumn column) {
return customFields.get(column);
}
...
}
Esta classe armazena os atributos definidos em tempo de execução (campos) 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
.
public interface DynamicColumnsEntity<T extends DynamicColumn> {
void setValue(T column, Object value);
Object getValue(T column);
}
setValue()
– é usado para definir o valor da coluna definida em tempo de execução quando este é lido da tabelagetValue()
– é 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 à anotação @Column
que mapeia membros conhecidos em tempo de compilação.
Ao executar o aplicativo, 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 de banco de dados CarManufacturer
. O método a seguir (declarado em CarService
) realiza a operação.
public void addManufacturerField(String name, String type) {
orm.getSqlQuery()
.update("alter table CarManufacturers add column " + name + " " + type);
}
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 atributo.
Map<DynamicColumn, Object> dynamicColumnsValues = new HashMap<>();
for (DynamicColumn dynamicColumn : dynamicColumns) {
// read values for each dynamic attribute
...
}
CustomFieldsCarManufacturer mazda = new CustomFieldsCarManufacturer("Mazda", dynamicColumnsValues);
carService.createManufacturer(mazda, dynamicColumns);
O método abaixo (declarado em CarService
) na verdade cria a entidade via ORM.
public void createManufacturer(CustomFieldsCarManufacturer manufacturer, List<DynamicColumn> attributes) {
orm.update(manufacturer, new UpdateSettings<>(attributes, null));
}
A versão de 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 serão persistidos.
Por fim, dois modelos de carro são criados, correspondendo a um dos fabricantes de carro previamente adicionados.
CarModel mx5 = new CarModel("MX5", CarType.CAR, mazda);
CarModel cx60 = new CarModel("CX60", CarType.SUV, mazda);
carService.createModels(mx5, cx60);
O método abaixo (declarado em CarService
) realmente cria as entidades via ORM, desta vez utilizando o método OrmOperations
update() para persistir entidades sem atributos dinâmicos. Para conveniência, várias entidades são criadas em uma chamada.
public void createModels(CarModel... models) {
orm.update(models);
}
Como última etapa, um dos fabricantes criados é carregado de volta pelo seu nome usando uma consulta gerada pelo ORM.
CarManufacturer mazda1 = carService.findManufacturerByName("Mazda", dynamicColumns);
readOnly = true) (
public CarManufacturer findManufacturerByName(String name, List<DynamicColumn> attributes) {
return orm.newSqlBuilder(CustomFieldsCarManufacturer.class)
.select(
AutoEagerLoader.forPath(CarManufacturer.class, CarModel.class),
new DynamicColumnsEntityNodeCallback<>(
new DefaultObjectFactory<>(CustomFieldsCarManufacturer.class),
attributes
)
)
.where().column("name").eq(name)
.execForEntity();
}
Algumas explicações sobre o método definido acima valem a pena fazer.
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. Junto com isso, um AutoEagerLoader
é fornecido para que o ORM entenda que deve carregar ansiosamente a lista de CarModel
s associados ao fabricante. No entanto, isso não tem nada a ver com os atributos definidos em tempo de execução, mas demonstra como um membro filho pode ser carregado ansiosamente.
Conclusão
Embora provavelmente haja outras maneiras de trabalhar com colunas definidas em tempo de execução quando os dados estão armazenados em bancos de dados relacionais, a abordagem apresentada neste artigo tem a vantagem de usar colunas padrão do banco de dados que são lidas/escritas usando consultas SQL padrão geradas diretamente pelo ORM.
Não era raro quando tivemos a oportunidade de discutir na “comunidade” o asentinel-orm, as razões que tivemos para desenvolver tal ferramenta. Geralmente, à 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 motor foi a necessidade de uma forma rápida, flexível e fácil de trabalhar com um número às vezes 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, provou ser o caminho certo. Os aplicativos 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
Source:
https://dzone.com/articles/runtime-defined-columns-with-asentinel-orm