Comment j'ai refusé DB4O dans un système industriel

image


Nous sommes un département de grande entreprise développant un important système Java SE / MS SQL / db4o. Pendant plusieurs années, le projet est passé d'un prototype à une exploitation industrielle et db4o s'est transformé en frein de calcul, j'ai voulu passer de db4o à la technologie noSQL moderne. Les essais et les erreurs ont conduit loin du plan d'origine - il était possible de refuser db4o, mais au prix d'un compromis. Sous les réflexions du chat et les détails d'implémentation


La technologie db4o est-elle morte?


Sur Habré, il est possible de trouver moins de publications sur db4o. Sur Stackoverflow, une sorte d'activité résiduelle est comme un nouveau commentaire sur une ancienne question ou une nouvelle question sans réponse . Wiki pense généralement que la version stable actuelle est datée de 2011.


Cela donne une impression générale: la technologie n'est pas pertinente. Il y a même eu une confirmation officielle : Actian a décidé de ne plus poursuivre et promouvoir activement l'offre commerciale de produits db4o auprès de nouveaux clients.


Comment db4o a-t-il été calculé


L'article Introduction aux bases de données orientées objet parle de la principale caractéristique de db4o - l'absence totale d'un schéma de données. Vous pouvez créer n'importe quel objet


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

puis il suffit de l'écrire dans le fichier de base de données


 db.Store(user1) 

L'objet enregistré peut ensuite être récupéré à l'aide de la méthode Query.execute () sous la forme dans laquelle il a été enregistré.


Au début du projet, cela a permis d'assurer rapidement l'affichage de la piste d'audit avec toutes les données soumises, sans se soucier de la structure des tables relationnelles. Cela a aidé le projet à survivre. Ensuite, il y avait peu de ressources dans le bac à sable, et immédiatement après la fin du calcul d'aujourd'hui, les données de demain ont commencé à se charger dans MS SQL. Tout changeait constamment - allez découvrir ce qui était servi automatiquement la nuit. Et le fichier db4o est accessible
lors du débogage, extrayez un instantané du jour souhaité et répondez à la question "nous avons soumis toutes les données, mais vous n'avez rien commandé".


Au fil du temps, le problème de survie a disparu, le projet a décollé, le travail avec les demandes des utilisateurs a changé. Ouvrez un fichier db4o en débogage et analysez une question difficile pour un développeur toujours occupé. Au lieu de cela, il y a une foule d'analystes, armés d'une description de la logique de commande et capables d'utiliser uniquement la partie visible par l'utilisateur des données. Bientôt, db4o a commencé à être utilisé uniquement pour afficher l'historique du calcul. Tout comme Pareto - une petite partie des capacités fournit la charge principale.


En opération de combat, le fichier historique prend ~ 35 Go / jour, le déchargement prend environ une heure. Le fichier lui-même se comprime bien (1:10), mais pour une raison quelconque, la bibliothèque com.db4o.ObjectContainer n'effectue pas de compression. Au nord de CentOS, la bibliothèque com.db4o.query.Query écrit / lit un fichier exclusivement dans un flux. La vitesse est un goulot d'étranglement.


Schéma de principe de l'appareil


Le modèle d'information du système est la hiérarchie des objets A, B, C et D. La hiérarchie n'est pas un arbre; les liens C1 -> B1 sont nécessaires pour le fonctionnement.


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

L'utilisateur interagit avec le serveur via l'interface utilisateur (GUI), qui est fournie par com.sun.net.httpserver.HttpsServer, le client et le serveur échangent des documents XML. Lors du premier affichage, le serveur attribue un identifiant au niveau utilisateur, qui ne change plus. Si l'utilisateur a besoin d'un historique d'un certain niveau, l'interface graphique envoie au serveur un identificateur enveloppé XML. Le serveur détermine les valeurs des clés de recherche dans la base de données, analyse le fichier db4o pour le jour souhaité et récupère l'objet demandé en mémoire ainsi que tous les objets auxquels il se réfère. Construit une présentation XML du niveau extrait et la renvoie au client.


Lors de l'analyse d'un fichier, db40 lit par défaut tous les objets enfants à une certaine profondeur, en extrayant une hiérarchie assez large avec l'objet souhaité. Le temps de lecture peut être réduit en définissant la profondeur d'activation minimale pour la classe Foo inutile avec conf.common (). ObjectClass (Foo.class) .maximumActivationDepth (1).


