Cómo rechacé db4o en un sistema industrial

imagen


Somos un departamento de una gran empresa que desarrolla un importante sistema Java SE / MS SQL / db4o. Durante varios años, el proyecto cambió de un prototipo a una operación industrial y db4o se convirtió en un freno de cálculo, quería cambiar de db4o a la moderna tecnología noSQL. La prueba y el error se alejaron mucho del plan original: db4o se abandonó con éxito, pero a costa de un compromiso. Bajo el gato reflexiones y detalles de implementación.


¿La tecnología db4o está muerta?


En Habré es posible encontrar no tantas publicaciones sobre db4o. En Stackoverflow, algún tipo de actividad residual es como un nuevo comentario sobre una pregunta anterior o una nueva pregunta sin respuesta . Wiki generalmente cree que la versión estable actual está fechada en 2011.


Esto forma una impresión general: la tecnología es irrelevante. Incluso hubo confirmación oficial : Actian decidió no seguir activamente y promover la oferta comercial de productos db4o para nuevos clientes por más tiempo.


¿Cómo se calculó db4o?


El artículo Introducción a las bases de datos orientadas a objetos habla sobre la característica principal de db4o: la ausencia total de un esquema de datos. Puedes crear cualquier objeto


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

y luego solo escríbelo en el archivo de la base de datos


 db.Store(user1) 

El objeto grabado se puede recuperar utilizando el método Query.execute () en la forma en que se guardó.


Al comienzo del proyecto, esto permitió garantizar rápidamente la visualización de la pista de auditoría con todos los datos enviados, sin preocuparse por la estructura de las tablas relacionales. Esto ayudó al proyecto a sobrevivir. Luego había pocos recursos en el sandbox e inmediatamente después del final del cálculo de hoy, los datos para mañana comenzaron a cargarse en MS SQL. Todo cambiaba constantemente: descubra qué se sirve automáticamente de noche. Y se puede acceder al archivo db4o
en la depuración, extraiga una instantánea del día deseado y responda la pregunta "enviamos todos los datos, pero no ordenó nada".


Con el tiempo, el problema de supervivencia desapareció, el proyecto despegó, el trabajo con las solicitudes de los usuarios ha cambiado. Abra un archivo db4o en la depuración y analice una pregunta difícil si un desarrollador siempre está ocupado. En cambio, hay una multitud de analistas, armados con una descripción de la lógica del pedido y capaces de usar solo la parte visible de los datos del usuario. Pronto db4o comenzó a usarse solo para mostrar el historial del cálculo. Al igual que Pareto , una pequeña parte de las capacidades proporciona la carga principal.


En la operación de combate, el archivo de historial tarda ~ 35 GB / día, la descarga tarda aproximadamente una hora. El archivo en sí mismo se comprime bien (1:10), pero por alguna razón la biblioteca com.db4o.ObjectContainer no realiza compresión. En el norte de CentOS, la biblioteca com.db4o.query.Query escribe / lee un archivo exclusivamente en una secuencia. La velocidad es un cuello de botella.


Diagrama esquemático del dispositivo.


El modelo de información del sistema es la jerarquía de los objetos A, B, C y D. La jerarquía no es un árbol; los enlaces C1 -> B1 son necesarios para la operación.


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

El usuario interactúa con el servidor a través de la interfaz de usuario (GUI), que es proporcionada por com.sun.net.httpserver.HttpsServer, el cliente y el servidor intercambian documentos XML. En la primera pantalla, el servidor asigna un identificador al nivel de usuario, que no cambia más. Si el usuario necesita un historial de algún nivel, la GUI envía al servidor un identificador envuelto en XML. El servidor determina los valores de las claves para buscar en la base de datos, escanea el archivo db4o para el día deseado y recupera el objeto solicitado en la memoria más todos los objetos a los que se refiere. Crea una presentación XML del nivel extraído y lo devuelve al cliente.


