Mon adresse n'est pas une maison ou une rue, mon adresse est l'Union soviétique?

microBIGDATA ou FIAS dans votre poche


Peter Brueghel le Jeune, paiement des impôts , 1640

La dernière entrée sur le rasoir sur les objets a disparu. Nous continuerons la reconnaissance au combat. Aujourd'hui, nous allons parler du difficile. Il ne s'agit pas encore de BIG DATA, mais il est déjà peu pratique de travailler - de très grandes quantités de données. Tout le monde ne rentrera pas dans toute la RAM, mais certains ne rentreront même pas sur le disque (il n'y a pas assez d'espace, mais beaucoup de déchets). Le nom de notre base de données FIAS de confiance est la base de données du système fédéral d'information sur les adresses. Archive de 5,5 Go. Et il est compressé dans une archive XML. Après le déballage, il y aura 53 Go (stockez 110 Go pour le déballage). Et lorsque vous commencez à l'analyser et à le convertir, 110 Go ne suffiront pas. À propos de la quantité requise de RAM sera également.

Tout irait bien, mais vous pouvez creuser plus loin. Il existe un tel projet international de collecte et de systématisation de données open source - OpenAddresses . Il y aura donc plus de bases de données. La couverture actuelle de la planète comporte de nombreux points blancs, par exemple, la Russie est presque absente. La taille de l'archive est de 10 Go.

Ou une base de données d'un projet OpenStreetMaps assez connu. Il est construit par des bénévoles sur la base de Wikipédia. Assez détaillé et multilingue. Maintenant une archive complète de XML compressé d'une taille de 74 Go.
S'ils ont commencé à parler d'adresses, des nouvelles inattendues sont arrivées à temps de DuckDuckGo , le meilleur moteur de recherche sécurisé pour aujourd'hui, sur sa transition vers les cartes Apple. Plus précisément sur Apple MapKit JS. La caractéristique la plus intéressante dans notre contexte est la «recherche d'adresse améliorée». Apple est-il le meilleur qui collecte et protège minutieusement nos données? Il faudra tracer ...
Donc le défi. Comment mettre toute cette richesse d'adresses dans un référentiel agréable à utiliser, pour vous permettre de rêver d'une API gratuite (en Python, bien sûr) et de ne pas laisser votre cher fer à repasser sur une charge non lue. Appelons ça MicroBigData - mcBD ou μBG en anglais :-)

Dans l'économie de chaque deuxième (ou même premier) développeur, cette chose est un répertoire d'adresses, c'est aussi un répertoire de toponymes, chose très nécessaire. Et quand c'est aussi normatif, préparé, nettoyé et bien documenté par le bon corps, c'est juste un conte de fées. Il faut rendre hommage, le fisc russe fait bien sa production numérique. Autant que possible. Il y a probablement des défauts à l'intérieur et le nettoyage des données se poursuit. Comment résoudre ce problème, laissez les chefs d'État réfléchir. Ils décident pour nous-mêmes et pour notre bénéfice à tous. À propos, une faute de frappe de FIAS a été trouvée dans l'exemple ci-dessous. Le résultat n'est pas affecté. Je ne l'ai pas réparé. Trouverez-vous?

Je ne sais pas dans quelle mesure les données d'adresse sont pertinentes dans vos projets - ce sont toutes les régions, les villes, les rues. Mais il semble que pas un seul projet pour les gens ne puisse s'en passer. C'est l'adresse où chercher une personne ou où lui envoyer des colis. Les détails du passeport ou de tout autre document doivent être enregistrés. Ou peut-être que c'est l'adresse du bureau de travail ou des attractions qu'il est recommandé de visiter. Et que faire? Où trouver

La solution la plus simple, sans tenir compte des erreurs et des doublons, est des objets primitifs contenant de simples littéraux de chaîne (ce sont des constantes de chaîne, ils sont également de la chaîne). Laissez les utilisateurs y entrer les prochaines entrées reçues. Et les objets sont capables de se sauver - nous avons déjà réussi cela .

De tels objets sont, par exemple, tels que décrits dans la classe ci-dessous. Directement à partir du manuel , quoique américain, mais adapté à notre réalité russe - au lieu de leur ZIP, il y aura notre code postal. Je voudrais également remplacer le code postal par un numéro, mais pour des raisons de monotonie, je laisse une chaîne. Quiconque reconnaît immédiatement un langage, et c'est ObjectScript, a droit à un semblable encourageant.