L'utilisation de classes anonymes conduit à la création de références implicites à la classe englobante this $ 0 . Db4o traite et restaure ces liens correctement (mais lentement).


0. Idée


Ainsi, les administrateurs ont une expression étrange sur leur visage lorsqu'il s'agit de soutenir ou d'administrer db4o. L'extraction des données est lente, la technologie n'est pas très vivante. Tâche: au lieu de db4o, appliquez la technologie NoSQL actuelle. Une paire de Spring Data + MongoDB a attiré mon attention.


1. Approche frontale


Ma première pensée a été d'utiliser org.springframework.data.mongodb.core.MongoOperations et la méthode save (), car elle ressemble à com.db4o.ObjectContainer.db.Store (user1). La documentation MongoDB indique que les documents sont stockés dans des collections, il est logique de présenter les objets système nécessaires en tant que documents des collections correspondantes. Il existe également des annotations @ DBRef qui vous permettent d'implémenter les relations entre les documents en général dans l'esprit de 3NF . Allons-y.


1.1. Déchargement. Type de référence de clé


Le système se compose de classes POJO conçues depuis longtemps et sans prendre en compte toutes ces nouvelles technologies. Des champs de type Map <POJO, POJO> sont utilisés, il y a une logique branchée de travailler avec eux. J'enregistre ce champ, j'obtiens une erreur


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

A cette occasion, seule la correspondance de 2011 a été trouvée , dans laquelle il était proposé de développer le MappingMongoConverter non standard. A noté jusqu'ici les champs problématiques @ Transitoire, je continue. Il s'est avéré sauver, en étudiant le résultat.


L'enregistrement se produit dans la collection, dont le nom coïncide avec le nom de la classe enregistrée. Je n'ai pas encore utilisé d'annotations @DBRef, donc il n'y a qu'une seule collection, les documents JSON sont assez volumineux et ramifiés. Je remarque que lorsque vous enregistrez l'objet, MongoOperations parcourt tous les liens (y compris hérités) non vides et les écrit en tant que document joint.


1.2. Déchargement. Champ ou tableau nommé?


Le modèle de système est tel que la classe C peut contenir plusieurs fois une référence à la même classe D. Dans un champ defaultMode distinct et parmi d'autres liens dans ArrayList, quelque chose comme ceci

 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); } } 

Après le déchargement, le document JSON en aura deux copies: un document joint nommé defaultMode et un élément sans nom du tableau de documents. Dans le premier cas, le document est accessible par nom, dans le second - par le nom du tableau avec un index. Vous pouvez rechercher des collections MongoDB dans les deux cas. En travaillant uniquement avec Spring Data et MongoDB, je suis arrivé à la conclusion que vous pouvez utiliser ArrayList, si vous le faites avec soin; Je n'ai pas remarqué les restrictions d'utilisation des tableaux Les fonctionnalités sont apparues plus tard, au niveau du connecteur MongoDB pour BI.


1.3. Téléchargez Arguments du constructeur


J'essaie de lire un document enregistré en utilisant la méthode MongoOperations.findOne (). Le chargement de l'objet A à partir de la base de données lève une exception


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

Il s'est avéré que la classe a un champ corpName, et le constructeur a un paramètre String name, et this.corpName = name est assigné dans le corps du constructeur. MongoOperations requiert que les noms de champ dans les classes correspondent aux noms des arguments du constructeur. S'il existe plusieurs constructeurs, vous devez en sélectionner un avec l'annotation @PersistenceConstructor. J'apporte les noms des champs et des paramètres en correspondance.


1.4. Téléchargez Avec $ D et ce $ 0


La classe intérieure imbriquée D encapsule le comportement par défaut de la classe C et n'a pas de sens séparément de la classe C. Une instance de D est créée pour chaque instance de C et vice versa - pour chaque instance de D, il existe une instance de C qui l'a générée. La classe D a des descendants qui implémentent des comportements alternatifs et peuvent être stockés dans listOfD. Le constructeur des classes descendantes de D nécessite la présence d'un objet C déjà existant.


