1 of 67

Mapeo Objetos / Relacional

Programación con Herramientas Modernas

2 of 67

El modelo de objetos y el relacional son diferentes

Los inconvenientes que surgen al persistir la información de objetos al modelo relacional se conocen como impedance mismatch, y las vamos a explicar en las diapositivas que siguen.

3 of 67

En el modelo relacional solo hay datos

Las funciones o stored procedures no están asociadas a una entidad, y la información en crudo puede también ser difícil de entender si no contamos con una documentación extra.

¿?

4 of 67

Consecuencias de almacenar solo datos

En este ejemplo, está claro que una interfaz stateless no tiene sentido persistirla. ¿Pero cómo mapeamos la referencia a criterioLimiteCredito en la tabla de clientes?

5 of 67

La pesada herencia

Asociar tabla = clase se dificulta cuando tenemos una jerarquía de objetos.

6 of 67

Referencias static/companion object

No podemos almacenar información relativa a una clase en la tabla (solo permite actualizar registros). Debemos crear entidades específicas (parámetros del sistema) para guardar esa información.

7 of 67

Ahora sí, comencemos a mapear

El recorrido va a incluir estos tópicos:

  • Identidad
  • Dominio / Conversiones de tipo
  • Cardinalidad / Relaciones de asociación: One to many - Many to one - Many to many
  • Subtipos / Herencia
  • Hidratación Lazy / Eager / Deshidratación
  • y otras cuestiones

8 of 67

Identidad

En el modelo de objetos nosotros tenemos al menos una referencia al objeto. En el modelo relacional necesitamos identificarlo unívocamente mediante la PRIMARY KEY (subrogada o natural), entonces es muy probable que mi modelo de objetos se vea afectado (agregando por ejemplo un atributo extra).

9 of 67

Dominio / Conversiones de tipos

  • El String de Java no tiene limitaciones => VARCHAR o CHAR requiere un tamaño fijo (cuántos caracteres)
  • Los Enums pueden almacenar solo valores o tener una tabla especial
  • Los números enteros o con decimales requieren también especificar una precisión en la base
  • El tipo de dato booleano se suele transformar a 0 / 1
  • Las infaltables fechas
  • Las constraints de SQL son populares para asegurar consistencia de mis datos (por ejemplo que el saldo sea siempre positivo, o que un String esté en un rango de valores permitidos), no tanto las invariantes en el modelo de objetos (hay pocos lenguajes que lo soportan: Eiffel, D, el framework Java Modeling Language de Java)

10 of 67

Cardinalidad: one-to-many

Si un pedido tiene muchos ítems, en el modelo de objetos tengo una colección de ítems en el pedido.

Pero en el modelo relacional, no está permitido tener atributos multivaluados (1FN), entonces la entidad hija (many) debe agregar una clave foránea hacia el identificador de la entidad madre (one).

11 of 67

Cardinalidad: one-to-many (continuación)

Vemos cómo implementar que un pedido tiene muchos ítems

En la opción A) la entidad Item tiene una clave subrogada autoincremental y el identificador del pedido no forma parte de la clave.

12 of 67

Cardinalidad: one-to-many (continuación)

En la opción B) la clave foránea de Pedido forma parte de la clave primaria del Item

13 of 67

Tipos de colecciones

Set: sin duplicados, no hay orden

Bag: admite duplicados, no hay orden

Lista: admite duplicados, hay orden

Ordered Set: sin duplicados, hay orden

Mapas clave / valor: acceso por clave, las claves no se repiten

14 of 67

¿Qué preguntas hacer para definir una colección?

15 of 67

Modelando una fila del supermercado

Como es una lista, necesitamos agregar en la base un campo orden, para poder saber el orden que cada elemento ocupa.

16 of 67

Resumen del mapeo de colecciones

Tipo de colección

¿Requiere columna orden?

La columna orden ¿forma parte de la PK?

Set

No

No

OrderedSet

No

List

Sí, a menos de que utilicemos un autoincremental

Bag

No necesariamente pero sí una columna adicional para poder repetir dos elementos

Sí, a menos de que utilicemos un autoincremental

17 of 67

