Perl/Rose::DB

De Astillas.net
Módulo Rose::DB
Versión 0.769-1
Uso Construcción de programas y aplicaciones
Propósito Gestor de relaciones entre objetos y bases de datos relacionales


Enlaces y referencias

Pendiente

Esta sección incluye aquellos conceptos que tengo pendientes de resolver o estudiar de una forma más satisfactoria que la actual:

  • Inicialización
    • Definir un mecanismo de inicialización de aplicaciones que usen esta biblioteca.
  • Errores
    • Definir la generación de errores
    • Incorporar esta generación a los módulos generales de aplicación
  • Gestor de objetos individuales (Rose::DB::Object)
    • Relaciones
      • Personalización de métodos
    • Tipos de datos compartidos para una determinada aplicación
  • Gestores de objetos múltiples (Rose::DB::Object::Manager)
    • Estudiar la personalización de los métodos de clase automáticos, especialmente en el nombre.

Conceptos

La herramienta Rose::DB está basada en la premisa de la abstracción. Emplea para ello varias clases con un comentido concreto.

Rose::DB

Rose::DB es una capa de abstracción para la funcionalidad proporcionada por DBI pero sin heredar nada de ella; en su lugar la clase Rose::DB tiene una referencia a un objeto DBI como atributo.

La herramienta añade también abstracción sobre las bases de datos finales y para ello identifica las fuentes de datos empleando un espacio de nombres a dos niveles consistente en un dominio y un tipo. Ambos son textos arbitrarios y si no se especifican se emplean valores predefinidos (que pueden obtenerse mediante los métodos de clase default_domain() y default_type()).

El autor recomienda utilizar el dominio para identificar el entorno (desarrollo, pruebas, producción, ... ) y el tipo para indicar el contenido lógico de la base de datos (informes, archivo, principal, ...).

Rose::DB::Object

Esta clase base está diseñada para encapsular una única fila en una tabla de la base de datos con la siguientes capacidades:

  1. Crear una fila en la base de datos (método save de un objeto).
  2. Inicializar un objeto o cargarlo desde una fila de la base de datos.
  3. Modificar una fila desde un objeto.
  4. Borrar una fila de la base de datos.
  5. Recuperar la información de objetos externos a los que se referencia mediante claves en el objeto (foreign keys) o mapas de relaciones.

La carga de los objetos se realiza bien mediante la clave primaria, bien mediante una clave única.

Rose::DB::Object::Manager

Esta otra clase base (hermana de la anterior) está pensada para tratar con varias filas de una base de datos, complementando a Rose::DB::Object y separando conceptualmente los dos aspectos: gestionar una fila como entidad y manipular varias como un conjunto.

Con ella se puede hacer:

  1. Recuperar un conjunto de objetos desde la base de datos empleando condiciones y límites.
  2. Iterar sobre un conjunto de objetos paso a paso.
  3. Cargar objetos referenciados junto con los indicados expresamente.
  4. Empleando consultas complejas se puede también:
    1. Contar el número de objetos resultantes.
    2. Actualizar los objetos.
    3. Borrar los objetos en la base de datos.

Claúsula de búsqueda

Las claúsulas de búsqueda permiten construir sentencias SQL de selección de filas mediante la parametrización de sus partes.

columns HASHREF
Referencia a una tabla hash indexada por el nombre de tabla y que contiene referencias a listas de nombres de columnas de cada una. Estas serán las columnas recuperadas en la consulta.
logic "AND" | "OR"
Unión predeterminada para las claúsulas de la consulta. Si no se especifica se utiliza siempre AND.
query PARAMS
Condiciones de selección de filas formadas por una referencia a una lista de pares nombre/valor, referencias a valores escalares o referencias a otras listas.

Mapa de métodos disponibles

Rose::DB::Object

Cada columna definida en el objeto tiene asociada un método de lectura y escritura con el nombre de la misma. La siguiente relación de métodos constituyen una lista de nombres reservados por lo que si la tabla contiene alguno de ellos tendrán que utilizarse un alias en la definición.

Método Descripción y parámetros
db [DB] Obtiene o cambia el objeto Rose::DB empleado para acceder a la base de datos que contiene la tabla correspondiente a la clase.
  • Referencia a objeto Rose::DB
dbh [DBI] Obtiene o cambia el manejador DBI.
  • Referencia a manejador de bases de datos DBI.