En plus des classes internes imbriquées, le système utilise des classes internes anonymes . Comme vous le savez , les deux contiennent une référence implicite à une instance d'une classe englobante. Autrement dit, dans le cadre de chaque instance de l'objet CD, le compilateur crée un lien ce $ 0, qui pointe vers l'objet parent C.


Encore une fois, j'essaie de lire le document enregistré de la collection et d'obtenir une exception


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

Je rappelle que les méthodes de la classe D utilisent les références C.this.fieldOfClassC avec might et main, et les descendants de la classe D nécessitent que le constructeur soit instancié avec C comme argument. Autrement dit, je dois fournir un certain ordre de création d'objets dans MongoOperations afin que l'objet parent C puisse être spécifié dans le constructeur D. Encore une fois, le MappingMongoConverter non standard?


Peut-être ne pas utiliser de classes anonymes et rendre les classes internes normales? Affiner, ou plutôt affiner l'architecture d'un système déjà implémenté est une tâche épatante ...


2. Approche de 3NF / @ DBRef


D'un autre côté, j'essaie d'aller, de sauvegarder chaque classe de ma collection et de faire des liens entre elles dans l'esprit de 3NF.


2.1. Déchargement. @DBRef is beautiful


La classe C contient plusieurs références à D. Si les liens defaultMode et ArrayList sont marqués comme @DBRef, la taille du document diminuera, au lieu d'énormes documents joints, il y aura des liens soignés. Dans le champ json, le document de la collection C apparaît

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

Dans la base de données MongoDB, une collection D est automatiquement créée et un document avec un champ dedans


 "_id" : ObjectId("5c496eed2c9c212614bb8176") 

Tout est simple et beau.


2.2. Téléchargez Constructeur de classe D


Lorsque vous travaillez avec des liens, l'objet C sait que l'objet D par défaut est créé exactement une fois. Si vous devez contourner tous les objets D sauf celui par défaut, comparez simplement les liens:


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

J'appelle findOne (), j'étudie ma classe C. Il s'avère que MongoOperations lit un document json et appelle le constructeur D pour chaque annotation @DBRef qu'il rencontre, créant à chaque fois un nouvel objet. J'obtiens une construction étrange - deux références différentes à D dans le champ defaultMode et dans le tableau listOfD, où le lien doit être le même.


Apprendre de la communauté : "Dbref à mon avis devrait être évité lorsque vous travaillez avec mongodb." Une autre considération dans la même veine de la documentation officielle: le modèle de données dénormalisé où les données connexes sont stockées dans un seul document sera optimal pour résoudre les DBRefs, votre application doit effectuer des requêtes supplémentaires pour renvoyer les documents référencés.


La page de documentation mentionnée indique au tout début: "Pour de nombreux cas d'utilisation dans MongoDB, le modèle de données dénormalisé où les données associées sont stockées dans un seul document sera optimal." Est-ce écrit pour moi?


Concentrez-vous avec le concepteur suggère que vous n'avez pas besoin de penser comme dans un SGBD relationnel. Le choix est:


  • si vous spécifiez @DBRef:
    • le constructeur de chaque annotation sera appelé et plusieurs objets identiques seront créés;
    • MongoOperations trouvera et lira tous les documents de toutes les collections associées. Il y aura une demande à l'index par ObjectId, puis la lecture de nombreuses collections d'une (grande) base de données;
  • si vous ne spécifiez pas, le json "anormal" sera enregistré avec des répétitions des mêmes données.

Je note par moi-même: vous ne pouvez pas compter sur @DBRef, mais utilisez un champ de type ObjectId, en le remplissant manuellement. Dans ce cas, au lieu de


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

le document json contiendra


 "defaultMode" : ObjectId("5c496eed2c9c212614bb8176") 

Il n'y aura pas de chargement automatique - MongoOperations ne sait pas dans quelle collection rechercher un document. Le document devra être chargé dans une demande distincte (paresseuse) indiquant la collection et ObjectId. Une seule requête doit retourner le résultat rapidement, en outre, un index automatique est créé pour chaque collection par ObjectId.


2.3. Et maintenant?


Sous-totaux. Il n'a pas été possible d'implémenter rapidement et facilement la fonctionnalité db4o sur MongoDB:


  • Il n'est pas clair comment utiliser un POJO personnalisé comme clé - Liste de valeurs;
  • On ne sait pas comment définir l'ordre dans lequel les objets sont créés dans MappingMongoConverter;
  • il n'est pas clair s'il faut télécharger un document "non normalisé" sans DBRef et s'il est nécessaire de trouver son propre mécanisme pour l'initialisation paresseuse.