Cardinalidad: many-to-one

La relación many-to-one es similar a la one-to-many, en la entidad hija (many) agregamos la foreign-key a la entidad madre (uno o varios campos, dependiendo de si la clave es simple o compuesta).

18 of 67

one-to-many vs. many-to-one

Para el modelo relacional, no parece haber demasiado diferencia. En el modelo de objetos, no obstante, eso depende del origen de la relación (si un pedido tiene muchos ítems, tendremos una colección, pero si la relación es unidireccional y va solo de Item a Pedido, se pierde una forma de navegar el grafo).

19 of 67

Cardinalidad: many-to-many

Si un vendedor vende muchos productos y un producto tiene muchos vendedores, en objetos puede modelarse como una relación bidireccional, tengo dos colecciones distintas:

20 of 67

Cardinalidad: many-to-many (continuación)

En el modelo relacional, el DER lógico admite relaciones many-to-many de la siguiente manera:

21 of 67

Cardinalidad: many-to-many (continuación)

Pero a la hora de implementarlo, la 1FN nos recuerda que “nada de atributos multivaluados”. Entonces debemos crear una entidad asociativa:

22 of 67

Cardinalidad: many-to-many (continuación)

Surge de la relación many-to-many pero además incorpora información propia

23 of 67

Cardinalidad: many-to-many (continuación)

Podemos incorporar relaciones bidireccionales en objetos, eso requiere mantener la información sincronizada (implica mayor esfuerzo).

24 of 67

Cardinalidad: many-to-many (continuación)

Por el contrario, el esquema relacional permite la navegabilidad en ambos sentidos, por la naturaleza de las claves foráneas:

25 of 67

Subtipos / Herencia

El DER lógico admite la definición de subtipos, como ocurre para cada uno de los productos en el ejemplo original:

26 of 67

Subtipos / Herencia (continuación)

Pero la implementación física de los subtipos requiere la adaptación, y veremos que hay tres estrategias para implementarlas:

  • SINGLE TABLE

27 of 67

Subtipos / Herencia (continuación)

Pero la implementación física de los subtipos requiere la adaptación, y veremos que hay tres estrategias para implementarlas:

  • SINGLE TABLE
  • TABLE PER CLASS

28 of 67

Subtipos / Herencia (continuación)

Pero la implementación física de los subtipos requiere la adaptación, y veremos que hay tres estrategias para implementarlas:

  • SINGLE TABLE
  • TABLE PER CLASS
  • JOINED

29 of 67

Estrategia Mapeo Herencia Table per class

Tenemos una entidad por cada clase concreta, lo que en nuestro ejemplo podría implementarse como:

30 of 67

Estrategia Mapeo Herencia Table per class (cont.)

En la mayoría de los frameworks de mapeo, en lugar de tener una FK por cada entidad concreta, se trabaja con un único campo. La desventaja: perdemos la FK y dentro de la base no podemos garantizar que haya consistencia (que no apunte a un identificador inválido, por ejemplo)

31 of 67

Estrategia Mapeo Herencia Table per class (cont.)

Vemos cómo se implementan ambas variantes para

  • Pedido 1, dos ítems
    • 2 u de Queso Camembert (fabricado)
    • 1 u de Queso Gruyère (conservado)
  • Pedido 2, dos ítems
    • 7 u de Jamón Cocido (comprado)
    • 3 u de Queso Camembert (fabricado)

32 of 67

Estrategia Mapeo Herencia Table per class (cont.)

  • Las entidades que surgen de las subclases de producto repiten los atributos definidos en la superclase
  • El nombre de la entidad sirve para saber a qué clase concreta representa cada registro

33 of 67

Estrategia Mapeo Herencia Joined

Una tabla por cada clase, incluyendo a las abstractas. Trata de normalizar el modelo de objetos.

34 of 67

Estrategia Mapeo Herencia Joined (cont.)

  • No necesita duplicar atributos en las tablas
  • Es el modelo que reproduce más fielmente el modelo de objetos
  • Permite trabajar con una FK de Item a Producto
  • Generar un nuevo producto requiere 2 inserts, y se necesita hacer JOIN siempre para obtener la información de un producto