Al escanear un archivo, db40 lee de forma predeterminada todos los objetos secundarios a una cierta profundidad, extrayendo una jerarquía bastante grande junto con el objeto deseado. El tiempo de lectura se puede reducir configurando la profundidad de activación mínima para la clase Foo innecesaria con conf.common (). ObjectClass (Foo.class) .maximumActivationDepth (1).


El uso de clases anónimas conduce a la creación de referencias implícitas a la clase que encierra este $ 0 . Db4o procesa y restaura dichos enlaces correctamente (pero lentamente).


0. Idea


Entonces, los administradores tienen una expresión extraña en sus caras cuando se trata de apoyar o administrar db4o. La extracción de datos es lenta, la tecnología no es muy animada. Tarea: en lugar de db4o, aplique la tecnología NoSQL actual. Un par de Spring Data + MongoDB me llamó la atención.


1. Enfoque frontal


Lo primero que pensé fue usar org.springframework.data.mongodb.core.MongoOperations y el método save (), porque parece com.db4o.ObjectContainer.db.Store (user1). La documentación de MongoDB dice que los documentos se almacenan en colecciones, es lógico presentar los objetos del sistema necesarios como documentos de las colecciones correspondientes. También hay anotaciones @ DBRef que le permiten implementar relaciones entre documentos en general en el espíritu de 3NF . Vamos


1.1. Descarga Tipo de referencia clave


El sistema consta de clases POJO diseñadas hace mucho tiempo y sin tener en cuenta todas estas nuevas tecnologías. Se utilizan campos de tipo Mapa <POJO, POJO>, existe una lógica ramificada de trabajar con ellos. Guardo este campo, recibo un error


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

En esta ocasión, solo se encontró correspondencia de 2011 , en la que se propuso desarrollar MappingMongoConverter no estándar. Noté hasta ahora el problema campos @ Transitorios, continúo. Resultó ahorrar, estudiando el resultado.


El guardado se produce en la colección, cuyo nombre coincide con el nombre de la clase guardada. Todavía no he usado anotaciones @DBRef, por lo que solo hay una colección, los documentos JSON son bastante grandes y ramificados. Noto que cuando guarda el objeto, MongoOperations revisa todos los enlaces no vacíos (incluidos los heredados) y los escribe como un documento adjunto.


1.2. Descarga Campo o matriz con nombre?


El modelo del sistema es tal que la clase C puede contener una referencia a la misma clase D varias veces. En un campo DefaultMode separado y entre otros enlaces en ArrayList, algo como esto

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

Después de la descarga, el documento JSON tendrá dos copias: un documento adjunto llamado defaultMode y un elemento sin nombre de la matriz de documentos. En el primer caso, se puede acceder al documento por nombre, en el segundo, por el nombre de la matriz con un índice. Puede buscar colecciones MongoDB en ambos casos. Trabajando solo con Spring Data y MongoDB, llegué a la conclusión de que puede usar ArrayList, si lo hace con cuidado; No noté ninguna restricción en el uso de matrices. Las características aparecieron más tarde, a nivel de MongoDB Connector para BI.


1.3. Descargar Argumentos del constructor


Estoy tratando de leer un documento guardado usando el método MongoOperations.findOne (). Al cargar el objeto A desde la base de datos, se produce una excepción


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

Resultó que la clase tiene un campo corpName, y el constructor tiene un parámetro de nombre de cadena, y this.corpName = name se asigna en el cuerpo del constructor. MongoOperations requiere que los nombres de campo en las clases coincidan con los nombres de los argumentos del constructor. Si hay varios constructores, debe seleccionar uno con la anotación @PersistenceConstructor. Traigo los nombres de los campos y parámetros en correspondencia.


1.4. Descargar Con $ D y esto $ 0


La clase D interna anidada encapsula el comportamiento predeterminado de la clase C y no tiene sentido por separado de la clase C. Se crea una instancia D para cada instancia de C y viceversa: para cada instancia de D hay una instancia de C que la generó. La clase D tiene descendientes que implementan comportamientos alternativos y se pueden almacenar en listOfD. El constructor de las clases descendientes de D requiere la presencia de un objeto ya existente C.


