Durante aproximadamente un año he estado leyendo un poco sobre ese problema de llevar SQL y lógica relacional a otras representaciones, en particular a la POO. Lo primero que surge a notarse es eso que llaman Object-Relational impedance mismatch. Mucho he leido sobre este problema, y mucho he pensado al respecto. Esta guerra es una yihad tan similar a la guerra de los lenguajes de programación que hace pensar sobre nosotros los programadores. Por un lado se tienen cruzados defensores del alado señor ORM, por el otro a los guerreros del anti-patrón. No esperen que tome bandos.
Ahora, quisiera dar algunas opiniones al respecto sin duda alguna, porque en realidad es notable la cantidad de concepciones que he visto respecto a esto. Dividiré mis opiniones en distintos campos.
¿Abstracción?
En una conversación (varias en realidad) con algunos colegas hemos conversado en la calidad del paradigma orientado a objetos como un candidato para representación de nivel superior del SQL y el modelo relacional, siempre llegando a la misma conclusión: no es opción. Sin duda alguna esto se encuentra bien justificado y las razones de los defensores de la impedance mismatch tienen mucha razón.
En primera, esa correspondencia que va del álgebra relacional a la programación orientada a objetos escasamente funciona (nótese la dirección de la relaciòn). La razón para esto es bien simple: el álgebra relacional no tiene lugar alguno dentro del paradigma orientado a objetos. Es evidente que las operaciones nativas del álgebra relacional difícilmente podrían tener un equivalente en la programación orientada a objetos, siendo uno de los ejemplos más notables los distintos tipos de join. La mayoría de los frameworks ORM hacen equivalencias parciales de las relaciones mediante métodos que simulan los nexos entre los distintos objetos, una solución medianamente válida. Si bien esto permite simular lo necesario en los framework ORM comienzan a ocurrir ciertas faltas
- Ineficiencia: En diversos ORM se falla en establecer estas relaciones dado que las query construidas son de terrible calidad y eficiencia. Un ejemplo de esto es Active Record en Ruby.
- Convenciones poco convencionales: Otros ORM toman una alternativa mucho peor, el desarrollo de convenciones de nombramiento dentro de los esquemas de base de datos a fin de dar información al framework de las relaciones entre las tablas (y a su vez de las clases). No es necesario decir que estas convenciones no son consistentes entre los frameworks. Igualmente tienen reglas de pluralización que evidentemente son dependientes del idioma.
Esto es apenas un abreboca de las carencias de la impedancia en la dirección relaciones -> objetos, podríamos ahondar un poco más pero no es esa la intención.
Por otra parte, debemos discutir la correspondencia entre la programación orientada a objetos y sus restricciones hacia el álgebra relacional. En este caso la impedancia crece aún más: encapsulamiento, polimorfismo, incapacidad de representar comportamiento, herencia, entre otros. Para mayores referencias recomiendo el artículo de Wikipedia, es mucho más extenso en este aspecto y lo define adecuadamente. Por supuesto, algunas de las carencias pueden ser subsanadas mediante algunas técnicas (por ejemplo, la herencia representada mediante relaciones “m a n” múltiples para cada subclase) pero esto no es más utilizar “trucos”, forzar la impedancia.
Prefiero no ahondar en aspectos teóricos (quisiera no perder su interés tan pronto, i.e. TL;DR), por consiguiente, en resumen, no existe forma razonable de hacer una equivalencia de abstracción entre el paradigma orientado a objetos y el modelo relacional. Sin embargo, existen grupos trabajando en otros modelos de alto nivel que podrían funcionar como equivalencias del SQL y el modelo relacional: grafos, teoría de categorías, lógica de descripción, entre otros.
Pero ¿Por qué ORM?
Ahora, si creen que lo que digo sobre ORM es cierto (si no me creen quizás crean a otros, una búsqueda en google de “orm criticism” o “orm antipattern” bastará) comenzamos a pensar, ¿tenemos alternativa? Por supuesto, esta alternativa es el ORM.
Esperen, sí, acabo de decir que ORM es el problema y la solución. Es cierto, ORM es el problema, pero es el problema ya que se pretende usar ORM como una abstracción, y simplemente esta abstracción no es posible. Pero ORM es la solución, usada y vista de manera apropiada. ORM ha querido usarse como un modelo para el dominio del problema y definitivamente carece de este potencial, pero definitivamente tiene potencial como abstracción para modelos de persistencia.
No hay duda que ha llegado el momento de sustituir SQL como lenguaje para bases de datos y para ilustrar esto cito a un amigo y formalista, Manuel Gomez
Llevamos un montón de décadas entendiendo que es muy, muy angosto el dominio en el cual assembly es un buen lenguaje para describir programas, pero consideramos algo normal describir esquemas de datos en SQL directamente, y las pocas abstracciones que aprendemos a usar son insuficientemente expresivas para describir en forma natural mucho de lo que quisiéramos modelar
No obstante, algunas de las soluciones más robustas se encuentran bajo la forma de ORM.
Definitivamente los desarrolladores en general no queremos escribir código SQL y razones no nos faltan pero, de momento, debido a lo carente de mejores alternativas (fuera de opciones como bases de datos orientadas a objetos) ORM es una muy buena opción como alternativa, siempre y cuando dejemos de pretender que varias dificultades concernientes a ORM no existen
- ORM no te va a abstraer de nada: Debemos dejar de querer utilizar ORM como una forma de no tener que saber SQL, ORM es incapaz de esto, completamente incapaz. Deberás seguir siendo familiar al modelo relacional, deberás seguir siendo familiar al SQL, ORM no sabrá ni es capaz de eliminar esta necesidad.
- Usarás tiempo en benchmarking: Como dijimos anteriormente, ORM es ineficiente y conocer las transformaciones SQL que el framework que uses realiza es necesaria a fin de verificar la mejor forma de utilizarlo. Esto implicará una curva de esfuerzo al inicio del uso.
- No tienes la panacea: ORM no es la solución final a todo problema que implique el uso de bases de datos relacionales, aunque la calidad del framework tendrá bastante importancia en la cantidad de problemas que puedas cubrir. Muchos framework ORM carecen de formas de establecer query multitablas eficientemente o joins de forma explícita (despues de todo tratan de mantener la ilusión de abstracción) al igual que realizan en múltiples consultas operaciones que podrían realizar en menor cantidad. Esas tareas que tu framework no pueda realizar de manera simple o explícita deberás solucionarlas con SQL.
Aha, ¿Y Perl?
Por supuesto, este blog es sobre Perl, ¿Qué se supone que hacemos escribiendo sobre ORM? Pues quería introducirles un excelente framework para modelos de persistencia: DBIx::Class.
He trabajado con diversos framework ORM a lo largo de diversos trabajos freelance y muchas de las carencias que les he comentado a lo largo del post las he visto repetidas veces. La calidad de los frameworks ORM en general lamentablemente suele ser muy baja y la cantidad de problemas que pueden resolver más allá de CRUD simples es realmente baja. Igualmente la forma de usos (generalmente asociada a la idiosincrasia de los lenguajes) tampoco suele hacer muy cómoda la forma de operar con ellos.
La unión de las figuras sintácticas de Perl y la ausencia de pretensión de abstracción del framework DBIx::Class logran darle un atractivo que no he logrado encontrar en otros framework ORM. Características como joins explícitos, multitabla, multinivel simple de escribir; paginación y prefetch hacen a este ORM eficiente y atractivo.
Una de las características que (personalmente) resultan sumamente atractivas y son mayormente divulgadas de DBIx::Class es su lector de esquemas de base de datos DBIx::Class::Schema::Loader.
La gran mayoría de los framework que dan opciones similares (por ejemplo, el ORM del framework Cake PHP) generan modelos y reconocen relaciones de forma automática, pero deben cumplir formatos en los campos de la base de datos, caso contrario no detectan características.
Esta carencia puede resultar molesta al momento de desarrollar aplicaciones alrededor de bases de datos existentes, caso nada inusual, donde los desarrolladores deberán escribir configuraciones adicionales a fin de detectar relaciones o inclusive cosas tan simples como el campo clave primaria.
DBIx::Class no sufre de este problema gracias a que el mismo toma gran cantidad de información del esquema y genera las relaciones al comprender las claves foraneas en lugar de simplemente los nombres de las columnas.
Cuando se quiere invocar este traductor es bastante simple y altamente configurable. Si se quiere hacer desde una cónsola este provee de un script que se encarga de realizarlo:
dbicdump -o dump_directory=./lib \ -o components='["InflateColumn::DateTime"]' \ -o debug=1 \ My::Schema \ 'dbi:Pg:dbname=foo' \ myuser \ mypassword
E igualmente provee de medios para crear códigos propios alrededor del traductor.
Ahora, he hablado mucho de diferencias teóricas que cumple DBIx::Class, pero esto es difícilmente suficiente para que crean lo que les indico. Así que les compartiré un caso de uso reciente.
Recientemente me encontraba trabajando en un código académico para un laboratorio de mis estudiantes, cuando me encontré con una consulta entre tres tablas relacionadas en serie con paginación sobre la tabla principal y la necesidad de obtener información de las tablas intermedias. Dado que de antemano sabía que utilizaría la información de la relación decidí resumir la cantidad de consultas en una sola mediante joins en serie por supuesto. Luego de investigar un poco la consulta resultó en el siguiente código:
my @users = $player_schema->search(undef, { page => 1, rows => 20, join => { 'have_units' => 'unit' }, prefetch => { 'have_units' => 'unit' } }, );
Nada impresionante de momento, un código orientado a objetos usual. No obstante, decidí realizar el debug sobre el código y observar el SQL generado
SELECT me.id, me.username, me.password, have_units.user, have_units.unit, have_units.amount, unit.id, unit.name, unit.description, unit.points, unit.attack, unit.defense, unit.load, unit.type FROM (SELECT me.id, me.username, me.password FROM player me LIMIT ?) me LEFT JOIN have_unit have_units ON have_units.user = me.id LEFT JOIN unit unit ON unit.id = have_units.unit: '20'
Mucho mejor sin duda alguna.
Si aún no logro impresionarlos los invito a revisar el cookbook del framework, donde podrán encontrar más chucherías interesantes.
Doy por finalizada la publicación por ahora, son aceptadas y bienvenidas quejas, comentarios y opiniones (en particular en este tema, bien llamado el Vietnam de la ciencia de la computación).