Class Soviet.Address Extends %Persistent { Property streetName As %String; Property cityName As %String; Property areaName As %String; Property postalCode As %String; } 

Bien sûr, beaucoup seront indignés, ils disent que tout sort des poches de l’objet (littéraux). Où l'a-t-on vu, pour que l'objet fasse briller ses champs publiquement?! Laissons-le de cette façon, cela fait trop mal un exemple éloquent et est compréhensible pour tout étudiant.

C'est en fait tout ce qui est nécessaire. Rempli dans les champs. Mettez en stockage. Transféré à d'autres objets pour le travail. Héritée à la suite de quelqu'un. Tout fonctionne. Et stocké!
Mais quelques mots pourquoi cela ne vaut pas la peine, il faut le dire. Quelle est notre adresse d'objet? Pourquoi ne peut-il pas s'agir simplement d'un groupe de chaînes de texte? Les objections les plus évidentes qui viennent à l'esprit viennent du contexte - qui utilise cette adresse, sous quelle forme et dans quel but? Essayez de mettre de côté votre réflexion sur la programmation et imaginez comment un «touriste étranger», un «historien», un «inspecteur des impôts», un «avocat», etc.

Je pense qu'immédiatement, une multitude de questions et de clarifications supplémentaires se posent: quelle langue utiliser, dans quel codage stocker et donner, à quelle époque classer, quels documents sont mis en œuvre, légaux ou postaux? Une ville est-elle un règlement nommé ou quoi? Même une rue peut se révéler être un boulevard, une rue latérale, une avenue ou autre chose. Que faire de tous ces détails d'implémentation importants?

Prenons un exemple vivant. Google est désormais géré par Sundar Pichai. Lui-même est originaire de l'Inde. Né dans la ville de Chennai (alias Chennai). Ou à Madras? En 1996, les Indiens ont décidé que le nom de la ville était très portugais et renommé la capitale du Tamil Nadu de Madras à Chennai. Et que devraient écrire Sundar et 72 millions de ses compatriotes dans ses documents électroniques?

En général, toute la science traite de cela - la toponymie appliquée .
Donc, des questions se posent. Comment gérer l' heure et la date ? L' argent est-il si évident ? Les coordonnées géographiques sont-elles si simples? Et comment est-ce implémenté dans votre code? Pouvez-vous transférer vers le SGBD sélectionné sans abaisser le niveau d'abstraction? Comment ne pas glisser dans les types atomiques de données machine et penser constamment à leur reconstruction? Ici, il vaut la peine de chercher la source d'une API primitive ou, inversement, saine. Pensez-y à votre guise.

Bref, le contexte est le plus important. Et le modèle objet nous permet de l'utiliser directement à travers l'encapsulation de «données machine» et la mise en œuvre d'un comportement «live» sensible au contexte. Pas du tout que les tuples de bas niveau disposés dans des tableaux ;-)

En attendant, revenons à l'implémentation «primitive» et compliquons nos vies. Pour commencer, éliminez les erreurs et les doublons. Autrement dit, nous chercherons un moyen d'écrire des adresses tout de suite tout de suite. Dans le même temps, nous aiderons les développeurs de l'interface utilisateur à organiser des conseils pour les utilisateurs lors du remplissage des champs de saisie de données.
Lorsque deux personnes se réunissent en un seul endroit - les textes et la plate-forme de données InterSystems IRIS, le développeur a une réelle opportunité de se déployer pleinement sans quitter la machine. Par exemple, en utilisant les composants d'objet intégrés iKnow et iFind . Ce sont des composants pour travailler avec des données non structurées et la recherche en texte intégral , respectivement. Le russe est pris en charge «prêt à l'emploi».
Tout d'abord, nous apprendrons à l'adresse à lire les données nécessaires à partir de la source d'origine. Heureusement, l'ensemble de données du service fiscal fédéral contient des descriptions toutes faites de la structure des documents XML. Selon la description jointe aux données du site FIAS , nous avons besoin de l'ensemble de données ADDROBJ, qui, dans mon cas, correspond au fichier AS_ADDROBJ_2_250_01_04_01_01.xsd

