Trabajando con una base de datos desde una aplicación

Al principio, describiré algunos problemas y características cuando trabaje con la base de datos, mostraré agujeros en las abstracciones. A continuación, analizaremos una abstracción más simple basada en la inmunidad.


Se supone que el lector está algo familiarizado con los patrones Active Record , Data Maper , Identity Map y Unit of Work .


Los problemas y las soluciones se consideran en el contexto de proyectos lo suficientemente grandes que no se pueden tirar y reescribir rápidamente.


Mapa de identidad


El primer problema es el problema de mantener la identidad. La identidad es algo que identifica de forma única a una entidad. En la base de datos, esta es la clave principal, y en la memoria, el enlace (puntero). Es bueno cuando los enlaces apuntan a un solo objeto.


Para las bibliotecas ruby ActiveRecord , esto no es así:


post_a = Post.find 1 post_b = Post.find 1 post_a.object_id != post_b.object_id # true post_a.title = "foo" post_b.title != "foo" # true 

Es decir obtenemos 2 referencias a 2 objetos diferentes en la memoria.


Por lo tanto, podemos perder cambios si inadvertidamente comenzamos a trabajar con la misma entidad, pero representada por diferentes objetos.


Hibernate tiene una sesión, de hecho, un caché de primer nivel que almacena la asignación de un identificador de entidad a un objeto en la memoria. Si volvemos a solicitar la misma entidad, obtendremos un enlace a un objeto existente. Es decir Hibernate implementa el patrón del Mapa de Identidad .


Transacciones largas


Pero, ¿qué pasa si no seleccionamos por identificador? Para evitar que el estado de los objetos y el estado de la base de datos no estén sincronizados, hiberne antes de solicitar una selección.
es decir vuelca objetos sucios en la base de datos para que la solicitud lea los datos acordados.


Este enfoque le obliga a mantener abierta la transacción de la base de datos mientras la transacción comercial está en progreso.
Si la transacción comercial es larga, el proceso responsable de la conexión en la base de datos también está inactivo. Por ejemplo, esto puede suceder si una transacción comercial solicita datos a través de la red o realiza cálculos complejos.


N + 1


Quizás el mayor "agujero" en la abstracción ORM es el problema de consulta N + 1.


Ejemplo en ruby ​​para la biblioteca ActiveRecord:


 posts = Post.all # select * from posts posts.each do |post| like = post.likes.order(id: :desc).first # SELECT * FROM likes WHERE post_id = ? ORDER BY id DESC LIMIT 1 # ... end 

ORM lleva al programador a la idea de que simplemente trabaja con objetos en la memoria. Pero funciona con un servicio disponible a través de la red y en el establecimiento de conexiones y transferencia de datos.
toma tiempo Incluso si la solicitud se ejecuta 50 ms, se ejecutarán 20 solicitudes por segundo.


Datos adicionales


Digamos que para evitar el problema N + 1 descrito anteriormente, usted escribe tal
solicitud :


 SELECT * FROM posts JOIN LATERAL ( SELECT * FROM likes WHERE post_id = posts.id ORDER BY likes.id DESC LIMIT 1 ) as last_like ON true; 

Es decir Además de los atributos de la publicación, también se seleccionan todos los atributos del último Me gusta. ¿A qué entidad se asignan estos datos? En este caso, puede devolver un par de la publicación y dar me gusta, porque El resultado contiene todos los atributos necesarios.


Pero, ¿qué sucede si seleccionamos solo parte de los campos o campos seleccionados que no están en el modelo, por ejemplo, el número de me gusta de publicación? ¿Necesitan ser mapeados en entidades? ¿Quizás dejarlos solo datos?


Estado e identidad


Considere el código js:


 const alice = { id: 0, name: 'Alice' }; 

Aquí, la referencia del objeto recibió el nombre de alice . Porque es una constante, entonces no hay forma de llamar a Alice otro objeto. Al mismo tiempo, el objeto en sí permaneció mutable.


Por ejemplo, podemos asignar un identificador existente:


 const bob = { id: 1, name: 'Bob' }; alice.id = bob.id; 

Permítame recordarle que una entidad tiene 2 identidades: un enlace y una clave principal en la base de datos. Y las constantes no pueden dejar de hacer Alice Bob, incluso después de guardar.


El objeto, el enlace al que llamamos alice , realiza 2 tareas: modela simultáneamente identidad y estado. Un estado es un valor que describe una entidad en un momento dado.


Pero, ¿qué sucede si separamos estas 2 responsabilidades y usamos estructuras inmutables para el estado?


 function Ref(initialState, validator) { let state = initialState; this.deref = () => state; this.swap = (updater) => { const newState = updater(state); if (! validator(state, newState) ) throw "Invalid state"; state = newState; return newState; }; } const UserState = Immutable.Record({ id: null, name: '' }); const aliceState = new UserState({id: 0, name: 'Alice'}); const alice = new Ref( aliceState, (oldS, newS) => oldS.id === newS.id ); alice.swap( oldS => oldS.set('name', 'Queen Alice') ); alice.swap( oldS => oldS.set('id', 1) ); // BOOM! 