Además de las clases internas anidadas, el sistema usa clases internas anónimas . Como sabe , ambos contienen una referencia implícita a una instancia de una clase de cierre. Es decir, como parte de cada instancia del objeto CD, el compilador crea un enlace $ 0, que apunta al objeto padre C.


Nuevamente trato de leer el documento guardado de la colección y obtengo una excepción


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

Recuerdo que los métodos de la clase D usan las referencias C.this.fieldOfClassC con might y main, y los descendientes de la clase D requieren que el constructor sea instanciado con C como argumento. Es decir, necesito proporcionar un cierto orden de creación de objetos en MongoOperations para que el objeto padre C pueda especificarse en el constructor D. De nuevo, ¿el MappingMongoConverter no estándar?


¿Quizás no usar clases anónimas y normalizar las clases internas? Refinar, o más bien refinar la arquitectura de un sistema ya implementado es una tarea increíble ...


2. Enfoque de 3NF / @ DBRef


Intento ir por otro lado, guardar cada clase en mi colección y hacer conexiones entre ellos en el espíritu de 3NF.


2.1. Descarga @DBRef es hermoso


La clase C contiene varias referencias a D. Si los enlaces defaultMode y ArrayList están marcados como @DBRef, entonces el tamaño del documento disminuirá, en lugar de grandes documentos adjuntos habrá enlaces limpios. En el campo json el documento de la colección C aparece el campo

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

En la base de datos MongoDB, se crea automáticamente una colección D y un documento con un campo


 "_id" : ObjectId("5c496eed2c9c212614bb8176") 

Todo es simple y hermoso.


2.2. Descargar Constructor de clase D


Cuando se trabaja con enlaces, el objeto C sabe que el objeto D predeterminado se crea exactamente una vez. Si necesita omitir todos los objetos D excepto el predeterminado, solo compare los enlaces:


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

Llamo findOne (), estudio mi clase C. Resulta que MongoOperations lee un documento json y llama al constructor D para cada anotación @DBRef que encuentra, cada vez que crea un nuevo objeto. Obtengo una construcción extraña: dos referencias diferentes a D en el campo defaultMode y en la matriz listOfD, donde el enlace debe ser el mismo.


Aprendiendo de la comunidad : "Dbref, en mi opinión, debe evitarse cuando se trabaja con mongodb". Otra consideración en la misma línea de la documentación oficial: el modelo de datos desnormalizados donde los datos relacionados se almacenan en un solo documento será óptimo para resolver DBRefs, su aplicación debe realizar consultas adicionales para devolver los documentos referenciados.


La página de documentación mencionada dice al principio: "Para muchos casos de uso en MongoDB, el modelo de datos desnormalizados donde los datos relacionados se almacenan en un solo documento será óptimo". ¿Está escrito para mí?


Centrarse con el diseñador sugiere que no necesita pensar como en un DBMS relacional. La elección es:


  • si especifica @DBRef:
    • se llamará al constructor para cada anotación y se crearán varios objetos idénticos;
    • MongoOperations buscará y leerá todos los documentos de todas las colecciones relacionadas. Habrá una solicitud al índice por ObjectId y luego lectura de muchas colecciones de una base de datos (grande);
  • si no especifica, el json "anormalizado" se guardará con repeticiones de los mismos datos.

Lo noto por mí mismo: no puede confiar en @DBRef, pero use un campo de tipo ObjectId, rellenándolo manualmente. En este caso, en lugar de


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

el documento json contendrá


 "defaultMode" : ObjectId("5c496eed2c9c212614bb8176") 

No habrá carga automática: MongoOperations no sabe en qué colección buscar un documento. El documento deberá cargarse en una solicitud separada (diferida) que indique la colección y el ObjectId. Una sola consulta debería devolver el resultado rápidamente, además, ObjectId crea un índice automático para cada colección.


2.3. ¿Y ahora qué?