Ensuite, nous utiliserons le convertisseur système du modèle XSD vers la structure de champ correspondante de la classe% XML.Adaptor, aimablement préparée pour nous par les développeurs IRIS. Le signe de pourcentage au début signifie simplement qu'il s'agit d'une classe de la bibliothèque système. Les détails d'utilisation sont dans la documentation . Nous effectuerons des opérations dans le terminal.

 set xmlScheme = ##class(%XML.Utils.SchemaReader).%New() do xmlScheme.Process("http://localhost/AS_ADDROBJ_2_250_01_04_01_01.xsd") 

La même chose peut être obtenue dans l' Atelier IDE (dans le menu Outils> Compléments> Assistant de schéma XML) ou par des requêtes similaires aux objets directement à partir du code du programme.



Puisque nous avons utilisé le constructeur sans spécifier les paramètres, à savoir le nom du package pour placer les classes résultantes, ils se sont retrouvés dans le package Test. Comme vous pouvez le voir dans la deuxième commande, j'ai donné le fichier de schéma via mon serveur Web local en Python:

 python3 -m http.server 80 

Vous pouvez utiliser n'importe quel autre serveur http que vous aimez. Ou téléchargez le fichier sur votre serveur IRIS et indiquez le chemin d'accès direct à celui-ci.

En conséquence, nous avons deux classes qui reflètent pleinement la structure de notre XML adressable:

Test.AddressObjects
 ///            Class Test.AddressObjects Extends (%Persistent, %XML.Adaptor) [ ProcedureBlock ] { Parameter XMLNAME = "AddressObjects"; Parameter XMLSEQUENCE = 1; ///    Relationship Object As Test.Object(XMLNAME = "Object", XMLPROJECTION = "ELEMENT") [ Cardinality = many, Inverse = AddressObjects ]; } 

