
1. Introdução
Quase qualquer sistema de informação, de uma maneira ou de outra, interage com armazenamentos de dados externos. Na maioria dos casos, esse é um banco de dados relacional e, geralmente, algum tipo de estrutura ORM é usada para trabalhar com dados. O ORM elimina a maioria das operações de rotina, oferecendo um pequeno conjunto de abstrações adicionais para trabalhar com dados.
Martin Fowler publicou um artigo interessante, um dos pensamentos principais: “ORMs nos ajudam a resolver um grande número de problemas em aplicativos corporativos ... Essa ferramenta não pode ser chamada de bonita, mas os problemas com os quais ela lida também não são bons. Eu acho que o ORM merece mais respeito e mais compreensão. ”
Usamos o ORM muito intensivamente na estrutura do CUBA , para conhecer em primeira mão os problemas e as limitações dessa tecnologia, pois o CUBA é usado em vários projetos em todo o mundo. Há muitos tópicos que podem ser discutidos em relação ao ORM, mas vamos nos concentrar em um deles: a escolha entre os métodos “preguiçoso” (preguiçoso) e “ganancioso” (ansioso) de amostragem de dados. Falaremos sobre diferentes abordagens para resolver esse problema com ilustrações da API JPA e Spring e também descreveremos como (e por que exatamente) o ORM é usado no CUBA e que trabalho estamos fazendo para melhorar o trabalho com dados em nossa estrutura.
Amostragem de dados: preguiçoso ou não?
Se o seu modelo de dados tiver apenas uma entidade, provavelmente você não notará nenhum problema ao trabalhar com o ORM. Vejamos um pequeno exemplo. Suponha que tenhamos uma entidade User ()
que possua dois atributos: ID
e Name ()
:
public class User { @Id @GeneratedValue private int id; private String name;
Para obter uma instância dessa entidade do banco de dados, basta chamar um método do objeto EntityManager
:
EntityManager em = entityManagerFactory.createEntityManager(); User user = em.find(User.class, id);
As coisas ficam um pouco mais interessantes quando um relacionamento um para muitos aparece:
public class User { @Id @GeneratedValue private int id; private String name; @OneToMany private List<Address> addresses;
Se precisarmos extrair uma instância do usuário do banco de dados, surge a pergunta: “Também selecionamos endereços?”. E a resposta "correta" aqui é: "Depende de ..." Em alguns casos, precisaremos de endereços, em alguns - não. Normalmente, o ORM fornece duas maneiras de buscar registros dependentes: lento e ganancioso. Por padrão, a maioria dos ORMs usa a maneira lenta. Mas, se escrevermos este código:
EntityManager em = entityManagerFactory.createEntityManager(); User user = em.find(User.class, 1); em.close(); System.out.println(user.getAddresses().get(0));
... então obtemos a exceção “LazyInitException”
, que confunde muito os recém-chegados que começaram a trabalhar com o ORM. E aqui chega o momento em que você precisa iniciar uma história sobre o que são instâncias "Anexadas" e "Desanexadas" de uma entidade, o que são sessões e transações.
Sim, isso significa que a entidade deve estar "anexada" à sessão para que você possa selecionar os dados dependentes. Bem, não vamos fechar transações imediatamente, e a vida se tornará imediatamente mais fácil. E aqui surge outro problema - as transações se tornam mais longas, o que aumenta o risco de conflito. Tornar as transações mais curtas? É possível, mas se você criar muitas e pequenas transações, obteremos o "Conto de Komar Komarovich - um nariz comprido e um Misha peludo - um rabo curto" sobre como a horda de pequenos mosquitos ursos venceu - isso acontecerá com o banco de dados. Se o número de pequenas transações aumentar significativamente, surgirão problemas de desempenho.
Como foi dito, ao buscar dados sobre um usuário, os endereços podem ser necessários ou não, portanto, dependendo da lógica de negócios, é necessário selecionar a coleção ou não. É necessário adicionar novas condições ao código ... Hmmm ... Algo está de alguma forma ficando complicado.
Então, e se você tentar um tipo diferente de amostra?
public class User { @Id @GeneratedValue private int id; private String name; @OneToMany(fetch = FetchType.EAGER) private List<Address> addresses;
Bem ... você não pode dizer que isso vai ajudar muito. Sim, vamos nos livrar do odiado LazyInit
e não há necessidade de verificar se a entidade está anexada à sessão ou não. Mas agora podemos ter problemas de desempenho, porque nem sempre precisamos de endereços, mas ainda selecionamos esses objetos na memória do servidor.
Mais alguma ideia?
Spring jdbc
Alguns desenvolvedores se cansam tanto do ORM que mudam para estruturas alternativas. Por exemplo, no Spring JDBC, que fornece a capacidade de converter dados relacionais em dados de objetos no modo "semi-automático". O desenvolvedor grava consultas para cada caso em que um ou outro conjunto de atributos é necessário (ou o mesmo código é reutilizado nos casos em que as mesmas estruturas de dados são necessárias).
Isso nos dá uma grande flexibilidade. Por exemplo, você pode selecionar apenas um atributo sem criar o objeto de entidade correspondente:
String name = this.jdbcTemplate.queryForObject( "select name from t_user where id = ?", new Object[]{1L}, String.class);
Ou selecione um objeto na forma usual:
User user = this.jdbcTemplate.queryForObject( "select id, name from t_user where id = ?", new Object[]{1L}, new RowMapper<User>() { public User mapRow(ResultSet rs, int rowNum) throws SQLException { User user = new User(); user.setName(rs.getString("name")); user.setId(rs.getInt("id")); return user; } });
Você também pode selecionar uma lista de endereços para o usuário, basta escrever um pouco mais de código e compor corretamente a consulta SQL para evitar o problema de n + 1 consultas .
Tããão, complicado de novo. Sim, controlamos todas as consultas e como os dados são mapeados nos objetos, mas precisamos escrever mais código, aprender SQL e saber como as consultas são executadas no banco de dados. Pessoalmente, acho que o conhecimento de SQL é uma habilidade essencial para um programador de aplicativos, mas nem todo mundo pensa assim, e não vou me envolver em polêmicas. Afinal, o conhecimento das instruções de montagem do x86 atualmente também é opcional. Vamos pensar melhor sobre como facilitar a vida dos programadores.
JPA EntityGraph
E vamos dar um passo atrás e pensar: do que precisamos? Parece que precisamos apenas indicar exatamente quais atributos precisamos em cada caso. Bem, vamos lá! O JPA 2.1 introduziu uma nova API - EntityGraph (gráfico de entidade). A ideia é muito simples: usamos anotações para descrever o que escolheremos no banco de dados. Aqui está um exemplo:
@Entity @NamedEntityGraphs({ @NamedEntityGraph(name = "user-only-entity-graph"), @NamedEntityGraph(name = "user-addresses-entity-graph", attributeNodes = {@NamedAttributeNode("addresses")}) }) public class User { @Id @GeneratedValue private int id; private String name; @OneToMany(fetch = FetchType.LAZY) private Set<Address> addresses;
Dois gráficos são descritos para esta entidade: user-only-entity-graph
não seleciona o atributo Addresses
(marcado como lento), enquanto o segundo gráfico diz ao ORM para selecionar esse atributo. Se marcarmos os Addresses
como ansiosos, o gráfico será ignorado e os endereços serão selecionados de qualquer maneira.
Portanto, no JPA 2.1, você pode fazer uma amostra de dados como este:
EntityManager em = entityManagerFactory.createEntityManager(); EntityGraph graph = em.getEntityGraph("user-addresses-entity-graph"); Map<String, Object> properties = Map.of("javax.persistence.fetchgraph", graph); User user = em.find(User.class, 1, properties); em.close();
Essa abordagem simplifica bastante o trabalho, sem a necessidade de pensar separadamente sobre atributos preguiçosos e duração da transação. Um bônus adicional é que o gráfico é aplicado no nível da consulta SQL, para que dados "extras" não sejam selecionados no aplicativo Java. Mas há um pequeno problema: você não pode dizer quais atributos foram selecionados e quais não foram. Há uma API para verificação, isso é feito usando a classe PersistenceUtil
:
PersistenceUtil pu = entityManagerFactory.getPersistenceUnitUtil(); System.out.println("User.addresses loaded: " + pu.isLoaded(user, "addresses"));
Mas isso é muito chato e nem todo mundo está pronto para fazer essas verificações. Existe algo mais que você possa simplificar e simplesmente não mostrar atributos que não foram selecionados?
Projeções de primavera
O Spring Framework tem uma coisa ótima chamada Projeções (e isso não é o mesmo que projeções no Hibernate ). Se você precisar selecionar apenas alguns atributos de uma entidade, uma interface com os atributos necessários será criada e o Spring selecionará "instâncias" dessa interface no banco de dados. Como exemplo, considere a seguinte interface:
interface NamesOnly { String getName(); }
Agora você pode definir um repositório Spring JPA para buscar entidades de Usuário da seguinte maneira:
interface UserRepository extends CrudRepository<User, Integer> { Collection<NamesOnly> findByName(String lastname); }
Nesse caso, depois de chamar o método findByName, na lista resultante, obtemos entidades que têm acesso apenas aos atributos definidos na interface! De acordo com o mesmo princípio, pode-se escolher entidades dependentes, ou seja, selecione imediatamente a relação "mestre-detalhe". Além disso, o Spring gera SQL "correto" na maioria dos casos, ou seja, apenas os atributos descritos na projeção são selecionados no banco de dados, é muito semelhante à forma como os gráficos de entidade funcionam.
Esta é uma API muito poderosa. Ao definir interfaces, você pode usar expressões SpEL, usar classes com alguma lógica interna em vez de interfaces e muito mais, tudo é descrito em detalhes na documentação .
O único problema com as projeções é que elas são implementadas como pares de chave-valor, ou seja, são somente leitura. Isso significa que, mesmo se definirmos um método setter para projeção, não poderemos salvar as alterações nos repositórios CRUD ou no EntityManager. Portanto, as projeções são DTOs que podem ser convertidos novamente em Entidade e salvos apenas se você escrever seu próprio código para isso.
Como selecionar dados no CUBA
Desde o início do desenvolvimento da estrutura CUBA, tentamos otimizar a parte do código que funciona com o banco de dados. No CUBA, usamos o EclipseLink como base para a API de acesso a dados. O que é bom no EclipseLink é que ele suporta o carregamento parcial de entidades desde o início, e esse foi um fator decisivo na escolha entre ele e o Hibernate. No EclipseLink, era possível especificar atributos para carregar muito antes do aparecimento do padrão JPA 2.1. O CUBA possui sua própria maneira de descrever um gráfico de entidade, chamado CUBA Views . Representações O CUBA é uma API bastante desenvolvida, você pode herdar algumas representações de outras, combiná-las, aplicando-se a entidades mestres e detalhadas. Outra motivação para a criação de visualizações CUBA é que queríamos usar transações curtas para podermos trabalhar com entidades desanexadas na interface com o usuário da web.
No CUBA, as visualizações são descritas em um arquivo XML, como no exemplo abaixo:
<view class="com.sample.User" extends="_minimal" name="user-minimal-view"> <property name="name"/> <property name="addresses" view="address-street-only-view"/> </property> </view>
Essa visualização seleciona a entidade User
e seu name
atributo local e também seleciona endereços aplicando a address-street-only-view
. Tudo isso acontece (atenção!) No nível da consulta SQL. Quando a exibição é criada, você pode usá-lo na seleção de dados usando a classe DataManager:
List<User> users = dataManager.load(User.class).view("user-edit-view").list();
Essa abordagem funciona bem, enquanto consome o tráfego de rede economicamente, pois os atributos não utilizados simplesmente não são transferidos do banco de dados para o aplicativo, mas, como no caso da JPA, existe um problema: não se pode dizer quais atributos da entidade foram carregados. E no CUBA há uma exceção “IllegalStateException: Cannot get unfetched attribute [...] from detached object”
, que, como LazyInit
, deve ter sido encontrado por todos que escrevem usando nossa estrutura. Como na JPA, existem maneiras de verificar quais atributos foram carregados e quais não, mas, novamente, escrever essas verificações é uma tarefa tediosa e meticulosa que perturba muito os desenvolvedores. Algo mais precisa ser inventado para não sobrecarregar as pessoas com trabalho que, em teoria, as máquinas podem fazer.
Concept - CUBA View Interfaces
Mas e se você tentar combinar gráficos e projeções de entidades? Decidimos tentar isso e desenvolvemos interfaces para interfaces de exibição de entidade que seguem a abordagem de projeção do Spring. Essas interfaces são convertidas em visualizações CUBA na inicialização do aplicativo e podem ser usadas no DataManager. A ideia é simples: descrevemos uma interface (ou um conjunto de interfaces), que é um gráfico de entidade.
interface UserMinimalView extends BaseEntityView<User, Integer> { String getName(); void setName(String val); List<AddressStreetOnly> getAddresses(); interface AddressStreetOnly extends BaseEntityView<Address, Integer> { String getStreet(); void setStreet(String street); } }
É importante notar que, em alguns casos específicos, é possível criar interfaces locais, como no caso do AddressStreetOnly
do exemplo acima, para não "poluir" a API pública do seu aplicativo.
No processo de iniciar um aplicativo CUBA (a maioria dos quais é a inicialização do contexto Spring), criamos programaticamente visualizações CUBA e as colocamos no repositório de beans interno no contexto.
Agora você precisa modificar levemente a implementação da classe DataManager para que ele aceite as visualizações da interface, e você pode selecionar entidades desta maneira:
List<UserMinimalView> users = dataManager.load(UserMinimalView.class).list();
Sob o capô, é gerado um objeto proxy que implementa a interface e agrupa a instância da entidade selecionada no banco de dados (da mesma maneira que no Hibernate). E, quando o desenvolvedor solicita o valor do atributo, o proxy delega a chamada do método para a instância "real" da entidade.
Ao desenvolver esse conceito, estamos tentando matar dois coelhos com uma cajadada:
- Os dados que não são descritos na interface não são carregados no aplicativo, economizando recursos do servidor.
- O desenvolvedor pode usar apenas os atributos acessíveis através da interface (e, portanto, são selecionados no banco de dados), eliminando as exceções
UnfetchedAttribute
sobre as quais escrevemos acima.
Diferentemente das projeções do Spring, envolvemos entidades em objetos proxy, além disso, cada interface herda a interface CUBA padrão - Entity
. Isso significa que os atributos do Entity View podem ser alterados e salve essas alterações no banco de dados usando a API CUBA padrão para trabalhar com dados.
E, a propósito, a “terceira lebre” - você pode criar atributos somente leitura se definir uma interface apenas com métodos getter. Portanto, já definimos as regras de modificação no nível da API da entidade.
Além disso, você pode executar algumas operações locais para entidades desanexadas usando atributos disponíveis, por exemplo, conversão de cadeia de nomes, como no exemplo abaixo:
@MetaProperty default String getNameLowercase() { return getName().toLowerCase(); }
Observe que os atributos calculados podem ser retirados do modelo de classe de entidade e transferidos para interfaces aplicáveis a uma lógica de negócios específica.
Outra característica interessante é a herança da interface. Você pode fazer várias visualizações com diferentes conjuntos de atributos e combiná-las. Por exemplo, você pode criar uma interface para uma entidade Usuário com os atributos nome e email e outra com os atributos nome e endereço. Agora, se você precisar selecionar nome, email e endereços, não precisará copiar esses atributos para a terceira interface, bastando herdar as duas primeiras visualizações. E sim, instâncias da terceira interface podem ser passadas para métodos que aceitam parâmetros com o tipo de interfaces pai. As regras de OOP são iguais para todos.
Também foi implementada uma conversão entre visualizações - cada interface possui um método reload (), no qual você pode passar a classe de visualização como parâmetro:
UserFullView userFull = userMinimal.reload(UserFullView.class);
UserFullView pode conter atributos adicionais; portanto, a entidade será recarregada do banco de dados, se necessário. E esse processo está atrasado. O acesso ao banco de dados será feito somente quando ocorrer o primeiro acesso aos atributos da entidade. Isso reduzirá a velocidade da primeira chamada um pouco, mas essa abordagem foi escolhida intencionalmente - se a instância da entidade for usada no módulo "web", que contém a interface do usuário e seus próprios controladores REST, esse módulo pode ser implantado em um servidor separado. E isso significa que a sobrecarga forçada da entidade criará tráfego de rede adicional - acesso ao módulo principal e depois ao banco de dados. Assim, adiando a sobrecarga até o momento em que é necessário, economizamos tráfego e reduzimos o número de consultas ao banco de dados.
O conceito foi desenvolvido como um módulo para CUBA, um exemplo de uso pode ser baixado no GitHub .
Conclusão
Parece que em um futuro próximo ainda estaremos usando massivamente o ORM em aplicativos corporativos simplesmente porque precisamos de algo que transformará dados relacionais em objetos. Obviamente, soluções específicas serão desenvolvidas para aplicativos complexos, exclusivos e de carga ultra alta, mas parece que as estruturas ORM permanecerão enquanto os bancos de dados relacionais.
No CUBA, tentamos simplificar ao máximo o trabalho com ORM e, em versões futuras, apresentaremos novos recursos para trabalhar com dados. Será difícil dizer se essas serão interfaces de apresentação ou algo mais, mas tenho certeza de uma coisa: continuaremos a simplificar o trabalho com dados em versões futuras da estrutura.