Vous pouvez ajouter un chargement paresseux. Vous pouvez essayer de faire MappingMongoConverter. Vous pouvez modifier les constructeurs / champs / listes existants. Mais il y a de nombreuses années de stratification de la logique métier - pas une altération faible et le risque de ne jamais être testé.


Solution de compromis: créer un nouveau mécanisme d'enregistrement des données pour le problème résolu, tout en conservant le mécanisme d'interaction avec l'interface graphique.


3. La troisième tentative, l'expérience des deux premiers


Pareto suggère que résoudre des problèmes avec la vitesse des utilisateurs signifiera le succès de toute la tâche. La tâche est la suivante: vous devez apprendre à enregistrer et restaurer rapidement les données de présentation utilisateur sans db4o.


Cela perdra la possibilité d'examiner l'objet enregistré lors du débogage. D'une part, c'est mauvais. D'un autre côté, de telles tâches se produisent rarement, et dans git toutes les livraisons de combat sont marquées. Pour la tolérance aux pannes, à chaque fois avant le déchargement, le système sérialise le calcul dans un fichier. Si vous devez examiner un objet lors du débogage, vous pouvez prendre la sérialisation, cloner l'assembly système correspondant et restaurer le calcul.


3.1. Données de présentation personnalisées


Pour créer des présentations de niveaux utilisateur, le système dispose d'une classe Viewer spéciale. La méthode Viewer.getXML () reçoit un niveau en entrée, en extrait les valeurs numériques et de chaîne nécessaires et génère du XML.


Si l'utilisateur a demandé d'afficher le niveau du calcul d'aujourd'hui, alors le niveau sera trouvé dans la RAM. Pour afficher un calcul du passé, la méthode com.db4o.query.Query.execute () trouvera le niveau dans le fichier. Le niveau du fichier n'est presque pas différent du niveau qui vient d'être créé et Viewer construira la présentation sans remarquer la substitution.


Pour résoudre mon problème, j'ai besoin d'un intermédiaire entre le niveau de calcul et sa présentation - le cadre de présentation (Frame), qui stockera les données et s'appuiera sur les données XML disponibles. La chaîne d'actions pour la construction de la présentation s'allongera, chaque fois qu'un cadre sera généré et que le cadre générera du XML:


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

Lors de l'enregistrement de l'histoire, vous devrez créer des cadres de tous les niveaux et écrire dans la base de données.


3.2. Déchargement


La tâche était relativement simple et ne posait aucun problème. Répétant la structure de la présentation XML, le cadre a reçu un dispositif récursif sous la forme d'une hiérarchie d'éléments avec les champs String, Integer et Double. Le cadre demande getXML () à tous ses éléments, le recueille dans un seul document et retourne. MongoOperations a fait un excellent travail avec la nature récursive du cadre et n'a pas posé de nouvelles questions à mesure qu'il progressait.


Enfin, tout a décollé! Le moteur WiredTiger compresse par défaut les collections de documents MongoDB; sur le système de fichiers, le déchargement prenait environ 3,5 Go par jour. Une diminution de dix fois par rapport à db4o n'est pas mauvaise.


Au début, le déchargement était organisé simplement - une traversée récursive de l'arbre de niveau, MongoOperations.save () pour chacun. Un tel déchargement a pris 5,5 heures, et ce malgré le fait que la construction de présentations implique uniquement la lecture d'objets. J'ajoute le multithreading: parcourir récursivement l'arborescence des niveaux, diviser tous les niveaux disponibles en packages d'une certaine taille, créer des implémentations Callable.call () en fonction du nombre de packages, transférer chaque package vers notre propre package et tout faire via ExecutorService.invokeAll ().


MongoOperations n'a de nouveau posé aucune question et a fait un excellent travail avec le mode multi-thread. Empiriquement sélectionné la taille de l'emballage, donnant la meilleure vitesse de déchargement. Il s'est avéré 15 minutes pour un package de 1000 niveaux.


3.3. Mongo BI Connector, ou comment les gens travaillent avec lui


