Como recusei o db4o em um sistema industrial

imagem


Somos um departamento de uma grande empresa que desenvolve um importante sistema Java SE / MS SQL / db4o. Por vários anos, o projeto mudou de um protótipo para operação industrial e o db4o se transformou em um freio de cálculo. Eu queria mudar do db4o para a moderna tecnologia noSQL. Tentativa e erro levaram muito longe do plano original - o db4o foi abandonado com sucesso, mas à custa de um compromisso. Sob o gato reflexões e detalhes de implementação.


A tecnologia db4o está morta?


No Habré, é possível encontrar poucas publicações sobre o db4o. No Stackoverflow, algum tipo de atividade residual é como um novo comentário sobre uma pergunta antiga ou uma pergunta não respondida . O Wiki geralmente acredita que a versão estável atual é de 2011.


Isso forma uma impressão geral: a tecnologia é irrelevante. Houve até uma confirmação oficial : a Actian decidiu não continuar e promover ativamente a oferta comercial de produtos db4o para novos clientes.


Como o db4o foi calculado


O artigo Introdução aos bancos de dados orientados a objetos fala sobre o principal recurso do db4o - a completa ausência de um esquema de dados. Você pode criar qualquer objeto


User user1 = new User("Vasya", "123456", 25); 

e então apenas escreva no arquivo do banco de dados


 db.Store(user1) 

O objeto gravado pode ser recuperado usando o método Query.execute () no formato em que foi salvo.


No início do projeto, isso tornou possível garantir rapidamente a exibição da trilha de auditoria com todos os dados enviados, sem se preocupar com a estrutura das tabelas relacionais. Isso ajudou o projeto a sobreviver. Havia poucos recursos na sandbox e, imediatamente após o final do cálculo de hoje, os dados para amanhã começaram a carregar no MS SQL. Tudo estava mudando constantemente - descubra o que exatamente era servido automaticamente à noite. E o arquivo db4o pode ser acessado
na depuração, extraia um instantâneo do dia desejado e responda à pergunta "enviamos todos os dados, mas você não solicitou nada".


Com o tempo, o problema da sobrevivência desapareceu, o projeto decolou, o trabalho com as solicitações dos usuários mudou. Abra um arquivo db4o na depuração e analise uma pergunta difícil para um desenvolvedor que está sempre ocupado. Em vez disso, há uma multidão de analistas, munidos de uma descrição da lógica da ordem e capazes de usar apenas a parte visível dos dados dos usuários. Logo o db4o começou a ser usado apenas para exibir o histórico do cálculo. Assim como Pareto - uma pequena parte dos recursos fornece a carga principal.


Em operação de combate, o arquivo histórico leva ~ 35 GB / dia, o descarregamento leva cerca de uma hora. O arquivo em si compacta bem (1:10), mas por algum motivo a biblioteca com.db4o.ObjectContainer não executa compactação. No norte do CentOS, a biblioteca com.db4o.query.Query grava / lê um arquivo exclusivamente em um fluxo. Velocidade é um gargalo.


Diagrama esquemático do dispositivo


O modelo de informações do sistema é a hierarquia dos objetos A, B, C e D. A hierarquia não é uma árvore; os links C1 -> B1 são necessários para a operação.


 ROOT || | ==>A1 | || | | ==> B1 <------ | | || | | | | ======> C1 | | | | | | | ===> C1.$D | | =======> C2 | | | | ==> B2 ==> C2.$D | | ===>A2 =======> C3 | | ==> B3 ===> C3.$D | ======> C4 | ===> C4.$D 

O usuário interage com o servidor por meio da interface do usuário (GUI), fornecida por com.sun.net.httpserver.HttpsServer, o cliente e o servidor trocam documentos XML. Na primeira exibição, o servidor atribui um identificador ao nível do usuário, que não muda mais. Se o usuário precisar de um histórico de algum nível, a GUI envia ao servidor um identificador agrupado em XML. O servidor determina os valores das chaves para pesquisar no banco de dados, varre o arquivo db4o pelo dia desejado e recupera o objeto solicitado na memória, além de todos os objetos aos quais se refere. Constrói uma apresentação XML do nível extraído e a retorna ao cliente.


Ao digitalizar um arquivo, o db40, por padrão, lê todos os objetos filhos até uma certa profundidade, extraindo uma hierarquia bastante grande junto com o objeto desejado. O tempo de leitura pode ser reduzido definindo a profundidade mínima de ativação para a classe Foo desnecessária com conf.common (). ObjectClass (Foo.class) .maximumActivationDepth (1).