error Devuelve el mensaje de error derivado de la última operación realizada.
not_found Retorna verdadero si la última llamada a load ha fallado debido a que no existe el objeto según clave primaria o única. Retorna falso en caso contrario.
meta Retorna una instancia a Rose::DB::Object::Metadata que describe la tabla, columnas y claves asociadas con la clase.
delete [%params]

Borra la fila representada por el objeto actual. Éste debe haber sido cargado previamente vía load o tener definida la clave primaria. Retorna verdadero si ha tenido éxito y falso en caso contrario.

Los parámetros pueden ser:

cascade delete | null | 1
Ver borrado en cascada
prepare_cached BOOL
Ver #prepare_cached
insert [%params]

Inserta el objeto actual en la base de datos. Sólo debe emplearse cuando se quiere forzar la inserción del objeto en lugar de su actualización.

Los parámetros pueden ser:

changes_only BOOL
Si es verdadero únicamente las columnas cuyo valor halla sido modificado serán empleadas en la instrucción insert.
prepare_cached BOOL
Ver #prepare_cached
load [%params]

Carga el objeto desde la fila correspondiente en la base de datos empleando bien la clave primaria, bien una clave única. Las columnas se inicializan con los valores de la fila.

Retorna uno de estos tres casos:

  • Referencia al objeto si la operación ha tenido éxito
  • Valor nulo (undef) si no ha podido ser cargada debido a un error.
  • Valor cero (0) si la fila no existe

Los parámetros pueden ser:

lock [TYPE | HASHREF ]
Carga el objeto empleando algún tipo de mecanismo de bloqueo.
for_update BOOL
Equivalente a utilizar el parámetro lock con el tipo for update.
nonlazy BOOL
Si es verdadero se cargan desde la base de datos todas las columnas incluyendo aquellas de carga retardada (lazy).
prepare_cache BOOL
Ver #prepare_cached
use_key $key
Emplea la clave indicada para efectuar la carga desde la base de datos.
with \@objects
Junto con el objeto actual carga la lista de objetos externos indicada en el parámetro.
update

Actualiza el objecto actual en la base de datos. Sólo debe emplearse cuando se desee forzar una operación de actualización (update) en lugar de una inserción (insert).

Los parámetros pueden ser:

changes_only BOOL
Incluye en la operación de actualización (update) sólo las columnas que hayan sufrido algún cambio.
prepare_cached BOOL
Ver #prepare_cached
save [%params]

Guarda el objeto actual en la base de datos. En ausencia de parámetros, si el objeto ha sido previamente cargado desde la base de datos, la fila será actualizada (update). En caso contrario la fila será insertada (insert).

Retorna una referencia al objeto si la operación ha tenido éxito y un valor falso en caso contrario. Si la operación de inserción ha generado un nuevo valor en la clave primaria (auto generado) éste estará reflejado en la correspondiente columna.

Los parámetros posibles son:

cascade BOOL
Ver actualización en cascada
changes_only BOOL
Si es verdadero las operaciones de actualización o inserción contendrán sólo las columnas con cambios.
insert BOOL
Si el valor es verdadero se fuerza a que la operación sea una inserción.
update BOOL
Si el valor es verdadero se fuerza a que la operación sea una actualización. Es incompatible con el parámetro insert.
prepare_cached BOOL
Ver #prepare_cached

prepare_cached

Cuando una de las operaciones anteriores requiera construir una instrucción SQL y acepta éste parámetro, un valor verdadero determina el empleo del método DBI prepare_cached cuando se compila la instrucción SQL correspondiente. Dicho método verifica que la instrucción ya exista compilada y no vuelve a contactar con el servidor SQL.

En caso contrario, si el parámetro es falso, se utiliza el método DBI prepare que -según el controlador de base de datos- puede llegar a contactar siempre con el servidor.

delete cascade

El borrado en cascada significa que se incluyen en el mismo las filas relacionadas con la actual. El tipo puede ser delete o null, siendo el valor 1 un sinónimo de delete.

Según el tipo de relación y el tipo de borrado tendremos:

Relación Tipo delete Tipo null
Uno a muchos

Se borran todas las filas en la tabla externa que referencia a la fila que estamos borrando.

Se asigna a nulo el valor de la columna, en todas las filas de la tabla externa, que contengan una referencia a la fila que borramos.

Muchos a muchos

Borrado de todas las filas en la tabla mapa que referencien a la fila que borramos.

Asignación de nulo en todas las columnas de la tabla mapa que referencia a esta fila.

Uno a uno

Borrado de la fila correspondiente en la tabla externa.