Subtotales No fue posible implementar rápida y fácilmente la funcionalidad db4o en MongoDB:


  • No está claro cómo usar un POJO personalizado como clave - clave de lista de valores;
  • No está claro cómo establecer el orden en que se crean los objetos en MappingMongoConverter;
  • no está claro si cargar un documento "no normalizado" sin DBRef y si es necesario crear su propio mecanismo para la inicialización diferida.

Puede agregar carga diferida. Puedes intentar hacer MappingMongoConverter. Puede modificar constructores / campos / listas existentes. Pero hay muchos años de estratificación de la lógica empresarial, no una alteración débil y el riesgo de nunca ser probado.


Solución de compromiso: crear un nuevo mecanismo para guardar datos para el problema que se está resolviendo, mientras se conserva el mecanismo para interactuar con la GUI.


3. El tercer intento, la experiencia de los dos primeros


Pareto sugiere que resolver problemas con la velocidad de los usuarios significará el éxito de toda la tarea. La tarea es esta: debe aprender a guardar y restaurar rápidamente los datos de presentación del usuario sin db4o.


Esto perderá la capacidad de examinar el objeto guardado en la depuración. Por un lado, esto es malo. Por otro lado, tales tareas rara vez ocurren, y en git se etiquetan todas las entregas de combate. Para la tolerancia a fallas, cada vez antes de la descarga, el sistema serializa el cálculo en un archivo. Si necesita examinar un objeto en la depuración, puede tomar la serialización, clonar el conjunto del sistema correspondiente y restaurar el cálculo.


3.1. Datos de presentación personalizados


Para crear presentaciones de niveles de usuario, el sistema tiene una clase de Visor especial. El método Viewer.getXML () recibe un nivel como entrada, extrae los valores numéricos y de cadena necesarios y genera XML.


Si el usuario solicitó mostrar el nivel del cálculo de hoy, el nivel se encontrará en la RAM. Para mostrar un cálculo del pasado, el método com.db4o.query.Query.execute () encontrará el nivel en el archivo. El nivel del archivo casi no es diferente del nivel recién creado y Viewer creará la presentación sin notar la sustitución.


Para resolver mi problema, necesito un intermediario entre el nivel de cálculo y su presentación: el marco de presentación (Frame), que almacenará datos y se basará en los datos XML disponibles. La cadena de acciones para construir la presentación se alargará, cada vez que se generará un marco y el marco generará XML:


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

Al guardar la historia, deberá crear marcos de todos los niveles y escribir en la base de datos.


3.2. Descarga


La tarea era relativamente simple y no había problemas con ella. Repitiendo la estructura de la presentación XML, el marco recibió un dispositivo recursivo en forma de una jerarquía de elementos con los campos Cadena, Entero y Doble. El marco solicita getXML () de todos sus elementos, lo recopila en un solo documento y lo devuelve. MongoOperations hizo un gran trabajo con la naturaleza recursiva del marco y no hizo nuevas preguntas a medida que avanzaba.


¡Finalmente, todo despegó! El motor WiredTiger comprime por defecto las colecciones de documentos MongoDB; en el sistema de archivos, la descarga tomó ~ 3.5 GB por día. Una disminución de diez veces sobre db4o no está mal.


Al principio, la descarga se organizó simplemente: un recorrido recursivo del árbol de niveles, MongoOperations.save () para cada uno. Dicha descarga tardó 5,5 horas, y esto a pesar del hecho de que construir presentaciones implica solo leer objetos. Agrego multihilo: recorre recursivamente el árbol de niveles, divide todos los niveles disponibles en paquetes de cierto tamaño, creo implementaciones de Callable.call () de acuerdo con el número de paquetes, transfiero cada paquete a nuestro propio paquete y lo hago todo a través de ExecutorService.invokeAll ().


MongoOperations nuevamente no hizo preguntas e hizo un gran trabajo con el modo multiproceso. Seleccionó empíricamente el tamaño del paquete, dando la mejor velocidad de descarga. Resultó 15 minutos para un paquete de 1000 niveles.


3.3. Mongo BI Connector, o cómo las personas trabajan con él