O uso de classes anônimas leva à criação de referências implícitas à classe anexa a $ 0 . O Db4o processa e restaura esses links corretamente (mas lentamente).


0. Idéia


Portanto, os administradores têm uma expressão estranha no que diz respeito ao suporte ou administração do db4o. A extração de dados é lenta, a tecnologia não é muito animada. Tarefa: em vez de db4o, aplique a tecnologia NoSQL atual. Um par de Spring Data + MongoDB chamou minha atenção.


1. Abordagem frontal


Meu primeiro pensamento foi usar org.springframework.data.mongodb.core.MongoOperations e o método save (), porque se parece com com.db4o.ObjectContainer.db.Store (user1). A documentação do MongoDB diz que os documentos são armazenados em coleções; é lógico apresentar os objetos de sistema necessários como documentos das coleções correspondentes. Também existem anotações @ DBRef que permitem implementar relacionamentos entre documentos em geral, no espírito da 3NF . Vamos lá


1.1 Descarregando. Tipo de referência de chave


O sistema consiste em classes POJO projetadas há muito tempo e sem levar em conta todas essas novas tecnologias. Os campos do tipo Mapa <POJO, POJO> são usados, existe uma lógica ramificada de trabalhar com eles. Salve este campo, recebo um erro


 org.springframework.data.mapping.MappingException: Cannot use a complex object as a key value. 

Nesta ocasião, apenas a correspondência de 2011 foi encontrada , na qual foi proposto o desenvolvimento do MappingMongoConverter fora do padrão. Observamos até agora os campos problemáticos @ Transient, continuo. Acabou economizando, estudando o resultado.


O salvamento ocorre na coleção, cujo nome coincide com o nome da classe salva. Ainda não usei anotações do @DBRef, portanto, existe apenas uma coleção, pois os documentos JSON são bastante grandes e ramificados. Percebo que, quando você salva o objeto, o MongoOperations passa por todos os links não vazios (herdados) e os grava como um documento anexado.


1.2 Descarregando. Campo ou matriz nomeado?


O modelo do sistema é tal que a classe C pode conter uma referência à mesma classe D várias vezes. Em um campo defaultMode separado e entre outros links em ArrayList, algo como isto

 public class C { private D defaultMode; private List<D> listOfD = new ArrayList<D>(); public class D { .. } public C(){ this.defaultMode = new D(); listOfD.add(defaultMode); } } 

Após o descarregamento, o documento JSON terá duas cópias: um documento anexado chamado defaultMode e um elemento sem nome da matriz de documentos. No primeiro caso, o documento pode ser acessado pelo nome, no segundo - pelo nome da matriz com um índice. Você pode pesquisar coleções do MongoDB nos dois casos. Trabalhando apenas com o Spring Data e o MongoDB, cheguei à conclusão de que você pode usar ArrayList, se cuidadosamente; Eu não notei nenhuma restrição no uso de matrizes. Os recursos apareceram mais tarde, no nível do MongoDB Connector for BI.


1.3 Baixar Argumentos do construtor


Estou tentando ler um documento salvo usando o método MongoOperations.findOne (). Carregar o objeto A do banco de dados lança uma exceção


 "No property name found on entity class A to bind constructor parameter to!" 

Verificou-se que a classe possui um campo corpName e o construtor possui um parâmetro String name, e this.corpName = name é atribuído no corpo do construtor. MongoOperations requer que os nomes de campo nas classes correspondam aos nomes dos argumentos do construtor. Se houver vários construtores, você precisará selecionar um com a anotação @PersistenceConstructor. Trago os nomes dos campos e parâmetros em correspondência.


1.4 Baixar Com $ D e este $ 0


A classe aninhada interna D encapsula o comportamento padrão da classe C e não faz sentido separadamente da classe C. Uma instância de D é criada para cada instância de C e vice-versa - para cada instância de D existe uma instância de C. A classe D ainda possui descendentes que implementam comportamentos alternativos e podem ser armazenados em listOfD. O construtor das classes descendentes de D requer a presença de um objeto C. já existente.


Além das classes internas aninhadas, o sistema usa classes internas anônimas . Como você sabe , os dois contêm uma referência implícita a uma instância de uma classe envolvente. Ou seja, como parte de cada instância do objeto CD, o compilador cria um link como $ 0, que aponta para o objeto pai C.


Mais uma vez, tento ler o documento salvo da coleção e obtenho uma exceção


 "No property this$0 found on entity class $D to bind constructor parameter to!" 