Asignación de valor nulo en la columna que referencia a ésta en la tabla externa.

En todos los casos se crea una nueva transacción si no existe una activa. Cualquier fallo en cualquier operación provoca la cancelación de todas las demás.

save cascade

La actualización de la base de datos en cascada afecta también a aquellos objetos que tengan una relación con el que provoca al acción, que hayan sido cargados previamente y que tengan algún valor modificado.

Este procedimiento se lleva a cabo recursivamente sobre todos los sub-objetos y dentro de una transacción. Si alguna de las operaciones falla todo el grupo lo hace.

Rose::DB::Object::Manager

Método Descripción y parámetros
delete_objects

Borra filas de una tabla basándose en los parámetros recibidos.

Estos pueden ser además de los comunes:

all => 1http
//feedproxy.google.com/~r/SecurityByDefault/~3/kamKDly_PfU/podemos-fiarnos-de-truecrypt.html?utm_source=feedburner&utm_medium=email
Borra todas las filas de la tabla
where => \%where_clausule
Claúsula de selección de filas.
get_objects

Obtiene filas de la base de datos en forma de objetos Rose::DB::Object según los parámetros recibidos. El valor retornado es una referencia a una lista de objetos (posiblemente vacía si no ha encontrado ninguno) o un valor nulo (undef) si ha habido algún error.

Los parámetros pueden ser (además de los comunes):

allow_empty_lists => BOOL
distinct => BOOL | \@table_names
fetch_only => \@table_names
for_update => BOOL
limit => BOOL
limit_with_subselect => BOOL
nested_joins => BOOL
multi_many_ok => BOOL
nonlazy => BOOL | \@relationship_names
object_args => \%constructor_values
offset => NUM
Número de filas iniciales a descartar. Debe emplearse junto con el parámetro limit.
page => NUM
Número de página de resultados a obtener comenzando por uno (1). Incompatibles con los parámetros limit y offset.
per_page => NUM
Número de objetos a recuperar por página.
query|where => \@select_parameters
require_objects => \@relationships_names_or_foreign_keys
select => "columns_names_with_comma" | \@column_names
sort_by => "order_by_clause" | \@column_names
table_aliases => BOOL
unique_aliases => BOOL
with_map_records => BOOL | \&method | \%relationship_keys
with_objects => \@foreign_keys_or_relationship_names
get_objects_count
get_objects_from_sql
get_objects_iterator
get_objects_iterator_from_sql
get_objects_sql
object_class
update_objects
make_manager_methods

Este método fabrica métodos de nombre alternativo en la clase destino para las siguientes operaciones: get_objects, get_objects_iterator, get_objects_count, delete_objects y update_objects.

Para ello este método precisa de los siguientes parámetros bien directa o bien indirectamente:

object class
Clase derivada de Rose::DB de la cual leer o contar las filas. En su ausencia se llamará al método de clase object_class para obtener dicho valor.
base name | method name
base name es un texto que se emplea como plantilla al crear los métodos para cada operación. En su ausencia se deberá proporcionar un nombre de método (method name) que se empleará directamente.
Si se proporciona un nombre base como articulos se utilizará para crear los métodos de la siguiente forma: get_articulos, get_articulos_count, get_articulos_count, delete_articulos y update_articulos. Eso quizás no sea lo más adecuado cuando se mezclan idiomas.
method types
Los tipos de métodos que deben ser generados y que corresponden a: objects, iterator, count, delete y update.
Si se emplea el parámetro methods éste debe contener una referencia a un hash cuyas claves serán los nombres de los métodos y los valores uno los tipos anteriores. Si el nombre de la clave termina en paréntesis se utilizará directamente como method name; en caso contrario se usará como base name para formar el nombre final.
# 
# Crea los métodos get_articulos, get_articulos_iterator y contar_articulos
MyManager->make_manager_methods( 
   methods => {
     "articulos"          => [ qw(objects iterator) ],
     "contar_articulos()" => 'count',
   } );
target class
La clase donde los métodos van a ser generados. Si este parámetro se omite y la clase que llama no es Rose::DB::Object::Manager ésta se empleará como clase destino.
make_manager_method_from_sql

Crea un método de clase que recuperará objetos empleando una instrucción SQL personalizada. El valor retornado por el método será una referencia a una lista de objetos a menos que se utilice el parámetro iterator.

Los parámetros son:

iterator => BOOL
Si está activo el método creado retornará un iterador (Rose::DB::Object::Iterator. En caso contrario obtendrá directamente los objetos, los empaquetará en una lista y retornará una referencia a la misma.
object_class => CLASS
Clase de los objetos retornados. Si se omite se utilizará el valor devuelto por el método de clase object_class.
method => "name"
Nombre del método. Obligatorio.
sql => $sql
Instrucción SQL. Puede contener marcas de parámetros (placeholders) que serán rellenados con los que reciba el método en su llamada. Obligatoria.
params => \@nombres_de_parametros
Proporciona una lista de nombres que se emplearán con las marcas de parámetros de la instrucción SQL.
shared_db => BOOL
Si es verdadero (valor predeterminado) el objeto db se compartirá con cada objeto Rose::DB::Object creado.
MyManager->make_manager_method_from_sql( 
     method   => 'clientes_nuevos',
     iterator => 1,
     params   => [ qw(fecha_alta) ],
     sql      => <<EOF
select * from personas where fecha_alta >= ?;
EOF
);

my $iterator = MyManager->clientes_nuevos( fecha_alta => '03/03/2013' );

while (my $cliente = $iterator->next) {
  ...
}

Parámetros comunes

La siguiente lista contiene los parámetros comunes para varios de los métodos de clase de la sección anterior:

prepare_cached => 1
Ver empleo de caché en preparación de sentencias SQL
db => Rose::DB
Objeto derivado de Rose::DB para acceder a la base de datos. Si se omite se creará uno llamando al método init_db de la clase del objeto.
object_class => CLASS
Nombre de la clase derivada de Rose::DB que confronta la tabla de la base de datos. Si se omite -dada su importancia- se llama al método de la clase object_class.

Relaciones

Cuando se declaran relaciones entre tablas se crean métodos en la clase declarante que permiten manipular los objetos de la relación.

Los métodos aquí descritos van a usar el nombre relacion como ejemplo para determinar el resultado final. Estos nombres pueden cambiarse empleando el parámetro methods cuando se declara la relación.

__PACKAGE__->meta->setup( 
    relationships => [
        imagenes  => {
           type       => 'one to many',
           class      => 'Image',
           column_map => { id => 'product_id' },
           methods    => {
              count   => 'numero_de_imagenes',
              add     => 'agregar_imagenes',
              get     => 'reemplazar_imagenes',
           },
        },
    ],
    ...
);

one to one

Métodos posibles y nombres predeterminados Notas
get => relacion
Recuperar y modificar el objeto relacionado
delete => delete_relacion
Anula la relación

one to many

Métodos posibles y nombres predeterminados Notas
{ get_set | get_set_now | get_set_on_save } => relacion
Recuperar y modificar el objeto relacionado
delete => delete_relacion
Anula la relación
find => find_relacion
Encuentra objetos según las condiciones indicadas

one to many

Métodos posibles y nombres predeterminados Notas
get => relacion
Recuperar y modificar los objetos relacionados
{ add_now | add_on_save } => add_relacion
Añade uno o varios objetos relacionados
find => find_relacion
Recupera objetos relacionados
iterator => relacion_iterator
Itera sobre los objetos relacionados
count => relacion_count
Obtiene el número de objetos relacionados

Los interfaces find, iterator y count admiten los parámetros:

  • query
  • sort_by

many to many

Métodos posibles y nombres predeterminados Notas
get => relacion
Recuperar y modificar los objetos relacionados
{ add_now | add_on_save } => add_relacion
Añade uno o varios objetos relacionados
find => find_relacion
Recupera objetos relacionados
iterator => relacion_iterator
Itera sobre los objetos relacionados
count => relacion_count
Obtiene el número de objetos relacionados

Los interfaces find, iterator y count admiten los parámetros:

  • query
  • sort_by

Metodología

El uso recomendado consiste en

  1. Definir una fuente de datos
  2. Definir los objetos individuales y sus relaciones
  3. Definir los objetos grupales para manejar conjuntos de objetos individuales

Fuentes de datos

El autor recomienda crear una clase base como fuente de datos para asegurar idéntica configuración y origen de información al resto de las clases derivadas. Esto permite además mantener limpio el espacio de nombres de la biblioteca Rose::DB.

package My::DB;
use base qw(Rose::DB);
use strict;
use warnings;

# Empleamos nuestro propio registro de fuentes de datos 
__PACKAGE__->use_private_registry();

# y las declaramos
__PACKAGE__->register_db(
      domain   => My::DB->default_domain,
      type     => My::DB->default_type,
      driver   => 'pg',
      database => 'my_db',
      host     => 'localhost',
      username => 'ulises',
      password => 'mysecret',
);

1;

Después conviene crear nuestra propia clase base para que los objetos que gestionan las entidades de datos empleen las mismas fuentes de datos y aconsejan usar algo como:

package My::DB::Object;
use base qw(Rose::DB::Object);

use My::DB;

sub init_db { My::DB->new; }

1;

Objetos individuales

Empleando el siguiente esquema de relaciones como ejemplo voy a transcribir más abajo el fuente para declarar los distintos objetos:

Esquema-er-productos.png


package Imagen;
use base qw(My::DB::Object);

__PACKAGE__->meta->setup(
    table       =>  'imagenes',
    columns     =>  [ 
        id      =>  { type => 'int', primary_key => 1, not_null => 1 },
        author  =>  { type => 'varchar', not_null => 1 },
        url     =>  { type => 'varchar' },
        imagen  =>  { type => 'varchar', length => 255, not_null => 1 },
    ],
);

package Marca;
use base qw(My::DB::Object);

__PACKAGE__->meta->setup( 
    table       =>  'marcas',
    columns     =>  [ 
        id          =>  { type => 'int', primary_key => 1, not_null => 1 },
        descripcion =>  { type => 'varchar', not_null => 1 },
        imagen_id   =>  { type => 'int' },
    ],
    foreign_keys    =>  [
        imagen  =>  {
            class       =>  'Imagen',
            key_columns =>  { imagen_id => 'id' },
        }
    ],
);

package Producto;
use base qw(My::DB::Object);

__PACKAGE__->meta->setup( 
    table       =>  'productos',
    columns     =>  [
        id          =>  { type => 'int', primary_key => 1, not_null => 1 },
        descripcion =>  { type => 'varchar', not_null => 1 },
        marca_id    =>  { type => 'int', not_null => 1 },
    ],
    foreign_keys    =>  [
        marca   =>  {
            class       =>  'Marca',
            key_columns =>  { marca_id => 'id' },
        },
    ],
);

1;

Objetos grupales

Este tipo de objetos sirven para manipular listas de otros objetos y se definen derivando una clase de Rose::DB::Object::Manager.

Muchos de ellos precisan de sentencias query para realizar sus operaciones, además de otros parámetros que condicionan su funcionamiento.

package Productos;
use base qw(Rose::DB::Object::Manager);

__PACKAGE__->make_manager_methods( 
     methods => {
        "cargar_marcas()"      => 'objects',
        "iterar_marcas()"      => 'iterator',
        "contar_marcas()"      => 'count',
        "borrar_marcas()"      => 'delete',
        "modificar_marcas()"   => 'update',
     },
);

1;

El método de clase make_manager_methods tiene tendencia a definir diversos métodos cuyo nombre deriva de la clase base y la operación a realizar. Estas operaciones son las siguientes:

objects
Recupera en una lista un grupo de objetos (y objetos subordinados) utilizando una cláusula de búsqueda con una única instrucción select.
iterator
Crea y retorna un objeto iterador sobre un grupo de objetos con los mismos parámetros que el anterior.
count
Igual que el tipo objects pero retornando un contador de objetos.
delete
Borra un grupo de objetos empleando una claúsula de selección en una única instrucción. El parámetro all es un conmutador para seleccionar el borrado de todos los objetos bajo su ámbito.
update
Efectúa modificaciones en atributos de objetos utilizando una claúsula de búsqueda para seleccionarlos.

Recetario

Depuración

Cada módulo parece disponer de una variable llamada Debug que, debidamente activa, provoca la generación de avisos vía warn pero para la que no he encontrado documentación concisa y clara.

En alguno de mis módulos de test empleo lo siguiente:

# Para la biblioteca Rose::DB
use Rose::DB::Object;
$Rose::DB::Object::Debug = 1;

# y para DBI 
use DBI;

DBI->trace(2); # incluye parámetros pasados al controlador SQL

La siguiente es una muestra de los mensajes producidos tras un test sencillo de creación de objetos:

...
SELECT id, name, idioma, texto FROM descripciones WHERE name = ? - bind params: mrc-001
SELECT id, name, idioma, texto FROM descripciones WHERE id = ? - bind params: 8
INSERT INTO descripciones
(
  id,
  name,
  idioma,
  texto
)
VALUES
(
  ?,
  ?,
  ?,
  ?
) - bind params: 8, mrc-001, es, Ultracam
...