Ref : un contenedor para un estado inmutable, que permite su reemplazo controlado. Ref modela la identidad tal como nombramos objetos. Llamamos al río Volga, pero en cada momento tiene un estado diferente que no cambia.


Almacenamiento


Considere la siguiente API:


 storage.tx( t => { const alice = t.get(0); const bobState = new UserState({id: 1, name: 'Bob'}); const bob = t.create(bobState); alice.swap( oldS => oldS.update('friends', old => old.push(bob.deref.id)) ); }); 

t.get y t.create devuelven una instancia de Ref .


Abrimos la transacción comercial t , encontramos a Alice por su identificador, creamos a Bob e indicamos que Alice considera a Bob su amigo.


El objeto t controla la creación de la ref .


Puede almacenar en sí mismo la asignación de identificadores de entidad al estado de ref que los contiene. Es decir puede implementar Identity Map. En este caso, t actúa como caché; ante la solicitud repetida de Alice, no habrá solicitud a la base de datos.


Puede recordar el estado inicial de las entidades para rastrear al final de la transacción qué cambios deben escribirse en la base de datos. Es decir puede implementar la Unidad de trabajo . O, si se agrega soporte de observador a Ref , es posible restablecer los cambios en la base de datos con cada cambio en ref . Estos son enfoques optimistas y pesimistas para arreglar los cambios.


Con un enfoque optimista, debe realizar un seguimiento de las versiones estatales de las entidades.
Al cambiar de la base de datos, debemos recordar la versión, y al confirmar los cambios, verificar que la versión de la entidad en la base de datos no difiera de la inicial. De lo contrario, debe repetir la transacción comercial. Este enfoque permite el uso de operaciones de inserción y eliminación de grupo y transacciones de bases de datos muy cortas, lo que ahorra recursos.


Con un enfoque pesimista, una transacción de base de datos es totalmente coherente con una transacción comercial. Es decir nos vemos obligados a retirar la conexión del grupo en el momento en que se completa la transacción comercial.


La API le permite extraer entidades de una en una, lo que no es muy óptimo. Porque hemos implementado el patrón del Mapa de Identidad , luego podemos ingresar el método de preload en la API:


 storage.tx( t => { t.preload([0, 1, 2, 3]); const alice = t.get(0); // from cache }); 

Consultas


Si no queremos transacciones largas, entonces no podemos hacer selecciones por una clave arbitraria, porque la memoria puede contener objetos sucios y la selección devolverá un resultado inesperado.


Podemos usar Query y recuperar cualquier dato (estado) fuera de la transacción y volver a leer los datos mientras está en la transacción.


 const aliceId = userQuery.findByEmail('alice@mail.com'); storage.tx( t => { const alice = t.getOne(aliceId); }); 

Por lo tanto, hay una división de responsabilidad. Para consultas, podemos usar motores de búsqueda para escalar la lectura usando réplicas. Y la API de almacenamiento siempre funciona con el almacenamiento principal (maestro). Naturalmente, las réplicas contendrán datos obsoletos, volver a leer los datos en la transacción resuelve este problema.


Comandos


Hay situaciones en las que se puede realizar una operación sin leer datos. Por ejemplo, deduzca una tarifa mensual de las cuentas de todos los clientes. O inserte y actualice datos (upsert) en caso de conflicto.


En caso de problemas de rendimiento, el paquete de Almacenamiento y Consulta se puede reemplazar con dicho comando.


Comunicaciones


Si las entidades se refieren aleatoriamente entre sí, es difícil garantizar la coherencia al cambiarlas. Las relaciones intentan simplificar, racionalizar, abandonar innecesariamente.


Los agregados son una forma de organizar las relaciones. Cada agregado tiene una entidad raíz y entidades anidadas. Cualquier entidad externa solo puede referirse a la raíz del agregado. La raíz asegura la integridad de toda la unidad. Una transacción no puede cruzar un límite agregado; en otras palabras, todo el agregado está involucrado en la transacción.


Un agregado puede, por ejemplo, consistir en Cuaresma (raíz) y sus traducciones. O la Orden y sus Posiciones.


Nuestra API funciona con agregados completos. Al mismo tiempo, la integridad referencial entre los agregados recae en la aplicación. La API no admite la carga diferida de enlaces.
Pero podemos elegir la dirección de las relaciones. Considere la relación uno a muchos Usuario - Publicar. Podemos almacenar la identificación de usuario en la publicación, pero ¿será conveniente? Obtendremos mucha más información si almacenamos una matriz de identificadores de publicación en el usuario.


Conclusión


Hice hincapié en los problemas al trabajar con la base de datos, mostré la opción de usar inmunidad.
El formato del artículo no permite ampliar el tema.


Si está interesado en este enfoque, preste atención a mi aplicación de libros desde cero , que describe la creación de una aplicación web desde cero con énfasis en la arquitectura. Entiende SOLIDOS, Arquitectura limpia, patrones de trabajo con la base de datos. Los ejemplos de código en el libro y la aplicación en sí están escritos en el lenguaje Clojure, que está imbuido con las ideas de inmunidad y la conveniencia del procesamiento de datos.

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


All Articles