Test.object
 ///  : http://localhost:28869/AS_ADDROBJ_2_250_01_04_01_01.xsd Class Test.Object Extends (%Persistent, %XML.Adaptor) [ ProcedureBlock ] { Parameter XMLNAME = "Object"; Parameter XMLSEQUENCE = 1; ///      Property AOGUID As %String(MAXLEN = 36, MINLEN = 36, XMLNAME = "AOGUID", XMLPROJECTION = "ATTRIBUTE") [ Required ]; ///   Property FORMALNAME As %String(MAXLEN = 120, MINLEN = 1, XMLNAME = "FORMALNAME", XMLPROJECTION = "ATTRIBUTE") [ Required ]; ///   Property REGIONCODE As %String(MAXLEN = 2, MINLEN = 2, XMLNAME = "REGIONCODE", XMLPROJECTION = "ATTRIBUTE") [ Required ]; ///   Property AUTOCODE As %String(MAXLEN = 1, MINLEN = 1, XMLNAME = "AUTOCODE", XMLPROJECTION = "ATTRIBUTE") [ Required ]; ///   Property AREACODE As %String(MAXLEN = 3, MINLEN = 3, XMLNAME = "AREACODE", XMLPROJECTION = "ATTRIBUTE") [ Required ]; ///   Property CITYCODE As %String(MAXLEN = 3, MINLEN = 3, XMLNAME = "CITYCODE", XMLPROJECTION = "ATTRIBUTE") [ Required ]; ///    Property CTARCODE As %String(MAXLEN = 3, MINLEN = 3, XMLNAME = "CTARCODE", XMLPROJECTION = "ATTRIBUTE") [ Required ]; ///    Property PLACECODE As %String(MAXLEN = 3, MINLEN = 3, XMLNAME = "PLACECODE", XMLPROJECTION = "ATTRIBUTE") [ Required ]; ///     Property PLANCODE As %String(MAXLEN = 4, MINLEN = 4, XMLNAME = "PLANCODE", XMLPROJECTION = "ATTRIBUTE") [ Required ]; ///   Property STREETCODE As %String(MAXLEN = 4, MINLEN = 4, XMLNAME = "STREETCODE", XMLPROJECTION = "ATTRIBUTE"); ///     Property EXTRCODE As %String(MAXLEN = 4, MINLEN = 4, XMLNAME = "EXTRCODE", XMLPROJECTION = "ATTRIBUTE") [ Required ]; ///      Property SEXTCODE As %String(MAXLEN = 3, MINLEN = 3, XMLNAME = "SEXTCODE", XMLPROJECTION = "ATTRIBUTE") [ Required ]; ///   Property OFFNAME As %String(MAXLEN = 120, MINLEN = 1, XMLNAME = "OFFNAME", XMLPROJECTION = "ATTRIBUTE"); ///   Property POSTALCODE As %String(MAXLEN = 6, MINLEN = 6, XMLNAME = "POSTALCODE", XMLPROJECTION = "ATTRIBUTE"); ///    Property IFNSFL As %String(MAXLEN = 4, MINLEN = 4, XMLNAME = "IFNSFL", XMLPROJECTION = "ATTRIBUTE"); ///      Property TERRIFNSFL As %String(MAXLEN = 4, MINLEN = 4, XMLNAME = "TERRIFNSFL", XMLPROJECTION = "ATTRIBUTE"); ///    Property IFNSUL As %String(MAXLEN = 4, MINLEN = 4, XMLNAME = "IFNSUL", XMLPROJECTION = "ATTRIBUTE"); ///      Property TERRIFNSUL As %String(MAXLEN = 4, MINLEN = 4, XMLNAME = "TERRIFNSUL", XMLPROJECTION = "ATTRIBUTE"); /// OKATO Property OKATO As %String(MAXLEN = 11, MINLEN = 11, XMLNAME = "OKATO", XMLPROJECTION = "ATTRIBUTE"); /// OKTMO Property OKTMO As %String(MAXLEN = 11, MINLEN = 8, XMLNAME = "OKTMO", XMLPROJECTION = "ATTRIBUTE"); ///    Property UPDATEDATE As %Date(XMLNAME = "UPDATEDATE", XMLPROJECTION = "ATTRIBUTE") [ Required ]; ///     Property SHORTNAME As %String(MAXLEN = 10, MINLEN = 1, XMLNAME = "SHORTNAME", XMLPROJECTION = "ATTRIBUTE") [ Required ]; ///    Property AOLEVEL As %Integer(XMLNAME = "AOLEVEL", XMLPROJECTION = "ATTRIBUTE", XMLTotalDigits = 10) [ Required ]; ///     Property PARENTGUID As %String(MAXLEN = 36, MINLEN = 36, XMLNAME = "PARENTGUID", XMLPROJECTION = "ATTRIBUTE"); ///   .  . Property AOID As %String(MAXLEN = 36, MINLEN = 36, XMLNAME = "AOID", XMLPROJECTION = "ATTRIBUTE") [ Required ]; ///        Property PREVID As %String(MAXLEN = 36, MINLEN = 36, XMLNAME = "PREVID", XMLPROJECTION = "ATTRIBUTE"); ///        Property NEXTID As %String(MAXLEN = 36, MINLEN = 36, XMLNAME = "NEXTID", XMLPROJECTION = "ATTRIBUTE"); ///           4.0. Property CODE As %String(MAXLEN = 17, MINLEN = 0, XMLNAME = "CODE", XMLPROJECTION = "ATTRIBUTE"); ///      4.0      (  ) Property PLAINCODE As %String(MAXLEN = 15, MINLEN = 0, XMLNAME = "PLAINCODE", XMLPROJECTION = "ATTRIBUTE"); ///     .     .      . /// 0 –   /// 1 -  Property ACTSTATUS As %Integer(XMLNAME = "ACTSTATUS", XMLPROJECTION = "ATTRIBUTE", XMLTotalDigits = 10) [ Required ]; ///   Property CENTSTATUS As %Integer(XMLNAME = "CENTSTATUS", XMLPROJECTION = "ATTRIBUTE", XMLTotalDigits = 10) [ Required ]; ///     –    (.   OperationStatus): /// 01 – ; /// 10 – ; /// 20 – ; /// 21 –  ; /// 30 – ; /// 31 -     ; /// 40 –    (); /// 41 –     ; /// 42 -        ; /// 43 -         ; /// 50 – ; /// 51 –     ; /// 60 –    ; /// 61 –        Property OPERSTATUS As %Integer(XMLNAME = "OPERSTATUS", XMLPROJECTION = "ATTRIBUTE", XMLTotalDigits = 10) [ Required ]; ///    4 (    ) Property CURRSTATUS As %Integer(XMLNAME = "CURRSTATUS", XMLPROJECTION = "ATTRIBUTE", XMLTotalDigits = 10) [ Required ]; ///    Property STARTDATE As %Date(XMLNAME = "STARTDATE", XMLPROJECTION = "ATTRIBUTE") [ Required ]; ///    Property ENDDATE As %Date(XMLNAME = "ENDDATE", XMLPROJECTION = "ATTRIBUTE") [ Required ]; ///      Property NORMDOC As %String(MAXLEN = 36, MINLEN = 36, XMLNAME = "NORMDOC", XMLPROJECTION = "ATTRIBUTE"); ///     Property LIVESTATUS As %xsd.byte(VALUELIST = ",0,1", XMLNAME = "LIVESTATUS", XMLPROJECTION = "ATTRIBUTE") [ Required ]; ///  : /// 0 -   /// 1 - ; /// 2 - - Property DIVTYPE As %xsd.int(VALUELIST = ",0,1,2", XMLNAME = "DIVTYPE", XMLPROJECTION = "ATTRIBUTE") [ Required ]; Relationship AddressObjects As Test.AddressObjects(XMLPROJECTION = "NONE") [ Cardinality = one, Inverse = Object ]; } 