Lembro-me de que os métodos da classe D usam as referências C.this.fieldOfClassC com poder e principal, e os descendentes da classe D exigem que o construtor seja instanciado com C como argumento. Ou seja, preciso fornecer uma certa ordem de criação de objetos no MongoOperations para que o objeto pai C possa ser especificado no construtor D. Novamente, o MappingMongoConverter não padrão?


Talvez não usando classes anônimas e normalizando as classes internas? Refinar, ou melhor, refinar a arquitetura de um sistema já implementado é uma tarefa uau ...


2. Abordagem da 3NF / @ DBRef


Eu tento ir por outro lado, salvar cada classe da minha coleção e fazer conexões entre elas no espírito da 3NF.


2.1 Descarregando. @DBRef is beautiful


A classe C contém várias referências a D. Se os links defaultMode e ArrayList estiverem marcados como @DBRef, o tamanho do documento diminuirá. Em vez de grandes documentos anexados, haverá links legais. No campo, o documento json da coleção C é exibido

 "defaultMode" : DBRef("D", ObjectId("5c496eed2c9c212614bb8176")) 

No banco de dados MongoDB, uma coleção D é criada automaticamente e um documento com um campo


 "_id" : ObjectId("5c496eed2c9c212614bb8176") 

Tudo é simples e bonito.


2.2 Baixar Construtor Classe D


Ao trabalhar com links, o objeto C sabe que o objeto D padrão é criado exatamente uma vez. Se você precisar ignorar todos os objetos D, exceto o padrão, basta comparar os links:


 private D defaultMode; private ArrayList<D> listOfD; for (D currentD: listOfD){ if (currentD == defaultMode) continue; doSomething(currentD); } 

Eu chamo findOne (), estudo minha classe C. Acontece que o MongoOperations lê um documento json e chama o construtor D para cada anotação @DBRef que encontra, sempre que cria um novo objeto. Eu recebo uma construção estranha - duas referências diferentes para D no campo defaultMode e na matriz listOfD, onde o link deve ser o mesmo.


Aprendendo com a comunidade : "Na minha opinião, o Dbref deve ser evitado ao trabalhar com o mongodb." Outra consideração na mesma linha da documentação oficial: o modelo de dados desnormalizados onde os dados relacionados são armazenados em um único documento será ideal para resolver DBRefs, seu aplicativo deve executar consultas adicionais para retornar os documentos referenciados.


A página de documentação mencionada diz bem no início: "Para muitos casos de uso no MongoDB, o modelo de dados desnormalizados onde os dados relacionados são armazenados em um único documento será ideal". Está escrito para mim?


O foco no designer sugere que você não precisa pensar em um DBMS relacional. A escolha é:


  • se você especificar @DBRef:
    • o construtor de cada anotação será chamado e vários objetos idênticos serão criados;
    • O MongoOperations encontrará e lerá todos os documentos de todas as coleções relacionadas. Haverá uma solicitação para o índice por ObjectId e, em seguida, a leitura de muitas coleções de um banco de dados (grande);
  • se você não especificar, o json "anormal" será salvo com repetições dos mesmos dados.

Observo por mim mesmo: você não pode confiar no @DBRef, mas use um campo do tipo ObjectId, preenchendo-o manualmente. Nesse caso, em vez de


 "defaultMode" : DBRef("D", ObjectId("5c496eed2c9c212614bb8176")) 

documento json conterá


 "defaultMode" : ObjectId("5c496eed2c9c212614bb8176") 

Não haverá carregamento automático - o MongoOperations não sabe em qual coleção procurar um documento. O documento precisará ser carregado em uma solicitação separada (preguiçosa) indicando a coleção e o ObjectId. Uma única consulta deve retornar o resultado rapidamente; além disso, um índice automático é criado para cada coleção pelo ObjectId.


2.3 E agora?


Subtotais. Não foi possível implementar rápida e facilmente a funcionalidade db4o no MongoDB:


  • Não está claro como usar um POJO personalizado como chave da lista Chave - Valor;
  • Não está claro como definir a ordem na qual os objetos são criados no MappingMongoConverter;
  • não está claro se é necessário fazer upload de um documento "não normalizado" sem o DBRef e se é necessário criar seu próprio mecanismo para inicialização lenta.

Você pode adicionar carregamento lento. Você pode tentar fazer o MappingMongoConverter. Você pode modificar construtores / campos / listas existentes. Mas há muitos anos em camadas da lógica de negócios - não uma alteração fraca e o risco de nunca ser testado.