35 of 67

Estrategia Mapeo Herencia Single Table

Toda la jerarquía se mapea a una sola tabla.

36 of 67

Estrategia Mapeo Herencia Single Table (cont.)

  • Item conserva su FK hacia la tabla de Productos
  • Como cada subclase puede tener diferentes atributos, es necesario admitir campos nulos
  • No requiere JOIN con ninguna otra tabla
  • Necesita un campo discriminante para que al hidratarse podamos saber qué instancia generar en el modelo de objetos

37 of 67

Análisis comparativo de las soluciones

Queremos conocer los productos de un proveedor dado que pesen más de 1 kg

SELECT columnasFROM PRODUCTOS prd� WHERE prd.PESO > 1AND prd.PROVEEDOR_ID = :ID_PROVEEDOR_A_BUSCAR

SINGLE TABLE

38 of 67

Análisis comparativo de las soluciones

Queremos conocer los productos de un proveedor dado que pesen más de 1 kg

SELECT columnas� FROM CONSERVADOS con� WHERE con.PESO > 1AND con.PROVEEDOR_ID = :ID_PROVEEDOR_A_BUSCAR� UNIONSELECT columnas� FROM COMPRADOS com� WHERE com.PESO > 1

AND com.PROVEEDOR_ID = :ID_PROVEEDOR_A_BUSCAR� UNIONSELECT columnas� FROM FABRICADOS fab� WHERE fab.PESO > 1AND fab.PROVEEDOR_ID = :ID_PROVEEDOR_A_BUSCAR

TABLE PER CLASS

como a priori no sabemos de qué instancia se trata, necesitamos hacer un UNION de las 3 tablas concretas (recordemos que en un UNION deben coincidir las columnas y los tipos de cada consulta)

39 of 67

Análisis comparativo de las soluciones

Queremos conocer los productos de un proveedor dado que pesen más de 1 kg

SELECT columnasFROM PRODUCTOS prd � LEFT JOIN CONSERVADOS CON� ON CON.PRODUCTO_ID = prd.ID � LEFT JOIN COMPRADOS COM� ON COM.PRODUCTO_ID = prd.ID � LEFT JOIN FABRICADOS FAB� ON FAB.PRODUCTO_ID = prd.ID� WHERE prd.PESO > 1AND prd.PROVEEDOR_ID = :ID_PROVEEDOR_A_BUSCAR

JOINED

como a priori no sabemos de qué instancia concreta se trata, necesitamos hacer un LEFT JOIN a cada subclase concreta

40 of 67

INNER JOIN vs. LEFT JOIN

create table personas

(id INT PRIMARY KEY,

nombre VARCHAR(100) NOT NULL);

create table perros

(id INT PRIMARY KEY,

apodo VARCHAR(100) NOT NULL,

persona_id INT NULL,

foreign key (persona_id)

references personas(id));

insert into personas values (1, 'catalina');

insert into perros values (1, 'firulais', 1);

insert into perros values (2, 'bobby', null);

select *

from perros p

join personas d

on p.persona_id = d.id;

select *

from perros p

left join personas d

on p.persona_id = d.id;

41 of 67

Análisis comparativo: relaciones -one

Supongamos ahora que queremos incorporar al modelo cuál es el producto estrella de cada proveedor.

  • Tanto SINGLE_TABLE como JOINED permiten agregar una FK PRODUCTO_ESTRELLA_ID en la entidad Proveedores.
  • En cambio la estrategia TABLE_PER_CLASS no permite trabajar con FK, como hemos visto anteriormente con la relación ITEM_PRODUCTO.

42 of 67

Análisis comparativo: campo discriminator

  • Tanto en la estrategia TABLE_PER_CLASS como en la JOINED la tabla define la instancia concreta a la que pertenece el objeto a instanciar cuando ocurra el proceso de hidratación
  • En la estrategia SINGLE_TABLE, necesitamos agregar esa información para el framework, de manera de saber la clase a la que pertenece el objeto a instanciar

43 of 67

Análisis comparativo: performance