De la liste complète des fichiers xml dans FIAS, nous n'utiliserons qu'un fichier avec les noms des régions, des villes et des rues. Au moment de préparer la publication, j'avais ceci:
AS_ADDROBJ_20190106_90809714-fe22-45b2-929c-52bd950963e0.XML

La taille du fichier n'est ni grande ni petite, mais presque 3 Go. Vous ne l'ouvrirez pas avec des outils de texte ordinaires - ils ne digèrent pas cette taille.
Soit dit en passant, la longueur maximale d'un littéral de chaîne (type String) dans InterSystems IRIS ne dépasse pas 3 641 144 caractères. Autrement dit, le téléchargement direct du fichier ou l'URL dans celui-ci échouera. D'autres restrictions peuvent être trouvées dans la documentation . Pour travailler avec de grands volumes de données, vous pouvez utiliser des flux de données (flux) qui n'ont pas une telle limite de longueur.
Voyons voir ce que nous obtenons?

Cuisson du poivron farci FIAS. Ce n'est qu'une préparation pour un avenir merveilleux. Nous obtenons d'abord l'ensemble minimal initial. Nous n'avons besoin que de ces ingrédients:

 Class FIAS.AddressObject Extends (%Persistent, %XML.Adaptor) [ ProcedureBlock ] { Parameter XMLNAME = "Object"; Parameter XMLSEQUENCE = 1; ///      Property AOGUID As %String(MAXLEN = 36, MINLEN = 36, XMLNAME = "AOGUID", XMLPROJECTION = "ATTRIBUTE") [ Required ]; ///   Property OFFNAME As %String(MAXLEN = 120, MINLEN = 1, XMLNAME = "OFFNAME", XMLPROJECTION = "ATTRIBUTE"); ///   Property POSTALCODE As %String(MAXLEN = 6, MINLEN = 6, XMLNAME = "POSTALCODE", XMLPROJECTION = "ATTRIBUTE"); ///     Property SHORTNAME As %String(MAXLEN = 10, MINLEN = 1, XMLNAME = "SHORTNAME", XMLPROJECTION = "ATTRIBUTE") [ Required ]; ///    Property AOLEVEL As %Integer(XMLNAME = "AOLEVEL", XMLPROJECTION = "ATTRIBUTE", XMLTotalDigits = 10) [ Required ]; ///     Property PARENTGUID As %String(MAXLEN = 36, MINLEN = 36, XMLNAME = "PARENTGUID", XMLPROJECTION = "ATTRIBUTE"); ///   .  . Property AOID As %String(MAXLEN = 36, MINLEN = 36, XMLNAME = "AOID", XMLPROJECTION = "ATTRIBUTE") [ Required ]; 

Ensuite, faites l'écriture . Nous créons un objet qui comprend XML comme natif - nous utilisons une classe de la bibliothèque système% XML.Reader:

 set reader = ##class(%XML.Reader).%New() 

Et nous lui donnons des instructions, qui prendre, et ignorons le reste. Nous prendrons une portion:

 do reader.Correlate("Object","FIAS.AddressObject") 

Ensuite, il existe des variantes sur la façon d'obtenir le fichier microbd d'origine. Si cela vous convient, vous pouvez le placer à côté du référentiel - localement dans le système de fichiers du serveur IRIS. Ou, comme dans mon exemple, demandez à envoyer via HTTP. Il existe une option encore plus universelle, sur laquelle il y aura quelques mots ci-dessous.

 set url="http://localhost/AS_ADDROBJ_20190106_90809714-fe22-45b2-929c-52bd950963e0.XML" write reader.OpenUrl(url) 

Important! En ce moment, la majorité qui se transmettra cet exemple aura une chose terrible. Le système reviendra au lieu du joyeux "1" (tout est en ordre), quelque chose commençant par "0 ¸ STORE ...". Et ça ne plaira pas. Autrement dit, le fichier avec une microbase apparemment n'est pas tout à fait micro et ne rentre pas dans notre objet. La mémoire qui lui était allouée n'était pas suffisante. Solvable? Bien sûr. La plateforme de données IRIS vous permet de créer des objets jusqu'à 4 To en RAM. Alors qu'est-ce qui a mal tourné? Par défaut, les paramètres système sont définis sur 256 Mo par objet. Et nous aurions besoin de beaucoup plus. Et rappelez-vous, ce sont des exigences de RAM. Y a-t-il suffisamment de stock sur votre ordinateur / serveur?
De quelle taille de mémoire nous avons besoin pour installer ce géant que nous avons installé empiriquement - près de 10 Go. Ce que vous devez spécifier dans les paramètres (Menu> Configurer la mémoire> Mémoire maximale par processus (Ko)) ou via la variable système $ ZSTORAGE (en kilo-octets):

 set $ZSTORAGE=10000000 

Vous avez lancé un nouveau processus avec les paramètres de mémoire nécessaires? Ensuite, tout est encore plus simple - nous lisons et sauvegardons.

Il existe une option alternative (et probablement préférée) - utilisez la propriété UsePPGHandler de la classe% XML.Reader qui vous permet de ne pas stocker XML en mémoire et fonctionne avec les paramètres de mémoire standard.

 set reader = ##class(%XML.Reader).%New() set reader.UsePPGHandler = 1 

plus ... Corrélation / Lecture, etc. ...

 do reader.Next(.object) do object.%Save() 

Et donc 3 722 548 fois pour chaque opération :-)