Solução de compromisso: criar um novo mecanismo para salvar dados para o problema que está sendo resolvido, mantendo o mecanismo para interagir com a GUI.


3. A terceira tentativa, a experiência dos dois primeiros


Pareto sugere que resolver problemas com a velocidade dos usuários significará o sucesso de toda a tarefa. A tarefa é a seguinte: você precisa aprender como salvar e restaurar rapidamente os dados de apresentação do usuário sem o db4o.


Isso perderá a capacidade de examinar o objeto salvo na depuração. Por um lado, isso é ruim. Por outro lado, essas tarefas raramente ocorrem e, no git, todas as entregas de combate são marcadas. Para tolerância a falhas, antes de descarregar, o sistema serializa o cálculo em um arquivo. Se você precisar examinar um objeto na depuração, poderá realizar a serialização, clonar o conjunto do sistema correspondente e restaurar o cálculo.


3.1 Dados de apresentação personalizados


Para criar apresentações dos níveis do usuário, o sistema possui uma classe Visualizador especial. O método Viewer.getXML () recebe um nível como entrada, extrai dele os valores numéricos e de sequência necessários e gera XML.


Se o usuário pediu para mostrar o nível do cálculo de hoje, o nível será encontrado na RAM. Para mostrar um cálculo do passado, o método com.db4o.query.Query.execute () encontrará o nível no arquivo. O nível do arquivo quase não é diferente do nível criado e o Viewer criará a apresentação sem perceber a substituição.


Para resolver meu problema, preciso de um intermediário entre o nível de cálculo e sua apresentação - a estrutura de apresentação (Quadro), que armazenará dados e se baseará nos dados XML disponíveis. A cadeia de ações para criar a apresentação ficará mais longa, sempre que um quadro for gerado e o quadro irá gerar XML:


  : < > -> Viewer.getXML() : < > -> Viewer.getFrame() -> Frame.getXML() 

Ao salvar a história, você precisará criar quadros de todos os níveis e gravar no banco de dados.


3.2 Descarregando


A tarefa era relativamente simples e não havia problemas. Repetindo a estrutura da apresentação XML, o quadro recebeu um dispositivo recursivo na forma de uma hierarquia de elementos com os campos String, Inteiro e Duplo. O quadro solicita getXML () de todos os seus elementos, o coleta em um único documento e retorna. O MongoOperations fez um ótimo trabalho com a natureza recursiva do quadro e não fez novas perguntas à medida que avançava.


Finalmente, tudo decolou! Por padrão, o mecanismo WiredTiger compacta as coleções de documentos do MongoDB; no sistema de arquivos, o descarregamento leva cerca de 3,5 GB por dia. Uma diminuição de dez vezes em relação ao db4o não é ruim.


No início, o descarregamento era organizado de maneira simples - uma travessia recursiva da árvore de níveis, MongoOperations.save () para cada um. Esse descarregamento levou 5,5 horas, e isso apesar do fato de a construção de apresentações envolver apenas a leitura de objetos. Eu adiciono multithreading: atravesse recursivamente a árvore de níveis, divida todos os níveis disponíveis em pacotes de um determinado tamanho, crie implementações Callable.call () de acordo com o número de pacotes, transfira cada pacote para nosso próprio pacote e faça tudo isso através de ExecutorService.invokeAll ().


O MongoOperations novamente não fez perguntas e fez um ótimo trabalho com o modo multiencadeado. Empiricamente selecionado o tamanho da embalagem, oferecendo a melhor velocidade de descarga. Foram 15 minutos para um pacote de 1000 níveis.


3.3 Mongo BI Connector, ou como as pessoas trabalham com ele


A linguagem de consulta do MongoDB é grande e poderosa; eu inevitavelmente adquiri experiência trabalhando com ela, chegando a esse lugar. O console suporta JavaScript, você pode escrever designs bonitos e poderosos. Este é um lado. Por outro lado, posso quebrar o cérebro de uma boa metade dos colegas analistas com um pedido


 db.users.find( { numbers: { $in: [ 390, 754, 454 ] } } ); 

em vez do habitual


 SELECT * FROM users WHERE numbers IN (390, 754, 454) 

O MongoDB Connector for BI vem em socorro, através do qual você pode apresentar documentos de coleção em forma de tabela. O banco de dados MongoDB é chamado de banco de dados baseado em documentos, não sabe como apresentar uma hierarquia de campos / documentos em forma de tabela. Para o conector funcionar, é necessário descrever a estrutura da tabela futura em um arquivo .drdl separado, cujo formato é muito semelhante ao yaml. No arquivo, você deve especificar a correspondência entre o campo da tabela relacional na saída e o caminho para o campo do documento JSON na entrada.