Le langage de requête MongoDB est grand et puissant, j'ai inévitablement acquis une expérience de travail avec lui, atteignant cet endroit. La console prend en charge JavaScript, vous pouvez écrire des designs magnifiques et puissants. Ceci est un côté. De l'autre, je peux briser le cerveau d'une bonne moitié de collègues analystes avec une demande


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

au lieu de l'habituel


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

MongoDB Connector for BI vient à la rescousse, à travers lequel vous pouvez présenter des documents de collection sous forme de tableau. La base de données MongoDB est appelée base de données documentaire, elle ne sait pas présenter une hiérarchie de champs / documents sous forme tabulaire. Pour que le connecteur fonctionne, il est nécessaire de décrire la structure de la future table dans un fichier .drdl distinct, dont le format est très similaire à yaml. Dans le fichier, vous devez spécifier la correspondance entre le champ de la table relationnelle en sortie et le chemin d'accès au champ du document JSON en entrée.


3.4. Fonctionnalités utilisant des tableaux


Il a été dit plus haut que pour MongoDB lui-même, il n'y a pas de différence particulière entre un tableau et un champ. Du point de vue du connecteur, un tableau est très différent d'un champ nommé; J'ai même dû refactoriser la classe Frame terminée. Un tableau de documents ne doit être utilisé que lorsqu'il est nécessaire de placer une partie des informations dans un tableau lié.


Si le document JSON est une hiérarchie de champs nommés, n'importe quel champ est accessible en spécifiant le chemin depuis la racine du document jusqu'à un point, par exemple xy. Si la correspondance xy => fieldXY est spécifiée dans le fichier DRDL, la table de sortie aura autant de lignes qu'il y a de documents dans la collection à l'entrée. Si dans certains documents, il n'y a pas de champ xy, NULL sera dans la ligne correspondante du tableau.


Supposons que nous ayons une base de données MongoDB appelée Frames, il y a une collection A dans la base de données et MongoOperations a écrit deux instances de classe A dans cette collection. Ce sont les documents: d'abord


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

et deuxième (ObjectId diffère par le dernier chiffre):


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

Le connecteur BI n'est pas en mesure d'accéder aux éléments du tableau par index, et il est tout simplement impossible d'extraire, par exemple, le champ days [1] .v du tableau dans le tableau. Au lieu de cela, le connecteur peut représenter chaque élément du tableau jours sous la forme d'une ligne dans une table distincte à l'aide de l'opérateur $ unwind . Cette table distincte sera associée à la relation un-à-plusieurs d'origine via l'identificateur de ligne. Dans notre exemple, les tables tableA sont définies pour les documents de collection et tableA_days pour les documents du tableau jours. Le fichier .drdl ressemble à ceci:


 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 

Le contenu des tableaux sera: table tableA


_idfieldXY
5cdd51e2394faf88a01bd456valeur de chaîne xy 1
5cdd51e2394faf88a01bd457valeur de chaîne xy 2

et table tableA_days


tableA_idtableA_dayNotableA_dayVal
5cdd51e2394faf88a01bd45600,0
5cdd51e2394faf88a01bd45610,1
5cdd51e2394faf88a01bd45700,3
5cdd51e2394faf88a01bd45710,4

Total


Il n'a pas été possible d'implémenter la tâche dans la formulation d'origine; vous ne pouvez pas simplement prendre et remplacer db4o par MongoDB. MongoOperations n'est pas en mesure de restaurer automatiquement un objet tel que db4o. Vous pouvez probablement le faire, mais les coûts de main-d'œuvre ne seront pas comparables à l'appel des méthodes store / query de la bibliothèque db4o.


Piste d'audit. Db4o est un outil très utile au début d'un projet. Vous pouvez simplement écrire l'objet, puis le restaurer et en même temps sans soucis ni tableaux. Tout cela avec une mise en garde importante: si vous devez changer la hiérarchie des classes (ajoutez la classe E entre A et B), alors toutes les informations précédemment stockées deviennent illisibles. Mais pour démarrer un projet, ce n'est pas très important, tant qu'il n'y a pas de grand tableau accumulé d'anciens fichiers.


Lorsqu'il y avait suffisamment d'expérience avec MongoOperations, l'écriture du téléchargement n'a pas causé de problèmes. Il est beaucoup plus facile d'écrire un nouveau code pour le framework que de refaire l'ancien, qui est également mis en production.

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


All Articles