El lenguaje de consulta MongoDB es grande y poderoso, inevitablemente adquirí experiencia trabajando con él, llegando a este lugar. La consola admite JavaScript, puede escribir diseños hermosos y potentes. Este es un lado. Por otro lado, puedo romper el cerebro de una buena mitad de colegas analistas con una solicitud


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

en lugar de lo habitual


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

MongoDB Connector for BI viene al rescate, a través del cual puede presentar documentos de colección en forma de tabla. La base de datos MongoDB se llama una base de datos basada en documentos, no sabe cómo presentar una jerarquía de campos / documentos en forma de tabla. Para que el conector funcione, es necesario describir la estructura de la tabla futura en un archivo .drdl separado, cuyo formato es muy similar al de yaml. En el archivo, debe especificar la correspondencia entre el campo de la tabla relacional en la salida y la ruta al campo del documento JSON en la entrada.


3.4. Características que usan matrices


Se dijo anteriormente que para MongoDB en sí no hay una diferencia especial entre una matriz y un campo. Desde la perspectiva del conector, una matriz es muy diferente de un campo con nombre; Incluso tuve que refactorizar la clase Frame finalizada. Se debe usar una matriz de documentos solo cuando sea necesario colocar parte de la información en una tabla vinculada.


Si el documento JSON es una jerarquía de campos con nombre, se puede acceder a cualquier campo especificando la ruta desde la raíz del documento a través de un período, por ejemplo xy. Si la correspondencia xy => fieldXY se especifica en el archivo DRDL, entonces la tabla de salida tendrá tantas filas como documentos en la colección en la entrada Si en algún documento no hay un campo xy, NULL estará en la fila correspondiente de la tabla.


Supongamos que tenemos una base de datos MongoDB llamada Frames, hay una colección A en la base de datos y MongoOperations ha escrito dos instancias de clase A para esta colección. Estos son los documentos: primero


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

y segundo (ObjectId difiere por el ú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" } 

El conector de BI no puede acceder a los elementos de la matriz por índice, y es simplemente imposible extraer, por ejemplo, el campo days [1] .v de la matriz a la tabla. En cambio, el conector puede representar cada elemento de la matriz de días como una fila en una tabla separada utilizando el operador $ unwind . Esta tabla separada se asociará con la relación original de uno a muchos a través del identificador de fila. En nuestro ejemplo, las tablas tableA se definen para documentos de colección y tableA_days para documentos de la matriz de días. El archivo .drdl tiene este aspecto:


 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 

El contenido de las tablas será: table tableA


_idfieldXY
5cdd51e2394faf88a01bd456valor de cadena xy 1
5cdd51e2394faf88a01bd457valor de cadena xy 2

y mesa tableA_days


tableA_idtableA_dayNotableA_dayVal
5cdd51e2394faf88a01bd4560 00.0
5cdd51e2394faf88a01bd45610.1
5cdd51e2394faf88a01bd4570 00,3
5cdd51e2394faf88a01bd45710.4 0.4

Total


No fue posible implementar la tarea en la formulación original; no puede simplemente tomar y reemplazar db4o con MongoDB. MongoOperations no puede restaurar automáticamente ningún objeto como db4o. Probablemente pueda hacer esto, pero los costos de mano de obra no serán comparables a la llamada a los métodos store / query de la biblioteca db4o.


Pista de auditoría. Db4o es una herramienta muy útil al comienzo de un proyecto. Simplemente puede escribir el objeto, luego restaurarlo y al mismo tiempo no tener preocupaciones y tablas. Todo esto con una advertencia importante: si necesita cambiar la jerarquía de clases (agregue la clase E entre A y B), entonces toda la información almacenada previamente se vuelve ilegible. Pero para comenzar un proyecto esto no es muy importante, siempre y cuando no haya una gran matriz acumulada de archivos antiguos.


Cuando había suficiente experiencia con MongoOperations, escribir la carga no causaba problemas. Escribir un nuevo código para el marco es mucho más fácil que rehacer el antiguo, que también se pone en producción.

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


All Articles