La estrategia SINGLE_TABLE tiene una complejidad algorítmica O(log n), casi constante. Esto asegura que a mayor volumen de datos la performance de búsqueda se mantiene buena.

Por otra parte, SINGLE_TABLE nos garantiza un solo insert, algo importante cuando tenemos un gran volumen de datos.

El único inconveniente es si tenemos una gran cantidad de valores nulos, con lo que la tabla se vuelve ancha (sparse), va a requerir mayor almacenamiento del registro en disco.

44 of 67

Análisis comparativo: performance

En la estrategia TABLE_PER_CLASS cuando queremos hacer consultas de un valor específico puede tener un buen rendimiento (ya que evita JOINs). Cuando necesitamos acceder a información de diferentes clases (consultas polimórficas) vas a tener problemas de escalabilidad: el UNION ALL suele ignorar índices para consultas masivas.

Agregar un elemento sigue siendo, no obstante, rápido, es un solo insert.

45 of 67

Análisis comparativo: performance

En la estrategia JOINED sufrimos siempre: para leer información necesitamos OUTER JOINs, para insertar, hay que hacerlo en varias tablas.

El costo por lo tanto con un gran volumen de datos será mayor en CPU y en memoria.

46 of 67

Análisis comparativo de performance: un ejemplo

Hay una clase padre Vehiculo y tres subclases Auto, Moto y Camion. En una estrategia JOINED, tenemos 4 tablas físicas.

47 of 67

Análisis comparativo de performance: un ejemplo

Si pedimos buscar los vehículos de la empresa creados en febrero de este año,

  • Filtro inicial: La DB busca en la tabla Vehiculo los que son de febrero.
  • Costo de saltos: Una vez que tiene esos 100 IDs, tiene que ir a las tablas hijas (Auto, Moto, Camion) para ver qué "partes" le faltan a cada objeto.

Para eso tiene varias estrategias, cada una con sus pros y sus contras

48 of 67

Nested Loop Join

  • Cada elemento de Vehículo hay que compararlo contra Auto, Moto y Camion
  • Si los elementos están ordenados eso facilita el algoritmo de JOIN pero eso requiere un paso extra
  • Cuando las tablas son voluminosas, es una operación que requiere mucho uso de CPU
  • Si tenemos una gran cantidad de autos en febrero, la cache de memoria no va a ser suficiente para buscar en la tabla de autos y forzará la búsqueda en disco (mucho más lenta)

49 of 67

Merge Join

  • También se hace una comparación de Vehículo contra Auto, Moto y Camión
  • La recorrida es secuencial garantizando que los elementos están ordenados: ese paso requiere un uso intensivo de CPU o que tengamos los índices correctos para poder satisfacer esta estrategia
  • La ventaja: mientras que con los Nested Loops saltamos de una tabla a la otra (la probabilidad de buscar datos de distintas páginas aumenta), el merge join es secuencial

50 of 67

  • Aplicamos una función de hash para cada registro de vehículo
  • Se genera en memoria información intermedia (tal registro tiene hash 10, tal otro registro tiene hash 20)
  • Después, en el loop de las subclases se completa el join en base a la función de hash aplicada para auto, moto y vehículo
  • Cuando tenemos gran volumen de datos, habrá uso intensivo tanto de CPU como de memoria RAM, pero además si nos queda corta la memoria asignada para la consulta, tendremos que bajar los resultados a tempdb (disco, lo que agrega I/O y básicamente asegura una pésima performance)

51 of 67

¿Qué pasa con Single Table?

Si queremos buscar los vehículos generados en febrero, es una sola tabla.

En el mejor de los casos, tendremos un índice.

52 of 67

¿Qué pasa con Single Table?

Si no tenemos un índice, si queremos filtrar 10M de vehículos, vamos a necesitar un FULL SCAN, es una operación costosa que tiene un crecimiento exponencial. A la larga va a terminar generando un timeout y degradando la performance. Como para aclarar que ninguna estrategia de mapeo de herencia es garantía por sí sola.

53 of 67

BONUS: Análisis de performance