C'est fatigant. Par conséquent, nous complétons notre classe FIAS.AddressObject avec une méthode d'importation, basée sur les commandes qui viennent d'être affichées:

 ClassMethod Import() { //     XML Set reader = ##class(%XML.Reader).%New() //   XML   Set status = reader.OpenURL("http://localhost/AS_ADDROBJ_20190106_90809714-fe22-45b2-929c-52bd950963e0.XML") If $$$ISERR(status) {Do $System.Status.DisplayError(status)} //       Do reader.Correlate("Object","FIAS.AddressObject") //       While (reader.Next(.object,.status)) { Set status = object.%Save() If $$$ISERR(status) {do $System.Status.DisplayError(status)} } //      ,   If $$$ISERR(status) {Do $System.Status.DisplayError(status)} } 

Utilisons la puissance de notre exocortex informatique - une seule commande dans le terminal :

 do ##class(FIAS.AddressObject).Import() 



Je demande à tout le monde à table. Il y avait MCD, et maintenant le plat fini sous la forme d'un global avec les noms vérifiés des villes et des poids russes est prêt.



Et enfin quelques mots sur le moment où 4 To ne suffisent pas. Dans ce cas, nous suivons les flux (ou les flux si vous le souhaitez). La documentation est disposée sur des étagères. Vous pouvez binaire, vous pouvez symbolique. Le stockage dans le monde n'est pas non plus interdit. La recette est la suivante: on prend un ruisseau, on le coupe en morceaux et on le donne aux objets dont on a besoin pour la consommation.

De plus, les beaux objets ObjectScript adressables et les API en Python ne convenaient pas. Il y aura une histoire distincte.
Agréable: Gartner vient de terminer la collecte annuelle de véritables évaluations et avis d'utilisateurs dans la catégorie SGBD et, sur cette base, a publié sa note du meilleur SGBD de 2019. Les produits InterSystems Caché et InterSystems IRIS Data Platform ont reçu la note de choix du consommateur la plus élevée. De qui vous avez choisi et comment vous avez évalué, vous pouvez jeter un œil vous-même .
Meilleur logiciel de systèmes de gestion de base de données opérationnelle de 2019, tel que examiné par les clients

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


All Articles