3.4 Recursos usando matrizes


Foi dito acima que para o próprio MongoDB não há diferença especial entre uma matriz e um campo. Da perspectiva do conector, uma matriz é muito diferente de um campo nomeado; Eu até tive que refatorar a classe Frame concluída. Uma matriz de documentos deve ser usada apenas quando for necessário colocar parte das informações em uma tabela vinculada.


Se o documento JSON for uma hierarquia de campos nomeados, qualquer campo poderá ser acessado especificando o caminho da raiz do documento por um período, por exemplo, xy. Se a correspondência xy => fieldXY for especificada no arquivo DRDL, a tabela de saída terá o número de linhas que houver documentos na coleção na entrada. Se em algum documento não houver um campo xy, NULL estará na linha correspondente da tabela.


Suponha que tenhamos um banco de dados do MongoDB chamado Frames, existe uma coleção A no banco de dados e o MongoOperations gravou duas instâncias da classe A. para essa coleção. Estes são os documentos: first


 { "_id": ObjectId("5cdd51e2394faf88a01bd456"), "x": { "y": "xy string value 1"}, "days": [{ "k": "0", "v": 0.0 }, { "k": "1", "v": 0.1 }], "_class": "A" } 

e segundo (ObjectId difere pelo último dígito):


 { "_id": ObjectId("5cdd51e2394faf88a01bd457"), "x": { "y": "xy string value 2"}, "days": [{ "k": "0", "v": 0.3 }, { "k": "1", "v": 0.4 }], "_class": "A" } 

O conector de BI não pode acessar os elementos da matriz por índice e é simplesmente impossível extrair, por exemplo, o campo days [1] .v da matriz para a tabela. Em vez disso, o conector pode representar cada elemento da matriz de dias como uma linha em uma tabela separada usando o operador $ unbind . Essa tabela separada será associada ao relacionamento um para muitos original por meio do identificador de linha. No nosso exemplo, as tabelas tableA são definidas para documentos de coleção e tableA_days para documentos da matriz de dias. O arquivo .drdl aparece assim:


 schema: - db: Frames tables: - table: tableA collection: A pipeline: [] columns: - Name: _id MongoType: bson.ObjectId SqlName: _id SqlType: objectid - Name: xy MongoType: string SqlName: fieldXY SqlType: varchar - table: tableA_days collection: A pipeline: - $unwind: path: $days columns: - Name: _id #   MongoType: bson.ObjectId SqlName: tableA_id SqlType: objectid - Name: days.k MongoType: string SqlName: tableA_dayNo SqlType: varchar - Name: days.v MongoType: string SqlName: tableA_dayVal SqlType: varchar 

O conteúdo das tabelas será: table tableA


_idfieldXY
5cdd51e2394faf88a01bd456valor da cadeia xy 1
5cdd51e2394faf88a01bd457valor da string xy 2

e tabela tableA_days


tableA_idtableA_dayNotableA_dayVal
5cdd51e2394faf88a01bd4560 00,0
5cdd51e2394faf88a01bd45610,1
5cdd51e2394faf88a01bd4570 00,3
5cdd51e2394faf88a01bd45710,4

Total


Não foi possível implementar a tarefa na formulação original; você não pode simplesmente pegar e substituir o db4o pelo MongoDB. O MongoOperations não pode restaurar automaticamente nenhum objeto como o db4o. Provavelmente, você pode fazer isso, mas os custos de mão-de-obra não serão comparáveis ​​a chamar os métodos de armazenamento / consulta da biblioteca db4o.


Trilha de auditoria. O Db4o é uma ferramenta muito útil no início de um projeto. Você pode simplesmente escrever o objeto, depois restaurá-lo e, ao mesmo tempo, sem preocupações e tabelas. Tudo isso com uma ressalva importante: se você precisar alterar a hierarquia de classes (adicione a classe E entre A e B), todas as informações armazenadas anteriormente se tornarão ilegíveis. Mas, para iniciar um projeto, isso não é muito importante, desde que não haja uma grande matriz acumulada de arquivos antigos.


Quando havia experiência suficiente com o MongoOperations, a gravação do upload não causava problemas. Escrever um novo código para a estrutura é muito mais fácil do que refazer o antigo, que também é colocado em produção.

Source: https://habr.com/ru/post/pt461417/


All Articles