Talvez você já saiba algo sobre a biblioteca Celesta de código aberto. Caso contrário, não importa, agora contaremos tudo. Mais um ano se passou, a versão 7.x foi lançada, muitas coisas mudaram e chegou a hora de resumir as mudanças e, ao mesmo tempo, lembrar o que é Celesta em geral.
Se você ainda não ouviu nada sobre o Celesta e, ao ler este artigo, deseja saber para quais tarefas de negócios seu aplicativo é mais eficaz, recomendo a primeira parte da postagem antiga ou este vídeo de meia hora (exceto palavras sobre o uso da linguagem Python). Mas melhor ainda, leia este artigo primeiro. Começarei com as alterações que ocorreram na versão 7 e, em seguida, examinarei um exemplo técnico completo do uso da versão moderna do Celesta para escrever um pequeno serviço de back-end para um aplicativo Java usando o Spring Boot.
O que mudou na versão 7.x?
- Nós nos recusamos a usar o Jython como uma linguagem incorporada ao Celesta. Se anteriormente começamos a falar sobre o Celesta com o fato de que a lógica de negócios é escrita em Python, agora ... qualquer linguagem Java pode servir como a linguagem da lógica de negócios: Java, Groovy, JRuby ou o mesmo Jython. Agora, o Celesta não chama o código de lógica de negócios, mas o código de lógica de negócios usa o Celesta e suas classes de acesso a dados como a biblioteca Java mais comum. Sim, a compatibilidade com versões anteriores foi violada por causa disso, mas esse é o preço que estávamos dispostos a pagar. Infelizmente, nossa aposta no Jython perdeu. Quando começamos a usar o Jython, há alguns anos, era um projeto animado e promissor, mas, com o passar dos anos, seu desenvolvimento desacelerou, o registro acumulado da especificação de linguagem se acumulou, os problemas de compatibilidade para a maioria das bibliotecas de pip não foram resolvidos. A última gota foram os novos bugs nas versões mais recentes do idioma, que se manifestaram ao trabalhar em uma carga de produção. Nós mesmos não temos os recursos para apoiar o projeto Jython, e decidimos participar dele. O Celesta não é mais dependente do Jython.
- As classes de acesso a dados agora são geradas por código na linguagem Java (e não em Python, como antes) usando o plug-in Maven. E como passamos da digitação dinâmica para a estática por causa disso, havia mais oportunidades de refatoração e ficou mais fácil escrever código subjetivamente correto.
- Apareceu a extensão para o JUnit5, tornando-se muito conveniente escrever testes de lógica que funcionem com o banco de dados no JUnit5 (que será discutido mais adiante).
- Um projeto separado apareceu - spring-boot-starter-celesta , que, como o nome indica, é o iniciador do Celesta no Spring Boot. A capacidade de compactar aplicativos Celesta em serviços Spring Boot facilmente implantáveis compensou a perda da capacidade de atualizar o aplicativo no servidor, simplesmente alterando a pasta com scripts Python.
- Transferimos toda a documentação do Wiki para o formato AsciiDoctor , colocamos no controle de versão junto com o código e agora temos documentação atualizada para cada versão do Celesta. Para a versão mais recente, a documentação on-line está disponível aqui: https://courseorchestra.imtqy.com/celesta/
- Muitas vezes nos perguntam se é possível usar a migração de banco de dados por meio de DDL idempotente separadamente do Celesta. Agora, existe uma oportunidade usando a ferramenta 2bass .
O que é Celesta e o que ela pode fazer?
Em poucas palavras, Celesta é:
- uma camada intermediária entre o banco de dados relacional e o código da lógica de negócios, com base na abordagem de design do banco de dados primeiro ,
- mecanismo de migração da estrutura do banco de dados,
- estrutura para testar código que funciona com dados.
Suportamos quatro tipos de bancos de dados relacionais: PostgreSQL, MS SQL Server, Oracle e H2.
Principais recursos do Celesta:
- Um princípio muito semelhante ao princípio básico do Java: "Escreva uma vez, execute em todos os RDBMS suportados". O código da lógica de negócios não sabe em que tipo de banco de dados ele será executado. Você pode escrever o código da lógica de negócios e executá-lo no MS SQL Server, depois alternar para PostgreSQL, e isso acontecerá sem complicações (bem, quase :)
- Reestruturação automática em um banco de dados ativo. A maior parte do ciclo de vida dos projetos Celesta ocorre quando o banco de dados de trabalho já está lá e é preenchido com dados que precisam ser salvos, mas também é necessário alterar constantemente sua estrutura. Um dos principais recursos do Celesta é a capacidade de "ajustar" automaticamente a estrutura do banco de dados ao seu modelo de dados.
- Teste. É prestada muita atenção para garantir que o código do Celesta seja testável, para que possamos testar automaticamente métodos que modificam dados no banco de dados, fazendo isso de maneira fácil, rápida e elegante, sem usar ferramentas externas, como DbUnit e contêineres.
Por que você precisa de independência do tipo de DBMS?
A independência do código da lógica de negócios em relação ao tipo de DBMS não foi o primeiro ponto que colocamos: o código escrito para o Celesta não sabe em qual DBMS ele está sendo executado. Porque
Em primeiro lugar, devido ao fato de a escolha de um tipo de SGBD não ser uma questão tecnológica, mas política. Chegando a um novo cliente comercial, geralmente descobrimos que ele já possui um tipo de DBMS favorito no qual os fundos são investidos e o cliente deseja ver outras soluções na infraestrutura existente. O cenário tecnológico está mudando: o PostgreSQL é cada vez mais encontrado em agências governamentais e empresas privadas, embora o MS SQL Server tenha prevalecido em nossa prática há alguns anos. O Celesta suporta os DBMSs mais comuns e não estamos preocupados com essas mudanças.
Em segundo lugar, gostaria de transferir o código já criado para solucionar problemas padrão de um projeto para outro, para criar uma biblioteca reutilizável. Coisas como diretórios hierárquicos ou módulos de distribuição de notificações por email são inerentemente padrão e por que precisamos oferecer suporte a várias versões para clientes com relações diferentes?
Em terceiro lugar, por último, mas não menos importante, a capacidade de executar testes de unidade sem usar o DbUnit e contêineres usando um banco de dados H2 na memória. Nesse modo, a base H2 inicia instantaneamente. O Celesta cria rapidamente um esquema de dados, após o qual você pode realizar os testes necessários e "esquecer" o banco de dados. Como o código da lógica de negócios realmente não sabe em que base ele é executado, portanto, se funciona sem erros no H2, sem erros funcionará no PostgreSQL. Obviamente, a tarefa dos desenvolvedores do próprio sistema Celesta é fazer todos os testes usando DBMSs reais para garantir que nossa plataforma execute igualmente sua API em diferentes relações. E nós fazemos isso. Mas o desenvolvedor da lógica de negócios não é mais necessário.
CelestaSQL
Como é alcançado o basementismo cruzado? Obviamente, ao custo de trabalhar com dados apenas por meio de uma API especial que isola a lógica de quaisquer detalhes específicos do banco de dados. O Celesta gera classes Java para acessar dados, por um lado, e código SQL e alguns objetos auxiliares dentro do banco de dados, por outro.
O Celesta não fornece mapeamento objeto-relacional em sua forma mais pura, porque ao projetar um modelo de dados, não provemos de classes, mas da estrutura do banco de dados. Ou seja, primeiro construímos um modelo ER de tabelas e, depois, com base nesse modelo, o próprio Celesta gera classes de cursor para acessar dados.
Você pode realizar o mesmo trabalho em todos os DBMSs suportados apenas para a funcionalidade que é aproximadamente igualmente implementada em cada um deles. Se retratamos condicionalmente o conjunto de recursos funcionais de cada uma das bases suportadas por nós na forma de “círculos de Euler”, obtemos a seguinte imagem:

Se fornecermos total independência do tipo de banco de dados, a funcionalidade que abrimos para os programadores de lógica de negócios deve estar dentro da interseção de todas as bases. À primeira vista, parece que essa é uma limitação significativa. Sim: alguns recursos específicos, por exemplo, não podemos usar o SQL Server. Mas, sem exceção, os bancos de dados relacionais suportam tabelas, chaves estrangeiras, visualizações, sequências, consultas SQL com JOIN e GROUP BY. Assim, podemos dar essas oportunidades aos desenvolvedores. Fornecemos aos desenvolvedores "SQL despersonalizado", que chamamos de "CelestaSQL", e no processo geramos consultas SQL para dialetos dos bancos de dados correspondentes.
A linguagem CelestaSQL inclui DDL para definir objetos de banco de dados e consultas SELECT para visualizações e filtros, mas não contém comandos DML: cursores são usados para modificar os dados, que ainda serão discutidos.
Cada banco de dados possui seu próprio conjunto de tipos de dados. O CelestaSQL também possui seu próprio conjunto de tipos. No momento da redação deste artigo, havia nove deles, e esta tabela os compara com tipos reais em vários bancos de dados e tipos de dados Java.
Pode parecer que nove tipos não são suficientes (em comparação com o que o PostgreSQL suporta , por exemplo), mas, na realidade, esses são os tipos suficientes para armazenar informações financeiras, comerciais e logísticas: strings, inteiros, fracionários , datas, valores booleanos e blobs são sempre suficientes para representar esses dados.
A própria linguagem CelestaSQL é descrita na documentação com um grande número de diagramas de sintaxe.
Modificação da estrutura do banco de dados. DDL idempotente
Outra característica importante do Celesta é sua abordagem para migrar a estrutura do banco de dados em funcionamento à medida que o projeto se desenvolve. Para fazer isso, é utilizada a abordagem incorporada ao Celesta usando DDL idempotente.
Em poucas palavras, quando escrevemos no CelestaSQL o seguinte texto:
CREATE TABLE OrderLine( order_id VARCHAR(30) NOT NULL, line_no INT NOT NULL, item_id VARCHAR(30) NOT NULL, item_name VARCHAR(100), qty INT NOT NULL DEFAULT 0, cost REAL NOT NULL DEFAULT 0.0, CONSTRAINT Idx_OrderLine PRIMARY KEY (order_id, line_no) );
- este texto não é interpretado por Celesta como "crie uma tabela, mas se já houver uma tabela, dê um erro", mas "traga a tabela para a estrutura desejada". Ou seja: “se não houver tabela, crie-a, se houver uma tabela, veja quais campos estão nela, com quais tipos, quais índices, quais chaves estrangeiras, quais valores padrão etc., e se é necessário alterar algo em esta tabela para trazê-lo para o tipo certo ".
Com essa abordagem, implementamos a capacidade de refatorar e controlar scripts de versão para determinar a estrutura do banco de dados:
- vemos no script a "imagem desejada" atual da estrutura,
- o quê, por quem e por que na estrutura mudou ao longo do tempo, podemos analisar o sistema de controle de versão,
- quanto aos comandos ALTER, o Celesta os gera e executa automaticamente "sob o capô", conforme necessário.
Obviamente, essa abordagem tem suas limitações. A Celesta faz todos os esforços para garantir que a migração automática seja indolor e contínua, mas isso não é possível em todos os casos. A motivação, as possibilidades e as limitações dessa abordagem foram descritas neste post (sua versão em inglês também está disponível).
Para acelerar o processo de verificação / atualização da estrutura do banco de dados, o Celesta aplica o armazenamento de somas de verificação de script DDL no banco de dados (até que a soma de verificação seja alterada, o processo de verificação e atualização da estrutura do banco de dados não inicia). Para que o processo de atualização prossiga sem problemas relacionados à ordem de mudança de objetos dependentes um do outro, é aplicada a classificação topológica de dependências entre esquemas por chaves estrangeiras. O processo de migração automática é descrito em mais detalhes na documentação .
Criando um projeto e modelo de dados Celesta
O projeto de demonstração, que consideraremos, está disponível no github . Vamos ver como você pode usar o Celesta ao escrever um aplicativo Spring Boot. Aqui estão as dependências do Maven que você precisa:
org.springframework.boot:spring-boot-starter-web
e ru.curs:spring-boot-starter-celesta
(para obter mais detalhes, ru.curs:spring-boot-starter-celesta
documentação).- Se você não estiver usando o Spring Boot, poderá conectar
ru.curs:celesta-system-services
a ru.curs:celesta-system-services
do ru.curs:celesta-system-services
. - Para geração de código de classes de acesso a dados baseadas em scripts Celesta-SQL,
ru.curs:celesta-maven-plugin
necessário o ru.curs:celesta-maven-plugin
- o código-fonte para um exemplo de demonstração ou documentação descreve como conectá-lo. - Para tirar proveito da capacidade de gravar testes de unidade JUnit5 para métodos que modificam dados, você deve conectar
ru.curs:celesta-unit
no escopo do teste.
Agora crie um modelo de dados e compile classes de acesso a dados.
Digamos que estamos desenvolvendo um projeto para uma empresa de comércio eletrônico que recentemente se fundiu com outra empresa. Cada um tem seu próprio banco de dados. Eles coletam pedidos, mas até mesclar seus bancos de dados, precisam de um único ponto de entrada para coletar pedidos de fora.
A implementação desse "ponto de entrada" deve ser bastante tradicional: um serviço HTTP com operações CRUD que armazena dados em um banco de dados relacional.
Devido ao fato de o Celesta implementar a abordagem de design do banco de dados, primeiro precisamos criar uma estrutura de tabela que armazene pedidos. Um pedido, como você sabe, é uma entidade composta: consiste em um cabeçalho no qual são armazenadas informações sobre o cliente, data do pedido e outros atributos do pedido, além de várias linhas (itens de mercadorias).
Então, para o trabalho: crie
src/main/celestasql
- por padrão, esse é o caminho para os scripts do projeto CelestaSQL- contém subpastas que repetem a estrutura de pastas dos pacotes java (
ru/curs/demo
no nosso caso). - na pasta package, crie um arquivo
.sql
com o seguinte conteúdo:
CREATE SCHEMA demo VERSION '1.0'; CREATE TABLE OrderHeader( id VARCHAR(30) NOT NULL, date DATETIME, customer_id VARCHAR(30), customer_name VARCHAR(50), manager_id VARCHAR(30), CONSTRAINT Pk_OrderHeader PRIMARY KEY (id) ); CREATE TABLE OrderLine( order_id VARCHAR(30) NOT NULL, line_no INT NOT NULL, item_id VARCHAR(30) NOT NULL, item_name VARCHAR(100), qty INT NOT NULL DEFAULT 0, cost REAL NOT NULL DEFAULT 0.0, CONSTRAINT Idx_OrderLine PRIMARY KEY (order_id, line_no) ); ALTER TABLE OrderLine ADD CONSTRAINT fk_OrderLine FOREIGN KEY (order_id) REFERENCES OrderHeader(id); CREATE VIEW OrderedQty AS SELECT item_id, sum(qty) AS qty FROM OrderLine GROUP BY item_id;
Aqui, descrevemos duas tabelas conectadas por uma chave estrangeira e uma exibição que retornará uma quantidade resumida para as mercadorias presentes em todos os pedidos. Como você pode ver, isso não é diferente do SQL regular, com exceção do comando CREATE SCHEMA
, no qual declaramos a versão do esquema demo
(para saber como o número da versão afeta a migração automática, consulte a documentação ). Mas também há recursos. Por exemplo, todos os nomes das tabelas e campos que usamos apenas podem ser convertidos em nomes válidos de classe e variável na linguagem Java. Portanto, espaços, caracteres especiais são excluídos. Você também pode observar que, nos comentários que colocamos sobre os nomes das tabelas e alguns dos campos, não começamos com / *, como de costume, mas com / **, como os comentários do JavaDoc começam - e isso não é por acaso! Um comentário definido sobre uma entidade começando com / ** estará disponível em tempo de execução na propriedade .getCelestaDoc()
dessa entidade. Isso é útil quando queremos fornecer aos elementos do banco de dados meta-informações adicionais: por exemplo, nomes de campos legíveis por humanos, informações sobre como representar campos na interface do usuário etc.
O script CelestaSQL serve duas tarefas igualmente importantes: primeiro, para a implantação / modificação da estrutura de um banco de dados relacional e, segundo, para a geração de código das classes de acesso a dados.
Agora podemos gerar classes de acesso a dados, basta executar o comando mvn generate-sources
ou, se você estiver trabalhando no IDEA, clique no botão 'Gerar fontes e atualizar pastas' no painel de controle do Maven. No segundo caso, o IDEA “ target/generated-sources/celesta
pasta criada em target/generated-sources/celesta
e disponibiliza seu conteúdo para importação nos códigos-fonte do projeto. O resultado da geração de código terá a seguinte aparência - uma classe para cada objeto no banco de dados:
A conexão com o banco de dados é especificada nas configurações do aplicativo, no nosso caso, no arquivo src/main/resources/application.yml
. Ao usar o spring-boot-starter-celesta, o IDEA informará as opções de código disponíveis na conclusão do código.
Se não queremos nos preocupar com o RDBMS "real" para fins de demonstração, podemos fazer com que o Celesta trabalhe com o banco de dados H2 embutido no modo de memória usando a seguinte configuração:
celesta: h2: inMemory: true
Para conectar um banco de dados "real", altere a configuração para algo como
celesta: jdbc: url: jdbc:postgresql://127.0.0.1:5432/celesta username: <your_username> password: <your_password>
(nesse caso, você também precisará adicionar um driver JDBC do PostgreSQL ao seu aplicativo através da dependência do Maven).
Ao iniciar um aplicativo Celesta com uma conexão com um servidor de banco de dados, é possível observar que as tabelas, visualizações, índices, etc. necessários são criados para um banco de dados vazio e, para um não vazio, são atualizados para as estruturas especificadas na DDL.
Criando métodos de manipulação de dados
Depois de descobrir como criar uma estrutura de banco de dados, você pode começar a escrever a lógica de negócios.
Para poder implementar os requisitos para a distribuição de direitos de acesso e ações de registro, qualquer operação com dados no Celesta é realizada em nome de um usuário, não há operações "anônimas". Portanto, qualquer código Celesta é executado no contexto da chamada descrita na classe CallContext .
- Antes de iniciar uma operação que pode modificar dados no banco de dados, o
CallContext
ativado. - No momento da ativação, uma conexão com o banco de dados é obtida do pool de conexões e a transação é iniciada.
- Após a conclusão da operação
CallContext
executa commit()
se a operação foi bem-sucedida ou rollback()
se uma exceção não tratada ocorreu durante a execução, o CallContext
fecha e a conexão com o banco de dados é retornada ao pool.
Se usarmos spring-boot-starter-celesta, essas ações serão executadas automaticamente para todos os métodos anotados por @CelestaTransaction
.
Suponha que desejemos escrever um manipulador que salve o documento no banco de dados. Seu código no nível do controlador pode ser assim:
@RestController @RequestMapping("/api") public class DocumentController { private final DocumentService srv; public DocumentController(DocumentService srv) { this.srv = srv; } @PutMapping("/save") public void saveOrder(@RequestBody OrderDto order) { CallContext ctx = new CallContext("user1");
Como regra, no nível do método do controlador (ou seja, quando a autenticação já passou), conhecemos o ID do usuário e podemos usá-lo ao criar o CallContext
. A ligação de um usuário a um contexto determina as permissões para acessar tabelas e também fornece a capacidade de registrar alterações feitas em seu nome. É verdade que, neste caso, para a operacionalidade do código interagindo com o banco de dados, os direitos do usuário "usuário1" devem ser indicados nas tabelas do sistema . Se você não deseja usar o sistema de distribuição de acesso Celesta e conceder à contexto da sessão todos os direitos sobre quaisquer tabelas, é possível criar um objeto SystemCallContext
.
O método de salvar a fatura no nível de serviço pode ser assim:
@Service public class DocumentService { @CelestaTransaction public void postOrder(CallContext context, OrderDto doc) { try (OrderHeaderCursor header = new OrderHeaderCursor(context); OrderLineCursor line = new OrderLineCursor(context)) { header.setId(doc.getId()); header.setDate(Date.from(doc.getDate().atStartOfDay(ZoneId.systemDefault()).toInstant())); header.setCustomer_id(doc.getCustomerId()); header.setCustomer_name(doc.getCustomerName()); header.insert(); int lineNo = 0; for (OrderLineDto docLine : doc.getLines()) { lineNo++; line.setLine_no(lineNo); line.setOrder_id(doc.getId()); line.setItem_id(docLine.getItemId()); line.setQty(docLine.getQty()); line.insert(); } } }
Observe a anotação @CelestaTransaction
. Graças a ele, o objeto proxy DocumentService
executará todas essas ações de serviço com o parâmetro CallContext ctx
descrito acima. Ou seja, no início da execução do método, ele já estará vinculado à conexão com o banco de dados e a transação estará pronta para começar. Podemos nos concentrar em escrever a lógica de negócios. No nosso caso, lendo o objeto OrderDto
e salvando-o no banco de dados.
Para fazer isso, usamos os chamados cursores - classes geradas usando o celesta-maven-plugin
. Já vimos o que são. Uma classe é criada para cada um dos objetos de esquema - duas tabelas e uma visualização. E agora podemos usar essas classes para acessar objetos de banco de dados em nossa lógica de negócios.
Para criar um cursor na tabela de pedidos e selecionar o primeiro registro, você precisa escrever o seguinte código:
OrderHeaderCursor header = new OrderHeaderCursor(context); header.tryFirst();
Após criar o objeto de cabeçalho, podemos acessar os campos da entrada da tabela por meio de getters e setters:
Ao criar um cursor, devemos usar o contexto de chamada ativo - esta é a única maneira de criar um cursor. O contexto da chamada carrega informações sobre o usuário atual e seus direitos de acesso.
Com o objeto cursor, podemos fazer coisas diferentes: filtrar, percorrer os registros e também, naturalmente, inserir, excluir e atualizar registros. Toda a API do cursor é descrita em detalhes na documentação .
Por exemplo, o código do nosso exemplo pode ser desenvolvido da seguinte maneira:
OrderHeaderCursor header = new OrderHeaderCursor(context); header.setRange("manager_id", "manager1"); header.tryFirst(); header.setCounter(header.getCounter() + 1); header.update();
Neste exemplo, configuramos o filtro pelo campo manager_id e, em seguida, encontramos o primeiro registro usando o método tryFirst.
(por que "tentar")Os métodos get
, first
, insert
, update
têm duas opções: sem o prefixo try (apenas get(...)
, etc.) e com o prefixo try ( tryGet(...)
, tryFirst()
, etc.) . Métodos sem o prefixo try lançam uma exceção se o banco de dados não tiver os dados apropriados para executar a ação. Por exemplo, first () lançará uma exceção se nenhum registro entrar no conjunto de filtros no cursor. Ao mesmo tempo, os métodos com o prefixo try não lançam uma exceção, mas retornam um valor booleano que sinaliza o sucesso ou a falha da operação correspondente. A prática recomendada é usar métodos sem o prefixo try sempre que possível. Dessa maneira, o código de "autoteste" é criado, sinalizando erros no tempo nos dados lógicos e / ou do banco de dados.
Quando o tryFirst
acionado, as variáveis do tryFirst
são preenchidas com os dados de um registro, podemos ler e atribuir valores a elas. E quando os dados no cursor estão totalmente preparados, executamos update()
e ele armazena o conteúdo do cursor no banco de dados.
Qual problema esse código pode ser afetado? Claro, o surgimento da condição de corrida / atualização perdida! Porque entre o momento em que recebemos os dados na linha com "tryFirst" e o momento em que tentamos atualizar esses dados no ponto "update", alguém já pode receber, modificar e atualizar esses dados no banco de dados. Após a leitura dos dados, o cursor não bloqueia seu uso por outros usuários! Para se proteger contra atualizações perdidas, o Celesta usa o princípio de bloqueio otimista. Em cada tabela, por padrão, o Celesta cria um campo de recversion
e, no nível ON do UPDATE, aumenta o número da versão e verifica se os dados atualizados têm a mesma versão da tabela. Se ocorrer um problema, lança uma exceção. Você pode ler mais sobre isso no artigo “ Proteção contra atualizações perdidas ”.
Lembre-se novamente de que uma transação está associada ao objeto CallContext. Se o procedimento Celesta for bem-sucedido, ocorrerá uma confirmação. Se o método Celesta terminar com uma exceção não tratada, ocorre reversão. Portanto, se ocorrer um erro em algum procedimento complicado, toda a transação relacionada ao contexto da chamada será revertida, como se não tivéssemos começado a fazer nada com os dados, os dados não serão corrompidos. Se, por algum motivo, você precisar de um commit no meio de, digamos, algum tipo de procedimento grande, um commit explícito poderá ser executado chamando context.commit()
.
Testando métodos de dados
Vamos criar um teste de unidade que verifique a correção do método de serviço que armazena OrderDto
no banco de dados.
Ao usar o JUnit5 e a extensão do JUnit5 disponível no módulo celesta-unit
, isso é muito fácil. A estrutura do teste é a seguinte:
@CelestaTest public class DocumentServiceTest { DocumentService srv = new DocumentService(); @Test void documentIsPutToDb(CallContext context) { OrderDto doc =... srv.postOrder(context, doc);
Graças à anotação @CelestaTest
, que é uma extensão do JUnit5, podemos declarar o parâmetro de CallContext context
nos métodos de teste. Esse contexto já está ativado e vinculado ao banco de dados (na memória H2) e, portanto, não precisamos agrupar a classe de serviço em um proxy - nós a criamos usando new
, e não usando Spring. No entanto, se necessário, injete o serviço no teste usando as ferramentas Spring, não há obstáculos para isso.
Criamos testes de unidade com o pressuposto de que, no momento da execução, o banco de dados estará completamente vazio, mas com a estrutura necessária e, após a execução, não podemos nos preocupar com o fato de termos deixado “lixo” no banco de dados. Esses testes são realizados em uma velocidade muito alta.
Vamos criar um segundo procedimento que retorna JSON com valores agregados, mostrando quantos produtos encomendamos.
O teste grava dois pedidos no banco de dados, após o qual verifica o valor total retornado pelo novo método getAggregateReport
:
@Test void reportReturnsAggregatedQuantities(CallContext context) { srv.postOrder(context, . . .); srv.postOrder(context, . . .); Map<String, Integer> result = srv.getAggregateReport(context); assertEquals(5, result.get("A").intValue()); assertEquals(7, result.get("B").intValue()); }
Para implementar o método getAggregateReport
usaremos a visualização OrderedQty, que, lembro-me, no arquivo CelestaSQL é assim:
create view OrderedQty as select item_id, sum(qty) as qty from OrderLine group by item_id;
A solicitação é padrão: resumimos as linhas de pedido por quantidade e agrupamos por código de produto. Um cursor OrderedQtyCursor já foi criado para a exibição, que podemos usar. Declaramos esse cursor, iteramos sobre ele e coletamos o Map<String, Integer>
desejado:
@CelestaTransaction public Map<String, Integer> getAggregateReport(CallContext context) { Map<String, Integer> result = new HashMap<>(); try (OrderedQtyCursor ordered_qty = new OrderedQtyCursor(context)) { for (OrderedQtyCursor line : ordered_qty) { result.put(ordered_qty.getItem_id(), ordered_qty.getQty()); } } return result; }
Visualizações Celesta materializadas
Por que usar uma exibição ruim para obter dados agregados? Essa abordagem é bastante viável, mas, na realidade, coloca uma bomba-relógio em todo o sistema: afinal, uma visão, que é uma consulta SQL, é cada vez mais lenta à medida que os dados se acumulam no sistema. Ele terá que resumir e agrupar mais e mais linhas. Como evitar isso?
O Celesta tenta implementar todas as tarefas padrão com as quais os programadores de lógica de negócios são constantemente confrontados no nível da plataforma.
O MS SQL Server tem o conceito de visualizações materializadas (indexadas), que são armazenadas como tabelas e são atualizadas rapidamente à medida que os dados nas tabelas de origem são alterados. Se estivéssemos trabalhando em um MS SQL Server “limpo”, no nosso caso, a substituição da exibição por uma exibição indexada seria exatamente o que precisávamos: recuperar o relatório agregado não diminuiria à medida que os dados se acumulassem e o trabalho para atualizar o relatório agregado seria executado no momento inserir dados na tabela de linhas de pedidos e também não aumentaria muito com um aumento no número de linhas.
Mas, caso trabalhemos com o PostgreSQL através do Celesta, o que podemos fazer? Redefina a visualização adicionando a palavra materializada:
create materialized view OrderedQty as select item_id, sum(qty) as qty from OrderLine group by item_id;
Vamos iniciar o sistema e ver o que aconteceu com o banco de dados.
OrderedQty
que a OrderedQty
desapareceu e a tabela OrderedQty
apareceu. Além disso, como a tabela OrderLine é preenchida com dados, as informações na tabela OrderedQty serão "magicamente" atualizadas, como se OrderedQty fosse uma visualização.
Não há mágica aqui se olharmos para os gatilhos criados na tabela OrderLine
. Celesta, tendo recebido a tarefa de criar uma "exibição materializada", analisou a consulta e criou gatilhos na tabela OrderLine
que atualizam OrderedQty
. Ao inserir uma única palavra-chave - materialized
- no arquivo CelestaSQL, resolvemos o problema de degradação do desempenho e o código da lógica de negócios nem precisava ser alterado!
, , , . «» Celesta , , JOIN-, GROUP BY. , , , , . . .
Conclusão
Celesta. — .