En este repositorio tenés el ejemplo implementado donde podés ver

  • que NESTED LOOP es bueno cuando tenés búsquedas puntuales (porque filtrás rápidamente los vehículos involucrados)
  • que MERGE es bueno cuando hacemos búsqueda de rangos previo un ORDER BY (tienen que estar ordenados)
  • que HASH es bueno para hacer búsquedas con agregaciones (count, sum, average, etc.)

54 of 67

Proceso de hidratación

  • Recuperamos la información de la base y construimos el grafo de objetos en la VM. Lo que debemos decidir es dónde cortar la búsqueda para no bajar todos los datos.

DB

55 of 67

Proceso de hidratación (cont.)

¿Qué query me conviene para poder resolver esta interfaz de usuario?

56 of 67

Proceso de hidratación (cont.)

¿Qué query me conviene para poder resolver esta interfaz de usuario?

SELECT *FROM PEDIDOS p,� JOIN CLIENTES cON p.CLIENTE_ID = c.ID

WHERE FECHA_PEDIDO BETWEEN :FechaDesde AND :FechaHasta

57 of 67

Proceso de hidratación (cont.)

¿Qué query me conviene para poder resolver esta interfaz de usuario?

SELECT *FROM PEDIDOS p,� JOIN CLIENTES cON p.CLIENTE_ID = c.ID

WHERE FECHA_PEDIDO BETWEEN :FechaDesde AND :FechaHasta

Cliente

Lista de items

1215259

Pedido

BASF SA

nombre

monto

58 of 67

Proceso de hidratación (cont.)

En cambio, si selecciono una factura para ver el detalle...

59 of 67

Proceso de hidratación (cont.)

En cambio, si selecciono una factura para ver el detalle...

Cliente

Lista de items

1215259

Pedido

Item

Item

Producto

100

1

Producto

BASF SA

60 of 67

Lazy association a través de un objeto proxy

La colección de ítems del pedido es lazy, esto significa que por defecto solo traemos información del pedido y el cliente (el JOIN se hace automático porque la relación es many-to-one).

Se implementa mediante un proxy, un objeto que simula ser la lista de ítems, y cuando se envía el primer mensaje se dispara la consulta a la base para traer la información que falta (necesitamos una sesión activa)

61 of 67

Lazy association a través de un objeto proxy (cont.)

Por ejemplo, si tenemos la sesión contra la base de datos aún abierta y se envía el mensaje itemMasCaro a un objeto pedido:

class Pedido {

...

fun itemMasCaro() = items.maxOf { it.precioTotal() }

Si la lista de items del pedido no es conocida, Hibernate va a disparar un query extra para poder calcular el ítem más caro (asumiendo que precioTotal se calcula como un simple atributo).

SELECT *FROM ITEMS i

WHERE i.PEDIDO_ID = :ID

62 of 67

Ciclo de vida de los objetos

  • ¿Tiene sentido guardar las notas de un alumne que no existe más?
  • ¿Y guardar los datos de un cliente aunque borremos la factura?

Hay objetos cuyo ciclo de vida está atado a otros: en ese caso debemos tener actualizaciones y eliminaciones en cascada. En otros casos, como en el segundo, tienen ciclos de vida independientes.

63 of 67

Data Mapper vs. Active Record

En la cursada vamos a modelar un objeto DAO (Data Access Object, en Springboot llamado Repository): la responsabilidad de definir queries estará en un objeto separado del dominio. Esto se conoce como Data Mapper. La otra variante que pueden investigar es Active Record (hay un ejemplo en Rails), donde al objeto de dominio se le inyectan métodos save(), delete(), etc.

Data Mapper

Active Record

64 of 67

Mapeo manual vs. Frameworks OR/M

El mapeo manual nos da más control, pero también nos obliga a definir muchas cosas. Los frameworks ayudan a tomar decisiones en forma más declarativa.

65 of 67

¿O -> R o R -> O?

La preponderancia del modelo de objetos o el relacional dependerá de muchos factores: si estamos mapeando un sistema existente (legacy), si nuestra aplicación hace principalmente transformación de datos (el juego está más del lado relacional), o si por el contrario la base de datos es únicamente donde descansa la persistencia de nuestra información.

66 of 67

Preguntas

67 of 67