Note

The online documentation is produced by a web publishing technology created by us to read the documents origins in OpenOffice Writer (ODT) and Microsoft Word (docx) formats and produces native web and PDF versions. In this way we maintain Louder project documentation update and in sync on each of its formats.
Select a Language:
Componente Active Record - ORM
Tabla de Contenido:
Introducción
Muchos de los procesos críticos del software en una empresa están relacionados con el funcionamiento, desarrollo y mantenimiento de los datos que representan la información, un activo muy valioso de una organización. Los entornos empresariales actuales requieren de la integración continua de requerimientos dadas las condiciones de un mundo cambiante y evolutivo. Las aplicaciones desarrolladas bajo un componente de acceso a la información poco escalable ó demasiado dependiente de componentes y proveedores tecnológicos, puede llevar a que una organización pierda clientes ó dinero debido a la imposibilidad de adaptarse rápidamente a las necesidades requeridas sin perder estabilidad ó requiriendo mayor tiempo de desarrollo e implementación generando sobre costos.

Un componente importante en Kumbia Enterprise Framework es el componente ActiveRecord. Este es el encargado de realizar el mapeo objeto-relacional y de encargarse de los modelos en la arquitectura MVC de las aplicaciones. El concepto de ORM se refiere a una técnica de mapear las relaciones de una base de datos a objetos nativos del lenguaje utilizado (PHP en este caso), de tal forma que se pueda interactuar con ellos en forma más natural. Los objetivos de este componente van más allá de mapear tablas y convertirlas en clases (incluyendo tipos de datos, constraints, lógica de dominio, etc.) ó de convertir registros en objetos. La idea es reducir los detalles de la interacción con las bases de datos en gran medida mediante varias capas de abstracción, incluyendo reducir el uso de SQL ó lidiar con conexiones y sintaxis programacional de bajo nivel.

Al implementar el acceso a las bases de datos usando un ORM se gana:

  • Independencia de la base de datos utilizada en gran medida, esto aumenta las características comerciales de un software ya que es posible cambiar de RBDM con un menor impacto sobre las áreas implicadas al uso e implementación del mismo.
  • Reducción del tamaño del código haciendo más simple el uso y entendimiento a nivel de desarrollo de sistemas que utilicen bases de datos.
  • La capa intermedia de acceso a loas RBDMs proporciona un método potente de interceptación de cualquier evento relacionado con las bases de datos, lo cual facilita la validación de la lógica de dominio, integridad de datos, definir niveles de seguridad, logging , auditoria de sistemas, etc.
  • El ORM permite administrar las asociaciones de las entidades del modelo incluso si el backend de almacenamiento es distinto.
  • ActiveRecord proporciona event handling en la capa intermedia, lo cual permite notificar cambios en el modelo de forma uniforme y consistente.

La mayor parte de la implementación de ActiveRecord se basa en el patrón de diseño del tipo Data Source Architectural del mismo nombre. En conjunto con el almacenamiento de meta-datos y la administración de transacciones se realizan los procesos de interacción con bases de datos a más alto nivel.
Cuando usar ActiveRecord
ActiveRecord es un patrón de diseño en los cual los datos y la estructura de los mismos se encuentran asociados en una misma clase. Los datos generalmente son persistentes, es decir que están almacenados en algún gestor relacional, archivo ó medio físico. La lógica de acceso a los datos es fácilmente implementable usando ActiveRecord y además la convierte en parte de la lógica de dominio de la aplicación.

ActiveRecord es una buena opción cuando la lógica de dominio no es muy compleja, es decir se implementa un modelo isomorfico, se usan derivaciones, colecciones ó herencia no-sencilla. Modelos entidad-relación con un diseño consistente ayudan a mejorar la implementación de patrón de diseño.
Entidades
Las entidades son objetos persistentes ligeros que normalmente representan una tabla en una base de datos relacional. El estado persistente de una entidad este representado usando atributos persistentes asociados a los campos de las tablas. Cada entidad se implementa en un modelo que es una clase en un archivo en el directorio models/.
Requerimientos de las Clases de Entidades
Las clases deben tener los siguientes requerimientos:

  • Cada archivo debe tener el nombre de la tabla y la extensión .php
  • Debe haber una entidad por archivo
  • La clase debe ser subclase (heredar) de la clase ActiveRecord
  • La clase no debe tener constructores
  • La clases que representen entidades finales no debe ser abstracta
Atributos de las Entidades
Debido a que PHP es un lenguaje con tipificación dinámica los atributos de las clases entidad no representan el tipo de dato real del campo en la base de datos. Con objetivos funcionales los getters de algunos campos crean objetos asociados al tipo de dato real, por ejemplo, los campos tipo fecha devuelven un objeto de la clase Date con el valor del campo.

Es posible agregar casting al valor de cada objeto usando la opción del script que crea modelos así:
php scripts/create_model.php –-table-name customers –-enable-casting yes

Los tipos de datos a los que les aplica casting son los siguientes:
Tabla: Relación de casting entre tipo de dato de la base de datos y tipo de dato de PHP
Tipo Dato BD
Tipo Dato PHP/Kumbia Enterprise
Varchar, Char, Text, BlobString
Integer, smallint, tinyintInteger
decimal, float, Moneydouble
dateDate
timeTime


Adicional a lo anterior es recomendable inicializar los atributos del modelo a un valor adecuado a su tipo de dato, de esta forma se evitan inconvientes con la ampliamente construcción del framework isset usada por el framework, ya que algunas veces considera que un campo no está definido pero en realidad tiene un valor nulo (null).
Atributos y Campos persistentes
La implementación de los atributos de las entidades en las clases se puede realizar estáticamente ó dinámicamente dependiendo de los requerimientos de la aplicación. En general una implementación estática propende por mejores practicas de desarrollo y software más seguro.

En la forma estática se definen los campos de la tabla como atributos protegidos de la clase. Esto permite encapsular el estado interno de cada propiedad y evitar que sea cambiada por equivocación ó a propósito en lógica de la aplicación. En estos casos también es necesario implementar métodos get/set para poder obtener/establecer el valor de los atributos. Los métodos de ActiveRecord isAttribute($property), writeAttribute($property, $value) y readAttribute($property) permiten dinámicamente conocer si existe un atributo, obtener y devolver su valor en forma pública. Por ejemplo isName(), getName() y setName() es una implementación estática para el atributo 'name' de una entidad Customer.

La forma general en la que los getters/setters deben ser implementados es la siguiente:
public function getProperty();
public function setProperty($valueOfProperty);

El método protegido setTrasient($attribute) permite establecer que campos de la entidad no deben ser persistidos. Las operaciones de inserción y actualización omiten los atributos marcados como Trasient.
Ejemplo: Establecer un campo trasient en el modelo
protected function initialize(){
     $this->setTransient("user_code");
}

Cuando los campos se mapean dinámicamente su visibilidad queda establecida como pública por defecto.
Llaves Primarias
Generalmente de entidades tienen una llave primaria que permite identificar un registro en forma única. Kumbia Enterprise Framework lee los meta-datos de la tabla mapeada directamente del gestor relacional e identifica la llave primaria con sus características: simples, compuestas y/o auto-numéricas.

El tipo de dato de llaves primarias debe ser un tipo de dato integral, en general se debe evitar el uso de campos con tipo de dato flotante, decimal ó Money, ya que pueden variar ligeramente y no ofrecen seguridad en que un registro sea univoco.

Los accesores (métodos get) de los atributos que pertenezcan a una llave primaria compuesta ó simple deben ser todos públicos.
Convenciones en llaves primarias
Kumbia Enterprise Framework soporta convención en llave primaria, para esto debe tener los siguientes requisitos:

  • El campo debe llamarse 'id'
  • El campo debe estar marcado como llave primaria en el gestor relacional
  • El campo debe ser entero preferiblemente unsigned (sin signo)
  • El campo debe ser una columna identidad ósea ser autonumérico (DB2, MySQL, Sybase, Microsoft SQL Server) ó estar asociado a una secuencia (Oracle, DB2, PostgreSQL, Interbase)

El uso de esta convención reduce la complejidad de la aplicación y reduce la codificación ya que muchos aspectos el framework puede asumir esto.
Fechas Auto-Asignables
ActiveRecord soporta fechas auto-asignables mediante convención mediante lo cual se puede realizar versionamiento concurrente óptimista ó simplemente llevar un registro de fechas de creación y modificación de registros.

Las convenciones para fechas auto-asignables son las siguientes:

  • Para que un campo tome automáticamente la fecha del sistema en la operación de insertar este debe tener el sufijo "_at" y permitir valores nulos.
  • Para que un campo tome automáticamente la fecha del sistema en la operación de actualizar este debe tener el sufijo "_in" y permitir valores nulos.
Multiplicidad en Relaciones de Entidades
Existen cuatro tipos de multiplicidad en relaciones: una-a-una, una-a-muchos, muchos-a-una y muchos-a-muchos. La multiplicidad puede ser unidireccional ó bidireccional y cada una puede ser simple ó mediante combinación de tablas. El tipo de relaciones establece como se lleva a persistencia una instancia de una entidad. Normalmente el gestor relacional administra constraints de llave foránea correspondientes a estas relaciones, la definición de estos ayuda a que la integridad de datos sea confiable y las relaciones definidas obtengan los resultados esperados. A través de la implementación de las relaciones es posible acceder a los registros relacionados a cada registro de forma uniforme ahorrando código.
Convenciones en Relaciones
Los nombres de campos de las entidades con ciertas convenciones permiten que se establezcan automáticamente los campos referencias y se automaticen tareas de codificación mediante una estructura nemotécnica:

  • Para especificar un campo que es llave foránea a otra relación se utiliza el nombre de campo "nombre_tabla_id"
  • La tabla referenciada debe tener un campo identidad que sea llave primaria con nombre "id".
  • Si es posible, los campos relacionados deben tener el mismo tipo de dato.
Relaciones Unidireccionales
Las relaciones unidireccionales son aquellas que se generan de una relación a otra pero no viceversa. Mediante los métodos belongsTo, hasMany ó hasOne se establece que uno ó mas campos hacen referencia a otros equivalentes en otra entidad.
Relaciones Bidireccionales
Las relaciones bidireccionales establecen asociaciones en las que cada una de ellas tiene una viceversa complementaria.
Muchos a uno
La asociación unidireccional muchos-a-uno es la más común de todas. Para establecer una relación de este tipo se utiliza el método protegido de ActiveRecord belongsTo:
Ejemplo: Multiplicidad muchos a uno relacionada con un solo campo

Para 2 Tablas:
CREATE TABLE `country` (
  `code` int(11) NOT NULL,
  `name` varchar(20) default NULL,
  PRIMARY KEY  (`code`)
);

CREATE TABLE `city` (
  `code` int(11) NOT NULL,
  `country_code` int(11) default NULL,
  `name` varchar(80) default NULL,
  PRIMARY KEY  (`code`)
);

El modelo de ciudades con una relación muchos-a-uno se implementa así:
<?php

class City extends ActiveRecord { 

     protected function initialize(){
          $this->belongsTo("country_code", "country", "code");
     }

}

El primer parámetro de belongsTo indica el campo de la entidad que hace la asociación, el segundo indica el nombre de la entidad referenciada y el tercero el nombre del campo en la entidad referenciada. La asociación una vez definida se puede utilizar así:
Ejemplo: Utilizar una asociación muchos a uno
$city = EntityManager::getEntityInstance("City");
$city->findByName("Bogotá");
$country = $city->getCountry()
echo $country->getNombre(); // => Colombia

Un getter con el nombre de la relación 'Country' permite obtener el registro asociado al país en la ciudad consultada.

Relacionada con varios campos:
El siguiente ejemplo implementa una relación muchos a uno con 2 campos referencia, en este caso los nombres de los campos son iguales en ambas tablas si fuesen diferentes se podrían definir en el tercer parámetro de belongsTo.
Ejemplo: Utilizar una relación compuesta muchos a uno
CREATE TABLE `customer` (
  `identification_type` char(3) NOT NULL default '',
  `number` varchar(40) NOT NULL default '',
  `nombre` varchar(120) default NULL,
  `status` char(1) default NULL,
  PRIMARY KEY  (`identification_type`,`number`)
);

CREATE TABLE `flights` (
  `flight_number` varchar(12) NOT NULL default '',
  `identification_type` char(3) default NULL,
  `number` varchar(40) default NULL,
  `flight_date` date default NULL,
  `initial_hour` time default NULL,
  `final_hour` time default NULL,
  PRIMARY KEY  (`flight_number`)
);

El modelo Flights se implementa así:
Ejemplo: Definir una relación belongsTo en un modelo entidad-relación sin convenciones
<?php

class Flights extends ActiveRecord { 

     protected function initialize(){
          $this->belongsTo(array("identification_type", "number"), "customer");
     }

}

La relación puede ser utilizada así:
$flight = $this->Flights->findByDate("2008-11-01");
$customer = $flight->getCustomer();

Un getter con el nombre de la relación permite obtener el registro asociado al cliente en el vuelo consultado.
Uno a Muchos
La asociación unidireccional uno-a-muchos indica que para cada registro de una entidad existen uno ó más registros asociado en la entidad referenciada. Para establecer una relación de este tipo se utiliza el método protegido de ActiveRecord hasMany.
Uno a Uno
La asociación unidireccional uno-a-uno es la más inusual de todas. Indica que para cada registro de una entidad existe solo uno asociado en la entidad referenciada. Para establecer una relación de este tipo se utiliza el método protegido de ActiveRecord hasOne.
Muchos a Muchos
FALTA
API de ActiveRecord
A continuación se presenta una referencia de los principales métodos de la clase ActiveRecord.
Origen de Datos
public void setSource($source)
Permite establecer la entidad del gestor de donde se mapearan los atributos del modelo.

public string getSource()
Devuelve el nombre de la entidad usada internamente para mapear los atributos del modelo.

public void setSchema(string $schema)
Establece el nombre del $schema en donde se encuentra la tabla donde se mapearan los datos.

public void setConnection(Db $connection)
Permite establecer un objeto Db dinámicamente sobrescribiendo la conexión actual del modelo.

public DbBase getConnection()
Obtiene el objeto Db utilizado para realizar las operaciones a bajo nivel con el gestor relacional.
Volcado de Meta-Datos
public boolean isDumped()
Indica si ya se han obtenido los meta-datos del gestor relacional en el objeto.

public void dumpModel()
Forza al modelo a obtener los meta-datos del gestor relacional.

protected boolean dump()
Obtiene los meta-datos del gestor relacional. Si ya se han obtenido no se vuelven a consultar

public void resetMetaData()
Elimina los meta-datos obtenidos del gestor-relacional cargandolos nuevamente.
Debug y Seguimiento
public void setDebug(boolean $debug)
Establece si el modelo esta en modo debug. Todas las operaciones internas de la conexión activa se visualizan como Flash::notices en la salida al navegador.

public void setLogger(mixed $logger)
Establece el modelo en modo debug. Todas las operaciones internas de la conexión asociada al modelo son almacenadas en un archivo con nombre $logger. Si el primer parámetro es true se utiliza la convención dbYYYYMMDD.txt
//Grabar todas las operaciones SQL internas a un archivo en logs/customersDebug.txt
$this->Employees->setLogger("customersDebug.txt");

También es posible pasar una instancia de logger utilizando cualquier adaptador disponible utilizando como parámetro el objeto creado:
Ejemplo: Loguear las operaciones internas de un modelo a un logger que usa compresión
$logger = new Logger('Compressed', 'log.employees.txt.gz');
$logger->setPath("/usr/local/log/");
$this->Employees->setLogger($logger);

El desarrollador también puede pasar una instancia de un objeto que implemente un método log como parámetro e implementar operaciones personalizadas mediante él.
Ejemplo: Definir un Logger personalizado para hacer seguimiento a las operaciones internas de un modelo
$myLog = new MyUserLog();
$this->Employees->setLogger($myLog);

public string inspect()
Obtiene una cadena de inspección con los valores internos de cada atributo de la entidad.
Transacciones
public void setTransaction(ActiveRecordTransaction $transaction)
Establece la transacción utilizada para efectuar las operaciones en el modelo. La transacción debe haberse inicializado antes de asignarla al objeto ó generará una excepción ActiveRecordException.
Consultar registros
public ActiveRecordResulset findAllBySql(string $sqlQuery)
Permite realizar una consulta en el modelo usando lenguaje de consulta SQL. Los valores obtenidos son devueltos como instancias de la clase.

public ActiveRecordResulset findBySql(string $sqlQuery)
Permite realizar una consulta en el modelo que devuelve un solo registro usando lenguaje de consulta SQL. Los valores obtenidos son devueltos como instancias de la clase.
Ejemplo: Realizar una consulta en un modelo usando SQL
//Obtener los empleados con una condición en un subselect
$empoyees = $this->Employees->findBySql("SELECT employees.* FROM employees WHERE id NOT IN (SELECT employees_id FROM historical_data WHERE period = '2005-02'");

public DbResource sql(string $sqlQuery)
Permite realizar una consulta usando lenguaje SQL. El cursor de bajo nivel es devuelto directamente.

public ActiveRecord findFirst(mixed $params)
Permite realizar una búsqueda de registros que devuelve un solo registro ó el primero que coincida con las condiciones indicadas. Los parámetros de consulta se establecen usando parámetros por nombre.
Ejemplo: Utilizar findFirst para obtener el primer registro de un modelo ó el primero que cumpla determinadas condiciones
//Obtener el primer empleado que se almacenó en la entidad
$employee = $this->Employees->findFirst();

//Obtener el primer usuario cuyo login sea 'j.smith'
$user = $this->Users->findFirst("login = 'j.smith'");

//Obtener el ultimo producto cuyo precio sea menor a 100 y este activo
$user = $this->Products->findFirst("price < 100 AND status = 'Active'", "order: price DESC");

public ActiveRecordResulset find(mixed $params)
Permite realizar una búsqueda de registros que devuelve todos los que coincidan con las condiciones indicadas.
Ejemplo: Utilizar find para consultar los registros del modelo
//Obtener todos los registros de una entidad
$employees = $this->Employees->find();

//Obtener todos los registros que cumplan con una condición
$employees = $this->Employees->find("status = 'Active'");

//Obtener todos los registros que cumplan una condición y aplicándoles un ordenamiento
$employees = $this->Employees->find("status = 'Active'", "order: name desc");

//Obtener todos los registros y aplicarles un ordenamiento
$employees = $this->Employees->find("order: name");

//Obtener los 20 primeros registros que cumplan con unas condiciones
$employees = $this->Employees->find("agreement_date >= '2008-12-31' AND status='Active'", "limit: 20");

public ActiveRecordResulset findForUpdate($params)
Permite realizar una búsqueda de registros que devuelve todos los que coincidan con las condiciones indicadas. Los registros encontrados son bloqueados en modo no compartido por lo que otras sesiones de aplicación que traten de leer/escribir estos registros resultaran en una espera.
Ejemplo: Bloquear un conjunto de registros en modo no compartido en una transacción
<?php

class OrdersController extends ApplicationController {

     public function increaseQuantityAction(){
          try {
               $transaction = new ActiveRecordTransaction(true);     
               $this->Products->setTransaction($transaction); 
               foreach($this->Products->findForUpdate("quantity<100") as  $product){ 
                    $product->setQuantity($product->getMinStock()*2);
                    if($product->save()==false){
                         $transaction->rollback();
                    }
               }
          }
          catch(TransactionFailed $e) {
               Flash::error($e->getMessage());
          }
     }
}

public ActiveRecordResulset findWithSharedLock($params)
Permite realizar una búsqueda de registros que devuelve todos los que coincidan con las condiciones indicadas. Los registros encontrados son bloqueados en modo no compartido por lo que otras sesiones de aplicación que traten de escribir en estos registros resultaran en una espera por parte del gestor relacional.

public string convertParamsToSql(mixed $params)
Crea una consulta de SQL mediante los parámetros por nombre indicados.

public array distinct(mixed $params)
Realiza una sentencia DISTINCT sobre una columna del modelo devolviendo un listado de los valores diferentes encontrados.

public boolean exists(string $wherePk)

public ActiveRecordResultset findAllBy(string $field, mixed $value)

public static array singleSelect(string $sql)
Realiza una consulta SQL sobre el gestor relacional en la tabla 'dual' en el caso de Oracle ó solo hace la instrucción SELECT sobre ninguna entidad en otros motores.
Contar registros
public integer count(mixed $params)
Devuelve un conteo de registros a partir de las condiciones indicadas. Los parámetros pueden ser establecidos usando parámetros por nombre ó un array cuyos indices indiquen el tipo de parámtro a establecer. El parámetro especial group modifica el resultado obtenido agrupandolo mediante otros atributos de la entidad.
Ejemplo: Realizar conteos sobre atributos del modelo
//Cuantos productos hay?
$this->Products->count();

//Cuantos productos tienen categoria 1
$this->Products->count("categories_id = 1");

//Contar productos por categoria
foreach($this->Products->count("group: categories_id") as $result){
     echo $result->categories_id." ".$result->rowcount;
}

// Contar productos por categoria, mostrar aquellas categorias que tienen mas de // 10 productos
$resultset =$this->Products->count("group: categories_id", "having: rowcount>10");
foreach($resultset as $result){
     echo $result->categories_id." ".$result->rowcount;
}

public mixed countBySql(string $sqlQuery)
Realiza un conteo en el modelo mediante una sentencia SQL.
Promediar registros
public double average($params)
Devuelve un promedio de los valores de una columna numérica a partir de las condiciones indicadas. El parámetro especial group modifica el resultado obtenido agrupándolo mediante otros atributos de la entidad.
Ejemplo: Realizar promedios sobre atributos del modelo
//En promedio, cual es el precio de un producto
$this->Products->average("price");

//En promedio, cual es el precio de un producto activo
$this->Products->average("price", "conditions: status='Active'");

//Promedio de precios por categoria
$averages = $this->Products->average("price", "group: categories_id");
foreach($averages as $result){
     echo $result->categories_id." ".$result->average."\n";
}
Realizar sumatorias
public double sum($params)
Este método permite realizar sumatorias de los atributos de la entidad. El parámetro especial group modifica el resultado obtenido agrupandolo mediante otros atributos de la entidad.
Ejemplo: Realizar sumatorias sobre atributos del modelo
//Cuanto suman los impuestos en las ordenes de compra?
$this->Orders->sum("taxes");

//Cuanto suman los impuestos del año 2007?
$this->Orders->sum("taxes", "conditions: year = '2007'");

//Cuanto suman las cantidades en las ordenes de compra por cada cliente
$summatories = $this->Orders->sum("quantity", "group: customers_id");
foreach($summatories as $result){
     echo $result->categories_id." ".$result->summatory."\n";
}
//Cuanto suman las cantidades en las ordenes de compra por cada cliente, cuya //cantidad sea superior a 100 unidades
$summatories = $this->Orders->sum("quantity", "group: customers_id", "having: summatory>100");
foreach($summatories as $result){
     echo $result->categories_id." ".$result->summatory."\n";
}
Obtener el valor máximo de un atributo
public mixed maximum($params)
Devuelve un valor máximo de una columna numérica a partir de las condiciones indicadas. El parámetro especial group modifica el resultado obtenido agrupandolo mediante otros atributos de la entidad.
Ejemplo: Obtener el valor máximo de un atributo de un modelo
//Quien es el empleado más antiguo?
$this->Employees->maximum("contract_date", "conditions: status='Active'");
Obtener el valor mínimo de un atributo
public mixed minimum($params)
Devuelve un valor mínimo de una columna numérica a partir de las condiciones indicadas. El parámetro especial group modifica el resultado obtenido agrupandolo mediante otros atributos de la entidad.
Ejemplo: Obtener el valor mínimo de un atributo de un modelo
//Quien es el empleado más nuevo?
$this->Employees->minimum("contract_date", "conditions: status='Active'");
Asignar valores a instancias
public ActiveRecord dumpResult(array $result)
Toma un vector cuyos indices coinciden con los nombres de los atributos del modelo y los asigna devolviendo un objeto copia de este con los valores asignados. Si algún indice no corresponde a un atributo del modelo se genera una excepción.

public void dumpResultSelf(array $result)
Toma un vector cuyos indices coinciden con los nombres de los atributos del modelo y los asigna al mismo objeto. Si algún indice no corresponde a un atributo del modelo se genera una excepción.
Validación
public array getMessages()
Obtiene los mensajes de validación generados en un proceso de inserción ó actualización.

public void appendMessage(ActiveRecordMessage $message)
Agrega un mensaje al buffer de mensajes de validación del modelo.
Información de atributos
public array getAttributes()
Obtiene un vector con los nombres de los atributos de la entidad.

public array getAttributesNames()
Obtiene un vector con los nombres de los atributos de la entidad. Es un alias de getAttributes().

public boolean hasField($string field)
Permite consultar si una entidad tiene un determinado campo $field.

public array getPrimaryKeyAttributes()
Obtiene un vector con los nombres de los atributos de la entidad que son llave primaria.

public array getNonPrimaryKeyAttributes()
Obtiene un vector con los nombres de los atributos de la entidad que no son llave primaria.

public array getNotNullAttributes()
Obtiene un vector con los nombres de los atributos de la entidad que no aceptan valores nulos.

public array getDatesAtAttributes()
Obtiene un vector con los nombres de los atributos de la entidad que auto-asignan la fecha actual cuando se realiza una operación de inserción.

public array getDatesInAttributes()
Obtiene un vector con los nombres de los atributos de la entidad que auto-asignan la fecha actual cuando se realiza una operación de modificación.

public boolean isANumericType(string $field)
Permite consultar si un atributo en la tabla tiene un tipo de dato númerico (int, integer, float, number, bigint, Money, etc).
Creación y actualización de registros
public boolean create(array $values=array())
Este método permite la creación de un registro apartir de los valores asignados previamente a los atributos del modelo ó asignando valores a estos a través de un array asociativo pasado como parámetro. Debe usarse este método cuando el desarrollador quiera asegurarse que se realice una actualización en vez de usar save().

public boolean update(array $values=array())
Este método permite la actualización de un registro apartir de los valores asignados previamente a los atributos del modelo ó asignando valores a estos a través de un array asociativo pasado como parámetro. Debe usarse este método cuando el desarrollador quiera asegurarse que se realice una actualización en vez de usar save().

public boolean save()
Este método permite crear/actualizar registros de acuerdo a si estos ya existen en la entidad asociada a un modelo. El método save es llamado también internamente por los métodos create y update de ActiveRecord. Para que este método funcione como se espera es necesario que se haya definido una llave primaria correctamente en la entidad para poder determinar si un registro debe ser actualizado ó creado.

El método save() ejecuta los validadores asociados, llaves primarias virtuales y eventos que se hayan definido en el modelo. El generador de identificadores asociado también es ejecutado y al terminar el proceso el objeto se refresca con los valores finales que quedaron en la base de datos.
Ejemplo: Crear un registro usando el método save() de ActiveRecord
<?php

$product = new Products();
$product->setName('Potattos');
$product->setType('Food');
if($product->save()==false){
     foreach($product->getMessages() as $message){
          Flash::error($message->getMessage());
     }
}

El ejecutarse save(), este devuelve un valor booleano indicando el éxito de la operación. En el ejemplo anterior se obtienen y muestran los mensajes de validación en pantalla cuando save() devuelve false.
Eliminación de registros
public boolean delete($params)
Elimina el registro activo asociado al objeto ActiveRecord.
Operaciones en Batch
public boolean updateAll(array $values, string $conditions="")
Realiza una actualización en batch de todos los campos de una entidad asegurando alto rendimiento. El parámetro $values permite indicar, mediante un array asociativo, los valores a actualizar, en donde los indices son los nombres de los campos. El parámetro $conditions permite definir las condiciones que se deben cumplir para que el registro sea actualizado. Si no se establece un valor para $conditions se actualizarán todos los registros de la entidad.

public boolean deleteAll($conditions="")
Realiza una eliminación de registros en batch en una entidad asegurando alto rendimiento. El parámetro $conditions permite definir las condiciones que se deben cumplir para que el registro sea actualizado. Si no se establece un valor para $conditions se borrarán todos los registros de la entidad.
Lectura/escritura de Atributos
public mixed readAttribute(string $attribute)
Obtiene el valor de un atributo del modelo apartir de su nombre. Este método lee el valor independiente de la visibilidad de la propiedad en la clase.

public mixed writeAttribute(string $attribute, mixed $value)
Asigna un valor a un atributo del modelo apartir de su nombre. Este método escribe el valor independiente de la visibilidad de la propiedad en la clase.
Validación
protected void validate(string $validatorClass, array $options)
Agrega un validador a un modelo. Consulte el capítulo de validadores de integridad para aprender más sobre este método.

public boolean validationHasFailed()
Este método puede ser usado dentro de un evento de validación para indicar si el proceso de completo de validación ha fallado. Consulte el capítulo de validadores de integridad para aprender más sobre este método.
Multiplicidad de relaciones
protected void hasOne(mixed $fields, string $referenceTable, mixed $referencedFields)
Crea una relación 1 a 1 con otra entidad presente en el administrador de entidades. El primer parámetro especifica el atributo ó atributos en la entidad local y $referencedFields el atributo ó atributos en la tabla referenciada. El parámetro $referenceTable indica la tabla referenciada.
Ejemplo: Establecer relaciones hasOne (1 a 1) de acuerdo al modelo entidad-relacion
//Relación de un campo a otro por convención
$this->hasOne("tabla_id");

//Relación de un campo a otro 
$this->hasOne("campo", "tabla_referenciada", "campo_referenciado");

//Relación de varios campos a varios campos
$this->hasOne(
     array("campo1", "campo2"), 
     "tabla_referenciada", 
     array("campo_referenciado1", "campo_referenciado2")
);

protected void belongsTo(mixed $fields, string $referenceTable, mixed $referencedFields, string $relationName='' )
Crea una relación 1 a 1 inversa con otra entidad presente en el administrador de entidades. El primer parámetro especifica el atributo ó atributos en la entidad local y $referencedFields el atributo ó atributos en la tabla referenciada. El parámetro $referenceTable indica la tabla referenciada.
Ejemplo: Establecer relaciones belongsTo (muchos a 1) de acuerdo al modelo entidad-relacion
//Relación de un campo a otro por convención
$this->belongsTo("tabla_id");

//Relación de un campo a otro 
$this->belongsTo("campo", "tabla_referenciada", "campo_referenciado");

//Relación de varios campos a varios campos
$this->belongsTo(
     array("campo1", "campo2"), 
     "tabla_referenciada", 
     array("campo_referenciado1", "campo_referenciado2")
);

protected void hasMany(mixed $fields, string $referenceTable, mixed $referencedFields)
Crea una relación 1 a n con otra entidad presente en el administrador de entidades. El primer parámetro especifica el atributo ó atributos en la entidad local y $referencedFields el atributo ó atributos en la tabla referenciada. El parámetro $referenceTable indica la tabla referenciada.
Ejemplo: Establecer relaciones hasMany (1 a muchos) de acuerdo al modelo entidad-relacion
//Relación de un campo a otro por convención
$this->hasMany("tabla_id");

//Relación de un campo a otro 
$this->hasMany("campo", "tabla_referenciada", "campo_referenciado");

//Relación de varios campos a varios campos
$this->hasMany(
     array("campo1", "campo2), 
     "tabla_referenciada", 
     array("campo_referenciado1", "campo_referenciado2")
);

protected void hasAndBelongsToMany(mixed $fields, string $referenceTable, string $gateTable, mixed $referencedFields)
Crea una relación n a m inversa con otra entidad presente en el administrador de entidades. El primer parámetro especifica el atributo ó atributos en la entidad local y $referencedFields el atributo ó atributos en la tabla referenciada. El parámetro $referenceTable indica la tabla referenciada y $gateTable la tabla puente para acceder a la tabla referenciada.
Herencia
public void parentOf(string $parent)
Establece una relación de herencia que utiliza una estrategía de tabla por subclase mediante un discriminador. El parámetro $parent indica la entidad padre.
Excepciones
protected void exceptions($e)
Al reescribir este método es posible tratar las excepciones generadas dentro de un modelo antes que sean lanzadas a otras partes de la aplicación.
Identifiers
Las clases mapeadas con ActiveRecord que pretendan efectuar operaciones de manipulación de datos deben declarar llaves primarias. Por defecto se utilizan estrategias para obtener el valor de estas al hacer una inserción.

Los tipos de identificadores soportados son:
Tabla: Tipos de generadores soportados
TipoDescripción
IncrementGenera identificadores de tipo integer, que son únicos solo cuando otros procesos no generar operaciones de inserción concurrentes. Esta opción no debería ser usada en clusteres.
IdentityNo realiza ninguna operación en especial ya que aprovecha una columna identidad presente en la entidad. Este tipo de columnas son soportadas por MySQL, IBM DB2, Microsoft SQL Server, IBM Informix y SQLite.
SequenceObtiene el valor de un objeto del gestor relacional llamado secuencias. Las secuencias son soportadas por Oracle, PostgreSQL e IBM DB2.
HiloUtiliza un algoritmo tipo hi/lo para eficientemente generar los valores únicos de tipo integer.
UUIDGenera identificadores unicos de 128bits tipo string basados en el algoritmo UUID (Universal Unique Identifier.
Assigned No aplica ninguna estrategía y utiliza el valor asignado directamente al objeto instancia del modelo.
UniqidPermite obtener identificadores únicos de 128bits usando la función de PHP llamada uniqid.

Establecer el Generador
Para establecer este tipo de identificadores se debe usar el método protegido de ActiveRecord llamado setIdGenerator. El primer parámetro indica que tipo de generador se usará y el segundo permite establecer las opciones del generador.
Algoritmo Hi/Lo
El generador Hi/Lo permite obtener el valor que deba se utilizado como llave primaria de un campo en otra entidad en forma eficiente. En una transacción al obtener el valor del identicador se efectúa (si se soporta) un bloqueo por fila en el registro para asegurarse que ningún otro proceso obtenga el mismo valor.
Ejemplo: Establecer un generador eficiente Hi/Lo en un modelo
<?php

class Invoices extends ActiveRecord {

     public function initialize(){
          $this->setIdGenerator("Hilo", "code", array(
               "table" => "invoces_data",
               "column" => "consecutive",
               "max_lo" => 100
          ));
     }

}
La opción max_lo indica el valor mínimo que debe tomar el consecutivo.
Tabla: Parámetros del generador Hi/Lo
Tipo
Descripción
table Table donde se encuentra el consecutivo
columnaColumna que lleva el consecutivo
sourceEs opcional. Permite establecer que la tabla se encuentra en otro schema.
max_loEs opcional. Permite controlar el mínimo valor que debe almacenar la columna.
numberEs opcional. Indica cuantos consecutivos se deben generar para aumentar la eficiencia.


Uno de los objetivos del algoritmo Hi/Lo es aumentar la eficiencia mediante la reducción de la lectura/escritura de la tabla consecutivo. Esto solo es posible cuando la operación se encuentra en medio de una transacción administrada por el TransactionManager.
Algoritmo UUID
El algoritmo UUID genera identificadores únicos que pueden ser usados como llave primaria mediante el algoritmo del mismo nombre. El sistemas UNIX utiliza /dev/urandom para generar el identificador.

Un UUID tiene la siguiente forma:
4cfce7c2-6089-102c-91cf-d8dbbe268425

Se establece un generador UUID de esta forma:

Ejemplo: Establecer un generador de identificadores que use el algorimo UUID
<?php

class Media extends ActiveRecord {

     public function initialize(){
          $this->setIdGenerator("Uuid", "id");
     }

}
Generador UniqId
El generador Uniqid genera identificadores únicos apartir de la función Uniqid de PHP. El valor generado es un string de 32 caracteres (un número hexadecimal de 128 bits) que es muy difícil de repetir.
Ejemplo: Establecer un generador de identificadores que use el algoritmo uniqid
<?php

class Media extends ActiveRecord {

     public function initialize(){
          $this->setIdGenerator("Uniqid", "id");
     }

}
Generador Native
El generador "Native" debe usarse en aquellos casos en los que el valor de la columna identidad son asignados por un trigger en la tabla y que toma el valor de una secuencia. En estos casos ActiveRecord no asignará ningún valor en la sentencia INSERT creada respetando el consecutivo de la secuencia aunque si consultará su valor para actualizar el estado del objeto.

El generador "Native" debe establecerse de la siguiente forma:
Ejemplo: Establecer un generador nativo en un modelo
<?php

class Orders extends ActiveRecord {

     public function sequenceName(){
          return "ORDERS_SEQ";
     }

     public function initialize(){
          $this->setIdGenerator("Native", "id");
     }

}
Columnas Identidad y Secuencias
Para bases de datos que soporten columnas identidad el generador asigna el valor adecuado para que se genere valor autonumérico en la columna adecuada. En estos casos el generador Identity se puede usar con motores como IBM DB2, MySQL y Microsoft SQL Server.

Para los motores que soportan secuencias como Oracle, PostgreSQL e IBM DB2 se debe utilizar el generador "Sequence" ó "Native". El generador "Sequence" se debe establecer cuando no existan triggers que asignen el valor de la columna en la tabla ya que esto hará que se altere el consecutivo innecesariamente.
Ejemplo: Establecer un generador de secuencias para un modelo con un gestor relacional que lo soporte
<?php

class Orders extends ActiveRecord {

     public function initialize(){
          $this->setIdGenerator("Sequence", "id", array(
               "name" => "ORDERS_ID"
          ));
     }

}
El nombre de la secuencia también puede ser establecido mediante el método público sequenceName tanto para los modelos con generadores como los que tienen identificador por convención:
Ejemplo: Definir el nombre la secuencia del generador de valores identidad
<?php

class Orders extends ActiveRecord {

     public function sequenceName(){
          return "ORDERS_ID_SEQ";
     }


     public function initialize(){
          $this->setIdGenerator("Sequence", "id");
     }

}
Convenciones en Identificadores
ActiveRecord puede automáticamente establecer el tipo de generador a Identity si en los meta-datos de la entidad encuentra que existe una columna identidad. Igualmente si el campo se llama explícitamente 'id' igualmente se entenderá la convención que se trata de un campo identidad generado por el gestor relacional ó mediante el generador Increment.

Requisitos para utilizar la convención en identificadores:

  • El campo debe llamarse "id"
  • El campo debe tener un tipo de dato enterno (bigint, int, integer, number)
  • El campo debe ser la llave primaria de la tabla
  • El campo debe ser no nulo
Los Meta-datos en ActiveRecordMetadata
Gran parte de la ciencia en la implementación de ActiveRecord esta relacionada con la administración de los metadatos de las tablas mapeadas. El almacenamiento de sus características es punto fundamental para la utilización de los métodos que consultan, borran, modifican, almacenan, etc. El subcomponente ActiveRecordMetadata implementa el patrón Metadata Mapping el cual permite crear un data map por schema sobre la información de las tablas y así reducir el consumo de memoria por objeto ActiveRecord y consolidar una base de datos in-memory de las características de cada entidad utilizada en la aplicación.

Normalmente los meta-datos son escritos manualmente por el desarrollador pero Kumbia Enterprise los toma directamente del gestor relacional, con esto se gana eficiencia en el desarrollo generando un schema auto-actualizable que refleja cualquier cambio en la estructura del modelo de datos.
Tipos de Meta-Datos Almacenados
En ActiveRecordMetaData se almacenan varios tipos de información sobre entidades que pretenden acelerar las operaciones de manipulación y consulta de datos en ActiveRecord. Estos tipos de datos son:
Tabla: Meta-datos almacenados para una entidad en el ActiveRecordMeta-Data
Tipo Meta-Dato
Descripción
Campos de la tablaNombres de los campos de las tablas
Llaves PrimariasCampos que hacen parte de la llave primaria de las tablas.
Campos no llave primariaConjunto de campos que no pertenece a la llave primaria de las tablas.
Campos No NulosCampos que no permiten valores nulos
Tipos de DatosTipo de datos de cada campo en una tabla.
Campos Fecha Auto-AsignablesCampos que por convención asignan automáticamente la fecha del sistema al actualizar ó modificar.

Meta-Datos en etapas de Desarrollo
Cuando las aplicaciones se encuentran en etapa de desarrollo es posible que el los desarrolladores encuentren que los cambios en la estructuras de la base de datos no se ven reflejados en las aplicaciones.

Esto sucede porque los meta-datos de las tablas son cacheados temporalmente en sesión para evitar que la base de datos sea accedida continuamente disminuyendo el rendimiento.

Para hacer que los meta-datos sean cargados nuevamente es necesario cerrar y abrir el navegador ó el cliente web y así se verán los cambios del modelo de datos reflejados.
API de ActiveRecordMetaData
public void static existsMetaData(string $table, string $schema)
Permite saber si ya se ha definido los meta-datos para una tabla y esquema en especial.

public void static createMetaData(string $table, string $schema)
Crea un registro para meta-datos en el meta-data store.

public void static setAttributes(string $tableName, string $schemaName, array $attributes)
Establece los nombres de los campos de una determinada tabla.

public array static getAttributes(string $tableName, string $schemaName)
Obtiene los nombres de los campos de una determinada tabla.

public array static setPrimaryKeys(string $tableName, string $schemaName, array $primaryKey)
Establece los campos que son llave primaria a una determinada tabla.

public array static getPrimaryKeys(string $tableName, string $schemaName="")
Obtiene los campos que son llava primaria en una determinada tabla

public void static setNonPrimaryKeys(string $tableName, string $schemaName, array $nonPrimaryKey)
Establece los campos que no son llave primaria a una determinada tabla.

public array static getNonPrimaryKeys($tableName, $schemaName)
Obtiene los campos que no son llava primaria en una determinada tabla

public void static setNotNull(string $tableName, string $schemaName, array $notNull)
Establece los campos que no pueden tener valores nulos.

public array static getNotNull($tableName, $schemaName)
Obtiene los valores que no son nulos en una tabla.

public void static setDataType(string $tableName, string $schemaName, array $dataType)
Establece los tipos de datos de una tabla.

public array static getDataTypes(string $tableName, string $schemaName)
Obtiene los tipos de datos de una determinada tabla.

public void static setDatesAt(string $tableName, string $schemaName, array $datesAt)
Establece los atributos de la tabla a los cuales se les asigna fecha automática al insertar.

public array static getDatesAt(string $tableName, string $schemaName)
Obtiene los atributos de la tabla a los cuales se les asigna fecha automática al insertar.

public void static setDatesIn(string $tableName, string $schemaName, array $datesIn)
Establece los atributos de la tabla a los cuales se les asigna fecha automática al actualizar.

public array static getDatesIn(string $tableName, string $schemaName)
Obtiene los atributos de la tabla a los cuales se les asigna fecha automática al actualizar.

public void static dumpMetaData(string $table, string $schema, array $metaData)
Almacena los meta-datos para una determinada tabla en el meta-data store.
Cursores y Resulsets de Consultas
Los resultados devueltos por los métodos de consulta de ActiveRecord son objetos instancias de la clase ActiveRecordResulset que encapsulan la manipulación y obtención de los registros individuales en el cursor enviado por el RBDM.

La clase implementa las interfaces Iterator, ArrayAccess, SeekableIterator y Countable con lo cuál el objeto se puede recorrer usando una sentencia como foreach, acceder a indices individuales mediante el operador de acceso de vectores y contar el total de registros usando funciones como count ó sizeof.

La implementación de este objeto logra una administración de memoria más eficiente ya que solo el registro activo en el cursor consume memoria en el script actual y se va liberando a medida que se recorren los registros. Las implementaciones de ORM que devuelven los registros en un array consumen mayor memoria y si la consulta devuelve una cantidad considerable de registros es probable que el interprete PHP aborte debido al consumo excesivo de memoria por parte de la aplicación.
Utilizar el cursor como tipo Forward-Only
Los objetos de resultado de consulta pueden ser recorridos usando sentencias del lenguaje como foreach y while utilizandolos como cursores tipo forward-only. Al terminar de recorrer los registros los cursores son auto-resetados permitiendo volver a recorrerlos.
Ejemplo: Recorrer un cursor tipo forward-only
//Recorrerlo con Foreach
foreach($this->Products->find() as $product){
     echo $product->getId(), "\n";
}

//Recorrerlo con While
$resultSet = $this->Products->find();
while($resultSet->valid()){
     $product = $resultSet->current();
     echo $product->getId(), "\n";
}
Utilizar el cursor como Scrollable
Los resultsets también pueden ser recorridos en modo scrollable de esta forma se puede acceder a un registro en particular usando un indice para establecer su posición.
$resultSet = $this->Products->find();

//Obtener el primer registro
echo $resultSet->getFirst()->id, "\n";

//Obtener el segundo registro
echo $resultSet->offsetGet(1)->id, "\n";

//Obtener el último
echo $resultSet->getLast()->id, "\n";
API de ActiveRecordResulset
public integer key()
Obtiene el número del registro que está actualmente activo en el cursor.

public boolean offsetExists(integer $index)
Permite consultar si existe un registro en una determinada posición.

public void rewind()
Devuelve el cursor interno del resulset al primer registro.

public boolean valid()
Indica la posición actual del cursor interno es valida, es decir que aun quedan más registros para recorrer.

public void next()
Mueve el cursor interno al siguiente registro del Resultset.

public void current()
Devuelve el objeto ActiveRecord activo en el cursor.

public void seek(int $position)
Mueve el cursor interno del resultset a la posición indicada por $position, esta debe ser un número entero mayor a 0.

public integer count()
Implementa el método que exige la interface Countable el cual permite saber cuantos registros ha devuelto el resultset.

public ActiveRecord getFirst()
Obtiene el primer registro del cursor. Implicitamente rebobina el puntero al primer registro.

public ActiveRecord getLast()
Obtiene el último registro del cursor. Implicitamente mueve el puntero interno al último registro.

public ActiveRecord offsetGet($index)
Obtiene el registro ubicado en la posición $index del cursor. Las posiciones empiezan en 0.

public boolean offsetExists($index)
Permite consultar si existe un registro una determinada posición del cursor.

public string getSQLQuery()
Devuelve el SQL que produjo la consulta.

public ActiveRecord getEntity()
Devuelve la entidad que produjo la consulta.
Mensajes de ActiveRecord
ActiveRecord tiene un subsistema de mensajes que permite flexibilizar la forma en que se presentan ó almacena la salida de validación que se genera en los procesos de inserción ó actualización. Cada mensaje consta de una instancia de la clase ActiveRecordMessage. El grupo de mensajes generado puede ser recogido usando el método getMessages() del objeto ActiveRecord donde se produjo la operación.

Cada mensaje ofrece información extendida como el nombre del campo que generó el mensaje y el tipo de mensaje.

En el siguiente ejemplo se ilustra como imprimir los mensajes que resultan de un proceso fallido de inserción:
Ejemplo: Obtener los mensajes de validación de un modelo
<?php

$customer = new Customer();
$customer->setName("Steve Conrad");
$customer->setEmail("s.conrad@mail.com");
if($customer->save()==false){
     foreach($customer->getMessages() as $message){
          Flash::error($message->getMessage());
     }
}
API de ActiveRecordMessage
El API de la clase ActiveRecordMessage es la siguiente:

public void setType(string $type)
Establece el tipo de mensaje.

public string getType()
Obtiene el tipo de mensaje.

public void setMessage(string $message)
Establece el mensaje interno del objeto.

public string getMessage()
Obtiene el mensaje generado.

public void setField(string $field)
Establece el nombre del campo de cuyo valor se generó el mensaje.

public string getField()
Obtiene el nombre del campo que generó el mensaje.
Transacciones en ActiveRecord
Las aplicaciones empresariales generalmente administran datos cuyo valor es critico para las organizaciones y su manejo debe ser seguro, estable y confiable. La integridad de datos se pierde cuando las operaciones son interrumpidas y no se completan satisfactoriamente. Las transacciones en el software tratan precisamente de evitar estas situaciones buscando que haya integridad en los datos y que se puede recuperar la información si ocurre un estado de fallo.

La implementación de Transacciones de Negocio para ActiveRecord esta basada en la idea del patrón del grupo Object-Relational Behavioral llamado Unit of Work, aunque este sin administrar el log de objetos utilizados. Su funcionamiento básicamente permite separar los objetos ActiveRecord que pertenecen a una transacción de tal forma que todas las operaciones efectuadas por ellos mantengan un estado de concurrencia consistente y se pueda controlar si se altera la base de datos ó se hace rollback en caso que se requiera.

La implementación de transacciones en ActiveRecord tiene 2 ventajas principales:

  • Se encapsulan detalles sobre la implementación de transacciones del gestor relacional utilizado.
  • Soporta administración de transacciones declarativas.
  • Esta integrado con el componente TransactionManager que reduce la codificación y administra globalmente las transacciones.
Administracion de Transacciones
La administración de transacciones puede ser utilizada a nivel global ó nivel local, independientemente de la forma en que se utilicen su objetivo es primordialmente controlar la concurrencia a los recursos de persistencia de la aplicación. La gestión de transacciones depende explícitamente de los requisitos de la aplicación:

  • Globales: Las globales son administradas y gestionadas por la aplicación, el bloqueo de recursos es dependiente de la naturaleza de los mismos. Como el estado de ejecución de una aplicación Web duerme entre una petición y otra es necesario cancelar y/o aceptar las transacciones pendientes cada vez que terminan los hilos de ejecución. Las transacciones pueden ser compartidas entre la ejecución de varias acciones en un mismo hilo. Kumbia Enterprise Framework permite establecer transacciones en forma automática para cualquier petición a la aplicación y cada objeto instanciado, sin embargo esta implementación puede resultar demasiado incondicional para las reglas de negocio de una aplicación. Su mayor ventaja es que las operaciones que se realizan a nivel global pueden ser replicadas a varios recursos transaccionales.

  • Locales: Permiten al desarrollador establecer modelos de programación más naturales y faciles de implementar que cumplen detalladamente con la lógica de negocio de la aplicación. La principal desventaja es que tiene a invadir el modelo de programación de la aplicación.

El componente TransactionManager administra la creación, cancelación, destrucción y aceptación de transacciones cuando se implementan en múltiples acciones en un determinado flujo de ejecución ó cuando se usan transacciones globales. Este componente implementa la interfase TransactionManagerInterface, así el desarrollador puede implementar un componente propio de administración de transacciones implementando esta:
<?php

interface TransactionManagerInterface {

     public function getUserTransaction($definition=null);
     public function commit();
     public function rollback();
     public function initializeManager();
     public function rollbackPendent();
     public static function notifyRollback();
     public static function notifyCommit();

}

El método TransactionManager::getUserTransaction() devuelve la última transacción ó crea una si es necesario de acuerdo a la definición pasada como argumento. La definición es una instancia de la clase TransactionDefinition. La definición de la transacción establece parámetros como el nivel de isolación ó si la transacción es propagable:
Ejemplo: Establecer los parámetros de la transacción con un TransactionDefinition
<?php

class AccountsController extends ApplicationController {

     public function createCustomerDataAction(){

          try {
               $definition = new TransactionDefinition();
               $definition-> setIsolationLevel(TransactionDefinition::ISOLATION_SERIALIZABLE);
               $definition->setPropagation(false);
               $definition->setReadOnly(false);
               $definition->setTimeout(0);

               $transaction = TransactionManager::getUserTransaction($definition);

               $customer = new Customer();
               $customer->setTransaction($transaction);
               $customer->setName("John Smith");
               $customer->setStatus("Active");
               if($customer->save()){
                    $this->routeToAction("action: createAccountData");
               } else {
                    $transaction->rollback();
               }
          }
          catch(TransactionFailed $e){
               Flash::error($e->getMessage());
          }
     }

     public function createCustomerDataAction($clientId){

          $clientId = $this->filter($clientId, "int");

          try {
               $transaction = TransactionManager::getUserTransaction($definition);
               $this->Account->setTransaction($transaction);

               $accounts = $this->Account->findWithSharedLock("client_id='$clientId'");
               foreach($accounts as $account){
                    if($account->getStatus()=='Inactive'){
                         $account->setBalance(0);
                         $account->setStatus('Active');
                         if($account->save()==false){
                              foreach($account->getMessages() as $message){
                                   Flash::error($message->getMessage());
                              }
                              $transaction->rollback();
                         }
                    }
               }
               if($transaction->commit()==true){
                    Flash::success("Se creó correctamente el cliente");
               }
          }
          catch(TransactionFailed $e){
               Flash::error($e->getMessage());
          }

     }

}

El ejemplo muestra como se aplica la transacción creada mediante la definición y se reutiliza entre las diferentes acciones que conforman la operación de negocio. Una descripción de los parámetros que se pueden definir en TransactionDefinition es la siguiente:

  • Isolación: Permite establecer el grado de isolación con el que trabajaran las transacciones. Este se refiere al estándar SQL92 y depende de si esta soportado por el gestor relacional.
  • Propagation: La propagación de transacciones permite que cuando una transacción sea cancelada se cancelen otras creadas en la misma petición a la aplicación.
  • Read-Only: Permite establecer una transacción de solo lectura, cualquier intento de modificación (inserción, actualización, borrado) de registros termina en la generación de una excepción.
  • Timeout: Permite establecer un tiempo en segundos después del cual la transacción será cancelada automáticamente si no se ha realizado un commit. Esto permite controlar ciertos procesos de negocio donde el acceso a los recursos es por demanda.

El método estático TransactionManager::getUserTransaction() devuelve una instancia de la clase ActiveRecordTransaction quien administra el acceso al Data Source de la transacción. Múltiples entidades comparten un mismo DataSource aunque esto sea transparentemente administrado.
Sincronización de Recursos con Transacciones
El desarrollador debe tener cuidado al utilizar transacciones, ya que multiples administradores transaccionales que trabajen sobre un mismo gestor relacional podria realizar bloqueos sobre recursos necesarios en una misma operación de negocio que termina en un deadlock y una excepción.

El uso de TransactionManager permite evitar esto en cierta medida ya que este controla la creación, reutilización, propagación, cancelación y aceptación de los procesos relacionados con transacciones así como otras operaciones.
Sincronización de Alto Nivel
El método de ActiveRecord::setTransaction() proporciona un método para el objeto que representa un registro comparta una transacción de forma transparente. La sincronización de alto nivel permite que varios objetos ActiveRecord compartan un mismo DataSource bajo una transacción y todas sus operaciones de bajo nivel son administradas por los componentes del framework lo cual aumenta la seguridad y confiabilidad que la operación no se salga de control y se ejecuten las tareas de finalización de transacciones requeridas.
Sincronización a Bajo Nivel
La conexión al gestor relacional puede ser obtenida de un objeto ActiveRecord mediante el método getConnection(). El objeto devuelto es una instancia de la clase Db que es quien ejecuta las tareas de bajo nivel directamente en el gestor relacional. Los métodos begin(), rollback() y commit() estan presentes y permiten iniciar y terminar transacciones a bajo nivel.
Ejemplo: Utilizar transacciones a bajo nivel
$invoice = new Invoice();
$db = $invoice->getConnection();
$db->begin();
$invoice->find(124);
$invoice->setStatus("Cancelled");
$invoice->save();
$db->commit();
Consideraciones de Sincronización
La importancia de la sincronización yace en la necesidad de una correcta implementación del proceso de negocio en donde la disponibilidad de los recursos sea consistente entre los que son transaccionales y los que no.

Es fundamental entender que los objetos que poseen una misma transacción administran ciertos recursos y se debe controlar que no vayan a ser utilizados ó requeridos en otra transacción dentro del mismo proceso, si no se controlan estas situaciones es posible que se presenten inconsistencias en los procesos de negocio.

En el siguiente proceso se puede comprender la situación presentada:
Ejemplo: Sincronización de recursos en controladores
<?php

try {
     $transaction = new ActiveRecordTransaction(true);
     foreach($Accounts->find("status = 'P'") as $accountItem){
          $accountItem->setTransaction($transaction);
          $accountItem->setStatus("A");
          if($accountItem->save()==false){
               foreach($accountItem->getMessages() as $message){
                    Flash::error($message->getMessage());
               }
               $transaction->rollback();
          }
     }

     $accounts = $Accounts->find("status='A' AND customer_id='$customerId'");
     foreach($accounts as $accountItem){
          $accountItem->setTransaction($transaction);
          $balance = $accountItem->getBalance();
          $accountItem->setBalance($balance-$manageDiscount);
          if($accountItem->save()==false){
               foreach($accountItem->getMessages() as $message){
                    Flash::error($message->getMessage());
               }
               $transaction->rollback();
          }
     }

     $transaction->commit();

}
catch(TransactionFailed $e){
     Flash::error($e->getMessage());
}

En el primer foreach se consultan las cuentas cuyo estado sea igual a 'P' y se les cambia el estado a 'A', en el segundo foreach se consultan las cuentas cuyo estado sea igual a 'A' y pertenezcan a un cliente determinado para luego aplicar un descuento sobre estas cuentas.

El proceso espera que las cuentas recién actualizadas en el primer foreach se les haga igualmente el descuento del segundo, sin embargo esto no será así ya que los resultados devueltos por la transacción base pertenecen a un espejo diferente al espejo de los objetos administrados por la transacción $transaction.

En resumen, los objetos que se espera que también se les aplique el descuento en el segundo foreach quedaran sin este, ya que en el gestor relacional aún no se ha reflejado la actualización de estados del primer foreach.

La solución a este inconveniente es seguir las siguientes consideraciones:

  • Los procesos de negocio que accedan a un mismo recurso, en este caso la entidad Account deben asociar todas las instancias a una misma transacción
  • Si existe integridad referencial en el modelo de datos igualmente es necesario que todas las entidades relacionadas y sus procesos estén administradas por una misma transacción.

Si no es necesario que se reflejen los cambios en el modelo de datos dentro de un mismo proceso de negocio entonces puede hacer caso omiso a estas consideraciones y utilizar tantos administradores de transacciones como requiera.

El ejemplo presentado anteriormente sin el inconveniente mencionado se implementaría así:
Ejemplo: Sincronización de recursos en controladores
<?php

try {
     $transaction = new ActiveRecordTransaction(true);
     $Accounts->setTransaction($transaction);
     foreach($Accounts->find("status = 'P'") as $accountItem){
          $accountItem->setStatus("A");
          if($accountItem->save()==false){
               foreach($accountItem->getMessages() as $message){
                    Flash::error($message->getMessage());
               }
               $transaction->rollback();
          }
     }

     $accounts = $Accounts->find("status='A' AND customer_id='$customerId'");
     foreach($accounts as $accountItem){
          $balance = $accountItem->getBalance();
          $accountItem->setBalance($balance-$manageDiscount);
          if($accountItem->save()==false){
               foreach($accountItem->getMessages() as $message){
                    Flash::error($message->getMessage());
               }
               $transaction->rollback();
          }
     }

     $transaction->commit();

}
catch(TransactionFailed $e){
     Flash::error($e->getMessage());
}

Cada instancia devuelta por find esta automáticamente asociada a la transacción $transaction y se controla que todas las operaciones efectuadas sobre la entidad Accounts se refleje en todo el proceso de negocio.
API de TransactionDefinition
public void setIsolationLevel(int $isolationLevel)
Establece el nivel de Isolación que tendrá la conexión asociada a la transacción. El valor del parámetro $isolationLevel es una constante de TransactionDefinition que puede ser ISOLATION_DEFAULT, ISOLATION_READ_COMMITED, ISOLATION_READ_UNCOMMITED, ISOLATION_REPETEABLE_READ y ISOLATION_SERIALIZABLE. La disponibilidad de estas depende exclusivamente de si esta soportada por el gestor relacional.

public void setPropagation(boolean $propagation)
Establece un valor booleano que indica si las transacciones creadas al cancelarse cualquiera se propagará el estado a las demás del administrador de transacciones.

public void setTimeout(int $timeout)
Establece el tiempo máximo que pueda durar una transacción antes que sea cancelada.

public void setReadOnly(boolean $readOnly)
Establece el carácter de solo lectura de la conexión
API de ActiveRecordTransaction
public __construct(boolean $autoBegin=false, TransactionDefinition $definition=null)
Constructor de la definición de la transacción.

public boolean commit()
Realiza la operación de 'commit' sobre la transacción administrada.

public boolean rollback()
Realiza la operación de 'rollback' sobre la transacción administrada. Si la transacción es propagable cancelará otras transacciones creadas bajo el mismo TransactionManager.

public boolean begin()
Permite iniciar la transacción en la conexión activa al gestor relacional.

public DbBase getConnection()
Obtiene le objeto del adaptador al gestor relacional que administrada la transacción.

public void setIsNewTransaction(boolean $isNew)
Establece si la transacción ha creado una nueva conexión al gestor relacional ó esta reusando una existente.

protected void setPropagation(boolean $propagation)
Establece si la transacción esta en modo de propagación. La propagación solo funciona si la transacción fue creada con el TransactionManager.

public bool getPropagation()
Devuelve el estado de propagación de la transacción. La propagación solo funciona si la transacción fue creada con el TransactionManager.
Timeouts en Transacciones
Kumbia Enterprise ofrece un entorno administrado para la implementación de transacciones de tal forma que es posible conocer cuando ocurre alguna novedad con una transacción. Los timeouts son un ejemplo de esto, cuando un procedimiento no puede obtener un bloqueo para efectuar la operación una excepción de tipo DbLockAdquisitionException es generada.
Ejemplo: Capturar una excepción al no poder obtener un bloqueo en una transacción
<?php

class OrdersController extends ApplicationController {

     public function increaseQuantityAction(){
          try {
               $transaction = new ActiveRecordTransaction(true);
               $this->Products->setTransaction($transaction); 
               $products = $this->Products->findForUpdate("quantity<100");
               foreach($products as $product){
                    $product->setQuantity($product->getMinStock()*2);
                    if($product->save()==false){
                         $transaction->rollback();
                    }
               }
          }
          catch(DbLockAdquisitionException $e){
               Flash::error("No se pudo obtener el bloqueo requerido");
          }
          catch(TransactionFailed $e){
               Flash::error($e->getMessage());
          }
     }
}
Validadores de Integridad de Datos
ActiveRecord permite que los modelos ejecuten tareas de validación definidas por el desarrollador que garanticen que los datos que se almacenen en la persistencia sean íntegros y se evite todo lo que esto conlleva.

Los eventos del modelo: beforeValidation, beforeValidationOnCreate, beforeValidationOnUpdate y validation permiten definir reglas de validación en forma general ó de acuerdo a la operación que se vaya a realizar sobre el modelo:
Ejemplo: Definir validadores de integridad de datos en un modelo
<?php

class Customers extends ActiveRecord {

     protected $id;
     protected $name;
     protected $e_mail;
     protected $status;

     protected function setId($id){
          $this->id = $id;
     }

     protected function setName($name){
          $this->name = $name;
     }

     protected function setEMail($e_mail){
          $this->e_mail = $e_mail;
     }

     protected function setStatus($status){
          $this->status = $status;
     }

     protected function validation(){
          $this->validate("Length", array(
               "field" => "name",
               "minimum" => 10, 
               "maximum" => 50
          ));
          $this->validate("Email", "e_mail");
          $this->validate("InclusionIn", array(
               "field" => "status", 
               "domain" => array("A", "I"), 
               "required" => false
          ));

          if($this->validationHasFailed()==true){
               return false;
          }
     }
}

El método protegido de los modelos ActiveRecord::validate() recibe en su primer parámetro el validador con el que se efectuará la validación y como segundo parámetro una lista de opciones para el mismo. Algunos validadores pueden recibir simplemente el nombre del campo a validar como segundo parámetro. El desarrollador puede determinar si la validación debe cancelar la acción que se está ejecutando sobre el modelo mediante el método ActiveRecord::validationHasFailed() el cuál indica si se han generado mensajes de validación en las clases validadoras. El devolver false desde cualquiera de los eventos de validación conlleva a la cancelación de la acción actual y a que el método save, create ó update también devuelvan false.

El Listado de validadores por defecto que contiene la versión estándar de Kumbia Enterprise Framework son los siguientes:
Tabla: Validadores por defecto que hacen parte de ActiveRecord
Validador
Descripción
PresenceOfEs un validador implicito para los modelos cuyas entidades tengan atributos NOT NULL y sean requeridos al insertar ó actualizar.
DateInValida que el valor de un atributo de la entidad tenga un formato de fecha valido.
LengthValida que el numero de caracteres de un el valor de un atributo de la entidad tipo String tenga un limite inferior, superior ó ambos.
InclusionIn Valida que el valor de un atributo de la entidad se encuentre en un listado estático definido en un vector. El vector se define usando la opción 'domain'.
ExclusionInValida que el valor de un atributo de la entidad no se encuentre en un listado estático definido en un vector. El vector se define usando la opción 'domain'
NumericalityValida que el valor de un atributo de la entidad sea numérico.
FormatValida que el valor de un atributo de la entidad tenga un formato establecido en una expresión regular, esta se define usando la opción 'format'.
EmailValida que el valor de un atributo de la entidad tenga un formato de e-mail correcto.
UniquenessPermite validar que el valor de un atributo no exista en otro registro de la entidad. La opción "field" puede recibir un Array cuando se debe validar combinaciones de campos.

Validadores para atributos No-Nulos
Cuando se define un validador, este valida cualquier valor que se vaya a insertar ó actualizar sobre la persistencia. Algunas veces el atributo puede permitir nulos, el valor nulo igual será validado por el Validador y si no cumple con las condiciones establecidas generará un mensaje y dependiendo de las condiciones podría detener la ejecución de la operación actual.

Una forma de establecer una regla de excepción para estos casos es agregar la opción 'required' con valor false al método validate:
Ejemplo: Establecer reglas de validación de integridad de datos en modelos
protected function validation(){     
     $this->validate("Numericality", array(
          "field" => "precio_venta", 
          "required" => false
));
     if($this->validationHasFailed()==true){
          return false;
     }
}

Ejemplo: Validar que una combinación de campos no se repita
protected function validation(){          
     $this->validate("Uniqueness", array(
          "field" => array("invoice_number", "invoice_preffix")
     );
     if($this->validationHasFailed()==true){
          return false;
     }
}
Tratar el resultado de un proceso de Validación
Cuando se ejecuta el método inteligente save() se ejecutan los procesos de validación y los validators asociados al modelo, normalmente, el método save devuelve un valor booleano indicando el éxito de la operación, cuando el valor devuelto es false es posible obtener los mensajes que se han generado y tratarlos adecuadamente para ser presentados al usuario.
Ejemplo: Obtener los mensajes de validación cuando falla una operación de manipulación de datos
<?php

$customer = new Customer();
$customer->setName("Carl Johnson");
$customer->setEmail("c.johnson@mail.com");
$customer->setContractDate("2009-02-01");
if($customer->save()==false){
     foreach($customer->getMessages() as $message){
          Flash::error($message->getMessage());
     }
}

Aplicando la API de ActiveRecordMessage es posible personalizar los mensajes de validación y la forma en que se tratan. En el siguiente ejemplo se muestra como presentar los mensajes de validación de correo electrónico como advertencias y el resto como errores:
Ejemplo: Personalizar la presentación de los mensajes de validación mediante el API de ActiveRecordMessage
<?php

$customer = new Customer();
$customer->setName("Carl Johnson");
$customer->setEmail("c.johnson@mail.com");
$customer->setContractDate("2009-02-01");
if($customer->save()==false){
     foreach($customer->getMessages() as $message){
          if($message->getType()=='Email'){
               Flash::warning($message->getMessage());
          } else {
               Flash::error($message->getMessage());
          }
     }
}
El campo que genera el mensaje también permite personalizar los mensajes generados:
Ejemplo: Obtener información de los mensajes de validación en un proceso de manipulación de datos
<?php

$customer = new Customer();
$customer->setName("Carl Johnson");
$customer->setEmail("c.johnson@mail.com");
$customer->setContractDate($contractDate);
if($customer->save()==false){
     foreach($customer->getMessages() as $message){
          if($message->getField()=='contractDate'){
               Flash::warning("La fecha de contrato no ha sido establecida");
          } else {
               Flash::error($message->getMessage());
          }
     }
}
Validadores de Usuario
El desarrollador puede definir validadores de aplicación que extiendan ó se adecuen mejor a las reglas de negocio de la aplicación. Todos los validadores deben cumplir con unos requisitos para su integración con ActiveRecord y su correcta ejecución:

  • El Validador debe estar ubicado en el directorio validators en el directorio de la aplicación. Si este directorio no existe se debe crear.
  • Los archivos donde se implementan las clases validadoras deben tener la siguiente convención: NombreValidator.php
  • Los validadores pueden heredar la clase ActiveRecordValidator la cual implementa una estructura consistente para el desarrollo de un validador eliminando detalles poco usables.
  • Las clases validadoras deben llamarse usando la convención: NombreValidator
  • Las clases validadoras deben implementar la interface ActiveRecordValidatorInterface.

La interface ActiveRecordValidatorInterface tiene la siguiente estructura:
interface ActiveRecordValidatorInterface {

     public function __construct($record, $field, $value, $options = array());
     public function checkOptions();
     public function getMessages();
     public function validate();

}

Tanto el constructor como el método getMessages() se encuentran ya definidos en la clase ActiveRecordValidator. Un validador solo implementa validate y opcionalmente sobrescribe checkOptions. El primero realiza la validación en si y devuelve un valor booleano indicando si el proceso fue satisfactorio ó no. El método checkOptions permite chequear si los parámetros enviados en las opciones del validador son correctos.

En el siguiente ejemplo se implementa un validador que proporciona a la aplicación un servicio de validación de números de identificación de acuerdo a las convenciones de algún país. Los números de identificación deben ser de 20 dígitos, los 3 primeros deben ser las letras ABC si es una empresa ó XYZ si es una persona natural.

El archivo validators/IdentificationValidator.php queda así:
Ejemplo: Crear un validador para integridad de datos en modelos
<?php

class IdentificationValidator extends ActiveRecordValidator
     implements ActiveRecordValidatorInterface {

     public function checkOptions(){
          if($this->isSetOption('type')==false){
               throw new ActiveRecordException("Debe indicar el tipo de numero de identificación para IdentificationValidator");
          }
          if(!in_array($this->getOption('type'), array('any', 'company', 'people'))){
               throw new ActiveRecordException("El tipo de número de identificación no es valido, para IdentificationValidator");
          }
     }

     public function validate(){
          $number = $this->getValue();
          $type = $this->getOption('type');
          $valid = true;
          if($type=='any'||$type=='company'){
               if(substr($number, 0, 3)=='ABZ'){
                    $valid = false;
               }
          }
          if($type=='any'||$type=='people'){
               if(substr($number, 0, 3)!='XYZ'){
                    $valid = false;
               }
          }
          if(strlen($number)!=20){
               $valid = false;
          }
          if(!$valid){
               $this->appendMessage("El valor del campo '".$this->getFieldName()."' debe ser una identificación válida");
               return false;
          } else {
               return true;
          }
     }
}

Para aplicar el validador se invoca el método validate en la implementación de algún evento del modelo de tipo 'validation':
Ejemplo: Implementar un evento de validación de acuerdo a la operación ejecutada
protected function beforeValidationOnCreate(){          
     $this->validate("Identification", array(
          "field" => "number_iden", 
          "type" => "any"
     ));
     if($this->validationHasFailed()==true){
          return false;
     }
}
Eventos en la Validación
Los modelos permiten la implementación de eventos que se lanza cuando se efectúa una operación de inserción ó actualización y que tienen como objetivo definir lógica de validación e integridad de datos en la aplicación.

A continuación se nombran y explican los eventos soportados así como su orden de ejecución:
Tabla: Tipos de eventos de validación y su orden de ejecución
Operación
NombrePuede cancelar operación:Explicación
Inserción y ActualizaciónbeforeValidationSISe ejecuta antes de que se validen los campos no nulos y se ejecuten los validadores de integridad de datos.
InserciónbeforeValidationOnCreateSISe ejecuta antes de que se validen los campos no nulos y se ejecuten los validadores de integridad de datos, solo cuando se realiza una inserción sobre el modelo.
ActualizaciónbeforeValidationOnUpdateSISe ejecuta antes de que se validen los campos no nulos y se ejecuten los validadores de integridad de datos, solo cuando se realiza una actulización del modelo
Actualización ó InserciónonValidationFailsSI (ya se ha detenido)Se ejecuta después que un validador de integridad falla.
InserciónafterValidationOnCreateSISe ejecuta después de que se validen los campos no nulos y se ejecuten los validadores de integridad de datos, solo cuando se realiza una inserción sobre el modelo.
ActualizaciónafterValidationOnUpdateSISe ejecuta después de que se validen los campos no nulos y se ejecuten los validadores de integridad de datos, solo cuando se realiza una actulización del modelo
Inserción y ActualizaciónafterValidationSISe ejecuta después de que se validen los campos no nulos y se ejecuten los validadores de integridad de datos.
Inserción y ActualizaciónbeforeSaveSISe ejecuta antes de realizar la operación requerida sobre el gestor relacional.
ActualizaciónbeforeUpdateSISe ejecuta antes de realizar la actualización en el gestor relacional.
InserciónbeforeCreateSISe ejecuta antes de realizar la inserción en el gestor relacional.
ActualizaciónafterUpdateNOSe ejecuta después de realizar la actualización en el gestor relacional.
InserciónafterCreateNOSe ejecuta después de realizar la inserción en el gestor relacional.
Inserción y ActualizaciónafterSaveNOSe ejecuta después de realizar la operación requerida sobre el gestor relacional.
EliminaciónbeforeDeleteSISe ejecuta antes de eliminar el registro del gestor relacional.
EliminaciónafterDeleteNOSe ejecuta después de eliminar el registro del gestor relacional.

Implementar un evento de validación
Cuando se ejecuta una operación de inserción, actualización ó eliminación sobre el modelo ActiveRecord verifica si se han definido métodos con los nombres de los eventos y en caso de encontrarlos los ejecuta en el orden mencionado en la tabla anterior.

Los eventos de validación deben ser métodos protegidos recomendablemente para evitar que la lógica de datos que contienen se exponga públicamente.

En el siguiente ejemplo se implementa un evento que valida que la cantidad a actualizar ó insertar sobre el modelo sea mayor a 0 según los requerimientos del negocio:
Ejemplo: Implementar un evento de validación para modelos
<?php

class Products extends ActiveRecord {

     protected function beforeSave(){
          if($this->quantity<0){
               Flash::error("La cantidad no puede ser negativa");
               return false;
          }
     }

}
Detener/Cancelar una operación
Si dadas las condiciones al ejecutar un evento de validación se requiere detener ó cancelar la operación que se esta ejecutando se puede realizar si el evento lo permite y así evitar que se almacenen datos incorrectos favoreciendo la integridad del dominio de datos.

En la tabla de eventos de validación es posible verificar si el evento permite la cancelación de la operación. Un método que requiera detener la operación debe devolver el valor boleano false, en este caso los métodos save, create y update también devolveran false indicando que no se pudo efectuar la operación.
Establecer un evento con un nombre no estándar
Los eventos de validación también pueden ser definidos usando atributos protegidos en la clase del modelo. De esta forma se pueden definir uno ó más eventos en forma dinámica flexibilizando la implementación de estos:
Ejemplo: Cambiar el nombre estándar de un evento en un modelo
<?php

class Products extends ActiveRecord {

     protected $beforeSave = "myCustomEvent"

     protected function myCustomEvent(){
          if($this->quantity<0){
               Flash::error("La cantidad no puede ser negativa");
               return false;
          }
     }

}

Si se requiere ejecutar varios métodos para un mismo evento se puede indicar un vector con la lista de métodos en el orden de ejecución requerido. Si alguno falla los demás no serán ejecutados.
Ejemplo: Definir múltiples callbacks para un mismo evento de un modelo
<?php

class Products extends ActiveRecord {

     protected $beforeSave = array("myFirstEvent", "mySecondEvent");

     protected function myFirstEvent(){
          if($this->quantity<0){
               Flash::error("La cantidad no puede ser negativa");
               return false;
          }
     }

     protected function mySecondEvent(){
          if($this->quantity>100){
               Flash::error("La cantidad no puede ser mayor a 100");
               return false;
          }
     }


}

Los eventos pueden ser establecidos dinámicamente desde otros eventos ó en el inicializador del modelo. De esta forma se pueden crear potentes modelos de validación de la lógica de datos en una aplicación:
Ejemplo: Definir eventos del modelo dinámicamente mediante atributos
<?php

class Products extends ActiveRecord {

     protected $beforeSave;

     protected function beforeValidation(){
          if($this->category=="Food"){
               $this->beforeSave = "checkFoodQuantity";
          } else {
               $this->beforeSave = "checkOtherQuantity";
          }
     }

     protected function checkFoodQuantity(){
          if($this->quantity>100){
               Flash::error("La cantidad de alimentos no puede ser mayor a 100");
               return false;
          }
     }

     protected function checkOtherQuantity(){
          if($this->quantity>50){
               Flash::error("La cantidad no puede ser mayor a 50");
               return false;
          }
     }

}
Evento cuando el proceso de validación detiene la operación
Las operaciones ejecutadas en el método save pueden fallar si al menos un validador falla en este caso develve el valor booleano false indicando este estado. Es posible definir un evento llamado 'onValidationFails' que es ejecutado cuando el proceso de validación devuelve un resultado no satisfactorio. En este evento se puede definir elementos de logging ó informativos de usuario para indicar el porqué no se ha podido realizar la operación requerida.
Ejemplo: Definir un evento a la espera de un fallo en el proceso de validación de un modelo
<?php

class Products extends ActiveRecord {

     protected function onValidationFails(){
          if($this->operationWasCreate()==true){
               Flash::error("La inserción falló");
          }
          if($this->operationWasUpdate()==true){
               Flash::error("La actualización falló");
          }
          foreach($this->getMessages() as $message()){
               Flash::error($message->getMessage());
          }
     }


}
Deshabilitar eventos de validación
Los eventos de validación pueden ser deshabilitados para aumentar el rendimiento en aplicaciones que no requieren de ellos ó cuando se ejecutan procesos de manipulación de datos que no requieran de esta característica.

El método ActiveRecord::disableEvents(bool $disable) permite cambiar este comportamiento. Un valor booleano true los deshabilita y un valor false los habilita nuevamente. Esta funcionalidad actúa en forma general para cualquier modelo de la aplicación.
Llaves Foráneas Virtuales
ActiveRecord permite definir llaves primarias virtuales que validen la integridad relacional en las operaciones de manipulación de datos asegurando que los campos llave contengan valores que existan en las entidades referenciadas.

Ventajas sobre el control convencional a nivel de base datos:

  • Es posible validar integridad relacional sobre columnas con tipos de datos diferentes
  • Se disminuye la carga del motor de base de datos
  • Es posible controlar la integridad sobre tablas en diferentes motores ó diferentes servidores
  • Se puede generar mensajes de usuario personalizados de acuerdo al intento fallido de efectuar una operación por violación de llave foránea.
  • Se puede validar la integridad en motores que no soporten llaves foraneas (MyISAM en MySQL).
  • Se pueden definir llaves foráneas que validen la integridad en objetos de la base de datos como vistas y sinonimos.
  • No se requieren indices en las tablas referenciadas (aunque es recomendado que los tengan).

Desventajas de las llaves foráneas virtuales:

  • El rendimiento no es tan óptimo como el de las administradas directamente por el motor de base de datos.
  • La integridad solo es validada cuando las operaciones se realizan mediante eventos realizados a través de los modelos.
Crear una llave foránea virtual
El método protegido de ActiveRecord llamado addForeignKey permite la definición de llaves foráneas virtuales en el inicializador de la clase modelo.

Mediante convenciones se coloca solamente el nombre del campo y de esta forma la tabla y campo referenciado se intuye normalmente:
Ejemplo: Definir una llave foránea virtual por convención
<?php

class Products extends ActiveRecord {

     protected function initialize(){
          $this->addForeignKey("categories_id");
     }

}

Sino se utiliza un modelo entidad-relación que use convenciones la forma de indicar las llaves foráneas se realiza de esta manera:
Ejemplo: Establecer llaves foráneas de acuerdo al modelo entidad relación
<?php

class Employees extends ActiveRecord {

     protected function initialize(){

          //Un campo a un campo
          $this->addForeignKey("code_cat", "categories", "code");

          //Varios campos a varios campos
          $this->addForeignKey(
               array("country_born", "city_born"), 
               "cities", 
               array("country_code", "city_code")
          );

     }

}
Opciones de las llaves foráneas
El cuarto parámetro de addForeignKey permite establecer opciones adicionales como el mensaje a generar cuando se viole la integridad y las acciones a realizar cuando se actualiza ó borra un registro y falla el proceso de validación.
Ejemplo: Establecer acciones de una llave foránea virtual en un modelo
<?php

class Employees extends ActiveRecord {

     protected function initialize(){

          $this->addForeignKey("office_id", "office", "id", array(
               "message" => "La oficina %s no existe",
               "on_delete" => ActiveRecord::ACTION_CASCADE,
               "on_update" => ActiveRecord::ACTION_RESTRICT
          ));

     }

}
Tabla: Opciones de addForeignKey
Opción
Descripción
messageMensaje del validador cuando se viola la llave foránea y la acción efectuada debe restringir la operación.
on_deleteEstablece una acción a ejecutar cuando una llave foránea es violada al hacer una eliminación de datos en la entidad. El valor es alguna de las constantes ActiveRecord::ACTION_CASCADE ó ActiveRecord::ACTION_RESTRICT.
on_updateEstablece una acción a ejecutar cuando una llave foránea es violada al hacer una actualización de datos en la entidad. El valor es alguna de las constantes ActiveRecord::ACTION_CASCADE ó ActiveRecord::ACTION_RESTRICT.

Tabla: Acciones dependiendo de la operación
Opción
Descripción
ActiveRecord::ACTION_CASCADECuando se realiza una eliminación de datos indica que todas los datos en llaves dependientes deben ser eliminados antes de efectuar la operación. Esto permite que no queden registros huérfanos. En una actualización se actualiza el nuevo valor en las relaciones dependientes.
ActiveRecord::ACTION_RESTRICTIndica que se debe cancelar la operación actual debido a la violación de la llave foránea. El objeto ActiveRecordMessage es cargado con el mensaje establecido en la opción 'message'.

Entidades Temporales
Administrar datos en una entidad proporciona recursos importantes para las aplicaciones de negocios. En ciertas ocasiones se requiere utilizar entidades para almacenar datos temporales que permitan la ejecución de procesos de negocio sin que estas afecten el dominio de datos en forma notable.

El componente ActiveRecord proporciona el subcomponente TemporaryActiveRecord el cual permite crear modelos que administran sus datos sobre entidades temporales en el gestor relacional.

Este tipo de entidades pueden ser consideradas de alto rendimiento ya que no requieren de escritura de disco además este tipo de modelos están optimizados para un procesamiento más efectivo en memoria.

En estos casos la persistencia es temporal ya que todos los datos que administra el modelo existen en memoria mientras la conexión al gestor relacional esta activa. Como estas conexiones se realizan en modo no-persistente los datos en las entidades creadas solo existen durante el tiempo que emplee la petición en ejecutarse por completo creándose y destruyéndose cada vez que se utilicen.
Crear un TemporaryActiveRecord
Lo siguiente se debe hacer para crear un modelo temporal:

  • Se crea un modelo que extienda al subcomponente TemporaryActiveRecord en el directorio de modelos.
  • Se debe implementar un método protegido que se debe llamar _tableDefinition es requerido para obtener la definición de la entidad, esta constituye un array asociativo con las llaves attributes e indexes.
  • La llave attributes es obligatoria y contiene un vector asociativo cuyas llaves corresponden a los nombres de los campos y cuyo valor es una descripción del campo tal y como es aceptada por el método de los adaptadores del componente Db llamado createTable.
  • Cuando se definan los atributos del modelo como protegidos se deben definir con los mismos nombres utilizados en la definición del método _tableDefinition.
  • Si no se definen los atributos protegidos ActiveRecord los definirá como públicos aunque esto no es recomendable.
  • Las entidades temporales también soportan relaciones unidireccionales y bidireccionales definiéndose como es usual en los modelos persistentes.
Comportamiento de un TemporaryActiveRecord
El siguiente comportamiento debe tenerse en cuenta cuando se trabaja con entidades temporales:

  • Al instanciarse por primera vez la clase del modelo temporal cuando el Facility es USER_LEVEL se crea una tabla temporal en la conexión por defecto al gestor relacional.
  • La tabla temporal creada tiene el nombre de la clase utilizada las limitaciones del gestor relacional en cuanto a los nombres de tablas y que otras entidades no existan con este nombre deben ser tenidas en cuenta.
  • Las sentencias de creación de la tabla temporal no corresponden a manipulación de datos por lo tanto las transacciones globales y locales no tienen en cuenta esta operación.
  • Como la tabla es creada en cada petición en donde se haga uso del modelo los meta-datos de la tabla no son almacenados así que no es posible obtener información de esta usando ActiveRecordMetaData.
  • La tabla es destruida en cuanto se cierra la conexión con el gestor relacional, si es requerido destruirla desde la aplicación se puede usar el método del administrador de entidades EntityManager::destroyTemporaryEntity(string $name) donde $name es el nombre del modelo, este método destruye la tabla temporal en cada conexión al gestor relacional donde fue creada. Si una nueva instancia del la clase modelo es invocada ó utilizada la tabla temporal se creará nuevamente.
Transacciones con Entidades Temporales
Las operaciones de manipulación de datos sobre las entidades temporales soportan transacciones en forma transparente, aunque se debe tener en cuenta que no todos los gestores relacionales soportan transacciones en tablas temporales.

Al crear una transacción ya sea directamente con ActiveRecordTransaction ó usando TransactionManager se crea una nueva conexión temporal al gestor relacional y es posible que la tabla temporal esté creada sobre la conexión no-transaccional por defecto.

Cuando esto se detecta, ActiveRecord crea nuevamente la tabla en la conexión transaccional pero es seguro que los datos que ya se hayan almacenado usando los modelos asociados a la conexión no transaccional no esten disponibles en la nueva.
Usar un TemporaryActiveRecord
En el siguiente ejemplo se ilustra como implementar una entidad temporal y como utilizarlo en un proceso de negocio.

El modelo temporal models/product_stadistics.php queda así:
Ejemplo: Crear una entidad temporal
<?php

class ProductStadistics extends TemporaryActiveRecord {

     protected $id;
     protected $product_id;
     protected $cantidad;

     public function getId(){
          return $this->id;
     }

     public function getProductId(){
          return $this->product_id;
     }

     public function getCantidad(){
          return $this->cantidad;
     }

     public function setId($id){
          $this->id = $id;
     }

     public function setProductId($product_id){
          $this->product_id = $product_id;
     }

     public function setCantidad($cantidad){
          $this->cantidad = $cantidad;
     }

     protected function _tableDefinition(){
          return array(
               "attributes" => array(
                    "id" => array(
                         "type" => DbBase::TYPE_INTEGER,
                         "notNull" => true,
                         "primary" => true,
                         "auto" => true
                    ),
                    "product_id" => array(
                         "type" => DbBase::TYPE_INTEGER,
                         "notNull" => true
                    ),
                    "cantidad" => array(
                         "type" => DbBase::TYPE_VARCHAR,
                         "notNull" => true,
                         "size" => 10
                    )
               ),
               "indexes" => array("product_id")
          );
     }

     protected function initialize(){
          $this->belongsTo("product");
     }

}
El modelo anterior permite ingresar una serie de datos y aprovechar las capacidades de ordenamiento y agrupamiento para obtener las estadísticas de la venta de unos productos.

Es necesario que las entidades relacionadas al proceso de negocio esten asociadas a la transacción donde se manipulan los datos de la entidad temporal cuando se instancian directamente de la clase modelo ó se inyectan en el controlador usando $this.
Ejemplo: Utilizar entidades temporales con transacciones
try {
     $transaction = TransactionManager::getUserTransaction();     
     $this->Movement->setTransaction($transaction);
     $this->Cart->setTransaction($transaction);
     $conditions = "sellDate = '".Date::getCurrentDate()."'";
     foreach($this->Movement->find($conditions) as $movement){
          $productStadistic = new ProductStatistics();
          $productStadistic->setTransaction($transaction);
          $productStadistic->setProductId($movement->getProductId());
          $productStadistic->setCantidad($movement->getQuantity());
          if($productStadistic->save()==false){
               foreach($productStadistic->getMessages() as $message){
                    Flash::error($message->getMessage());
               }
               $transaction->rollback();
          }
     }
     $transaction->commit();
     $productStadistic = $this->ProductStadistics->findFirst();

     echo "La cantidad total de productos es: ";
     echo $productStadistic->sum("cantidad")."<br>";

     echo "La cantidad minima es: ";
     echo $productStadistic->minimum("cantidad")."<br>";

     echo "La cantidad máxima es: ";
     echo $productStadistic->maximum("cantidad")."<br>";

     echo "El promedio de cantidad es: ";
     echo $productStadistic->average("cantidad")."<br>";

}
catch(TransactionFailed $e){
     Flash::error($e->getMessage());
}
ActiveRecordJoin
El subcomponente ActiveRecordJoin permite aprovechar las relaciones establecidas en el modelo de datos para generar consultas simples ó con agrupamientos en más de 2 entidades relacionadas ó no relacionadas, proponiendo una forma adicional de utilizar el Object-Relational-Mapping (ORM).

El constructor de la clase acepta un vector con las opciones de la consulta. De acuerdo al tipo (consulta simple, agrupación de conteo, sumatoria, máximo, mínimo) que se requiera utilizar estos varian.

En el ejemplo existen 4 entidades Products ProductCategories, Customers, Invoices e InvoicesLines:
CREATE TABLE `products_categories` (
  `id` int(18) NOT NULL,
  `name` varchar(70) default NULL,
  PRIMARY KEY  (`id`)
);

CREATE TABLE `products` (
  `id` int(18) NOT NULL,
  `name` varchar(100) default NULL,
  `products_categories_id` int(11) NOT NULL,
  `quantity` int(11) NOT NULL,
  `price` decimal(16,2) NOT NULL,
  PRIMARY KEY  (`id`)
);

CREATE TABLE `customers` (
  `id` int(18) NOT NULL,
  `name` varchar(20) default NULL,
  PRIMARY KEY  (`id`)
);

CREATE TABLE `invoices` (
  `id` int(18) NOT NULL,
  `customers_id` int(18) NOT NULL,
  `sell_date` date NOT NULL,
  PRIMARY KEY  (`id`)
);

CREATE TABLE `invoices_lines` (
  `id` int(18) NOT NULL,
  `invoices_id` int(18) NOT NULL,
  `products_id` int(18) NOT NULL,
 `quantity` int(11) NOT NULL,
 `price` decimal(16,2) NOT NULL,
 `taxes` decimal(16,2) NOT NULL,
  PRIMARY KEY  (`id`)
);

Los modelos de estas entidades son los siguientes, (a propósito se omiten los getters/setters):
Ejemplo: Definición de modelos y sus relaciones para uso con ActiveRecordJoin
<?php

// Modelo de Categorias de Productos
class ProductsCategories extends ActiveRecord {
     
     public function initialize(){
          // Relación 1-n con productos
          $this->hasMany("products");
     }
     
}

// Modelo de Productos
class Products extends ActiveRecord {
     
     public function initialize(){
          // Relacion 1-1 inversa con categorias de productos
          $this->belongsTo("products_categories");
          // Relacion 1-n con lineas de facturas
          $this->hasMany("invoices_lines");
     }
     
}

// Modelo de Clientes
class Customers extends ActiveRecord {
     
     public function initialize(){
          // Relacion 1-n con facturas
          $this->hasMany("invoices");
     }
     
}

// Modelo de Facturas
class Invoices extends ActiveRecord {
     
     public function initialize(){
          // Relacion 1-1 inversa con clientes
          $this->belongsTo("customers");          
     }
     
}

// Modelo de Detalle de Facturas
class InvoicesLines extends ActiveRecord {
     
     public function initialize(){
          // Relacion 1-1 inversa con clientes
          $this->belongsTo("invoices");          
          // Relacion 1-1 inversa con productos
          $this->belongsTo("products");
     }
     
}

Una vez se definan las relaciones entre las entidades del dominio de datos es posible utilizarlas tanto para obtener registros simples ó colecciones mediante ActiveRecorJoin:
Ejemplo: Hacer un Join de tres entidades
//Listar los productos vendidos, su cantidad y la fecha en que se vendieron

$query = new ActiveRecordJoin(array(
     "entities" => array("Invoices", "Products", "InvoicesLines"),
     "fields" => array(
          "{#Products}.name", 
          "{#Invoices}.sell_date", 
          "{#InvoicesLines}.quantity"
     )
));
Los resultados de la consulta pueden ser obtenidos mediante el método getResultSet() del objeto ActiveRecordJoin. El valor de cada columna de la consulta puede ser obtenido usando un getter virtual creado implícitamente en cada objeto resultado ó mediante el nombre de la columna como una propiedad pública.
Ejemplo: Obtener los registros generados en un Join con ActiveRecordJoin
//Mediante getters
foreach($query->getResultSet() as $result){
     echo $result->getName()." ".
     $result->getSellDate()." ".
     $result->getQuantity()."\n";
}

//Mediante atributos públicos
foreach($query->getResultSet() as $result){
     echo $result->name." ".$result->sell_date." ".$result->quantity."\n";
}
La consulta interna SQL SELECT puede ser examinada mediante el método getSQLQuery() ActiveRecordJoin. La consulta anterior genera la siguiente sentencia:
Ejemplo: SQL generado por ActiveRecordJoin
SELECT products.name, invoices.sell_date, invoices_lines.quantity FROM invoices, products, invoices_lines WHERE products.id = invoices_lines.products_id AND invoices.id = invoices_lines.invoices_id ORDER BY 1

Notése como las relaciones adecuadas hacen parte de la sentencia SELECT en forma dinámica.
Agrupamientos con ActiveRecordJoin
ActiveRecordJoin permite crear consultas avanzadas utilizando agrupamientos. Los agrupamientos soportados son: sumatorias, conteos, mínimos, máximos y promedios. Un agrupamiento se establece de la siguiente forma:
Ejemplo: Agrupamientos de datos con ActiveRecordJoin
//Cuantos productos se han vendido por cada producto
$query = new ActiveRecordJoin(array(
     "entities" => array("Invoices", "Products", "InvoicesLines"),
     "groupFields" => array("{#Products}.name"),
     "sumatory" => array("{#InvoicesLines}.quantity")
));

// Cuantos productos se han vendido por cada producto, cuya cantidad vendida sea // mayor a 150
$query = new ActiveRecordJoin(array(
     "entities" => array("Invoices", "Products", "InvoicesLines"),
     "groupFields" => array("{#Products}.name"),
     "sumatory" => array("{#InvoicesLines}.quantity"),
     "having" => "quantity>10"
));

//En promedio cuál ha sido el precio del producto durante el mes de octubre
$query = new ActiveRecordJoin(array(
     "entities" => array("Invoices", "Products", "InvoicesLines"),
     "groupFields" => array("{#Products}.name"),
     "sumatory" => array("{#InvoicesLines}.price"),
     "conditions" => "MONTH({#Invoices}.sell_date) = 10"
));
Parámetros de ActiveRecordJoin
Los parámetros soportados por ActiveRecordJoin son:
Tabla: Parámetros soportados por ActiveRecordJoin
Opción
Descripción
entitiesUn vector que indica los modelos utilizados para realizar el join. No se debe utilizar el mismo nombre las tablas sino el nombre de las clases usadas como modelos para acceder a ellas.
conditionsUn string con condiciones adicionales del join. No se debe utilizar los nombres de las tablas en las condiciones sino la convención {#NombreClase}. (opcional)
sumatoryUn vector con los campos en los cuales se debe aplicar un agrupamiento de sumatoria. (opcional)
averageUn vector con los campos en los cuales se debe aplicar un agrupamiento de promedio. (opcional)
countUn vector con los campos en los cuales se debe aplicar un agrupamiento de conteo. (opcional)
maximumUn vector con los campos en los cuales se debe aplicar un agrupamiento de valor máximo. (opcional)
minimumUn vector con los campos en los cuales se debe aplicar un agrupamiento de valor mínimo. (opcional)
havingUn string con condiciones para el agrupamiento. (opcional).
noRelationsCon el valor true indica que no se deben construir dinámicamente las relaciones entre las entidades del join. Por defecto su valor es false. (opcional).

ActiveRecordUnion
El objetivo del subcomponente ActiveRecordUnion es unir dos ó más objetos ActiveRecordResultset ó ActiveRecordRow sin volcar a memoria los resultados producidos por ellos consiguiendo una gestión de los recursos de la aplicación más eficiente.
SessionRecord
El subcomponente SessionRecord permite administrar entidades de persistencia de sesión de forma natural como si fuesen entidades pero destinadas a mantener registros de manera independiente por id de sesión. El estado de los objetos instanciados es trasient ya que al terminarse la sesión los datos se vuelven inusables.

Por defecto las entidades tipo SessionRecord localizan el campo sid que deben tener un tamaño suficiente para almacenar un valor de identificador de sesión. El valor de la variable de configuración de PHP session.hash_function establece el tipo de algoritmo utilizado para generar el identificador de sesión, si su valor es 0 indica que se usa md5 de 128 bits (35 caracteres) y cuando es 1 indica que es sha1 de 160 bits (40 caracteres).

La siguiente tabla contiene la estructura necesaria para utilizar una entidad con SessionRecord.
CREATE TABLE `cart` (
  `id` int(11) NOT NULL auto_increment,
  `sid` char(35) default NULL,
  `product_id` int(11) NOT NULL,
  `quantity` int(11) default NULL,
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=UTF8;


La definición de la clase de un modelo de este tipo es la normal pero en vez de heredar de ActiveRecord debe heredar de la clase SessionRecord. Las operaciones sobre el modelo se ejecutan normalmente con la diferencia que los registros grabados y devueltos siempre harán referencia a aquellos donde el session_id actual sea el del campo sid de la tabla.
PropertyAccessors y Mutators
ActiveRecord accede al valor de los atributos de la tabla al realizar una operación mediante los métodos readAttribute y writeAttribute heredados en cualquier modelo. El desarrollador puede sobreescribir estos atributos en la definición del módelo y controlar la forma en que obtienen/establecen internamente los valores de los atributos de la entidad.
Ejemplo: Sobreescribir un PropertyAccessor en un modelo
<?php

class Products extends ActiveRecord {

     protected function readAttribute($attributeName){
          if($attributeName=="very_private_field"){
               return null;
          } else {
               return $this->$attributeName;
          }
     }

}
DynamicUpdate y DynamicInsert
Un modelo tiene la propiedad de ahorrar trabajo al gestor relacional mediante estas opciones. En el primero al realizar una operación de actualización solo los campos que han cambiado en la base de datos son actualizados, en la segunda solo los campos que contienen valores no nulos se insertan en la operación.

Por defecto ambas propiedades estan desactivadas para cambiar su valor se deben usar los métodos protegidos setDynamicUpdate y setDynamicInsert respectivamente. Al activarlas en contra prestación por ejemplo al actualizar cada registro debe leerse antes de realizar la operación y compararar cada valor de los atributos con el del objeto actual.

Ejemplo: Establecer la creación de sentencias de inserción y actualización solo para los atributos del modelo que han cambiando
<?php

class Categories extends ActiveRecord {

     protected function initialize(){
          $this->setDynamicUpdate(true);
          $this->setDynamicInsert(true);
     }

}
Manejo de Excepciones
Las siguientes excepciones son generadas y asociadas a operaciones con ActiveRecord:
Tabla: Excepciones generadas cuando se trabaja con ActiveRecord
Excepción
ComponenteDescripción
DbExceptionDbExcepción generica lanzada por adaptador de conexión al gestor relacional utilizado.
DbLockAdquisitionExceptionDbExcepción lanzada cuando la transacción actual en la conexión no puede efectuar un bloqueo sobre algún recurso por ejemplo una tabla ó una serie de registros.
DbSQLGrammarExceptionDbExcepción lanzada cuando se envia una sentencia SQL mal formada ó con errores de sintaxis.
DbInvalidFormatExceptionDbExcepción lanzada cuando se trata de asignar un valor con un formato invalido al tipo de dato de la columna en una una entidad.
DbContraintViolationExceptionDbExcepción lanzada cuando la operación de modificación ó actualización viola un constraint de llave foránea.
ActiveRecordExceptionActiveRecordExcepción generica de ActiveRecord.
Capturar excepciones dentro de modelos
Si se requiere tratar las excepciones generadas dentro de un modelo en específico se puede sobreescribir el método protegido exceptions el cuál recibe las excepciones generadas y por defecto las re-lanza al controlador ó la vista donde se invocó el modelo.

En el siguiente ejemplo se ilustra como efectuar un procedimiento que trate las excepciones por violación de llaves foráneas para un modelo en particular:
Ejemplo: Tratar excepciones por violación de llave foránea
<?php

class Inventory extends ActiveRecord {

     protected function exceptions($e){
          if($e instanceof DbConstraintViolationException){
               //Algún procedimiento
          } else {
               throw $e;
          }
     }
}
Información de Excepciones
Las excepciones generadas por el adaptador al motor de base de datos usualmente permiten obtener mayor información sobre el entorno de la excepción agregando datos como el código y descripción de bajo nivel generadas por el RTP.

En el capítulo del componente Db se explica como obtener mayor información sobre excepciones generadas en la base de datos.
Plugins de Modelos
Los Plugins de modelos ó capa de datos permiten extender la funcionalidad del componente ActiveRecord y sus subcomponentes. La arquitectura de plugins de ActiveRecord permite observar, extender y manipular el comportamiento de los modelos de la aplicación según las condiciones lo exijan.

Los plugins permiten interceptar eventos de los modelos de tal forma que estos sean observables y además ejecutárse uno tras otro de manera centralizada sin requerir refactorización ó reintegración en la aplicación.
Crear un Plugin de ActiveRecord
Los plugins de ActiveRecord son clases que implementan eventos que son invocados a medida que avanza la ejecución del proceso de acceso a la capa de datos en cualquier petición. Estas clases deben cumplir con los siguientes requerimientos:

  • Deben estar ubicados en el directorio de plugins usualmente apps/app-name/plugins
  • El nombre del archivo que implementa la clase debe ser el nombre del plugin
  • El nombre de la clase debe tener la extensión Plugin
  • Los plugins de controlador deben heredar de la clase ModelPlugin ó ser subclase de ella

Las clases pueden implementar métodos públicos que referencian los eventos ocurridos en los controladores, la lista de ellos es la siguiente:
Tabla: Eventos que se pueden implementar en plugins de ActiveRecord
Nombre Evento
Descripción
afterInitializeOcurre después de inicializar un modelo. Normalmente los modelos son inicializados solo una vez en cada petición.
onExceptionOcurre cuando se genera una excepción dentro de un modelo.

Organización de Modelos
Los archivos de modelos deben estar ubicados en el directorio models/ ó donde la variable de configuración modelsDir lo indique. Cuando los modelos se cargan en modo auto-inicializador es posible organizarlos en subdirectorios de tal forma que representen un grupo ó categoría lógica al cual pertenezcan.

Un ejemplo de una organización lógica es el siguiente:
Ejemplo: Organización lógica de modelos
models/
     base/
          modelBase.php
     security/
          roles.php
          users.php
          access_list.php
     inventory/
          references.php
          kardex.php
     orders.php
     movement.php
Auto-inicialización de Modelos
Los modelos pueden inicializarse de 2 formas, automáticamente ó dinámicamente. Por defecto los modelos son auto-inicializados esto significa que en cada petición todas las clases del directorio de modelos son leídas y se construyen las relaciones y restricciones definidas en ellos.

El usar una forma ó la otra depende del tipo de aplicación que se tenga. Es recomendable usar auto-inicialización cuando:

  • El servidor donde está instalada la maquina tiene buenas prestaciones (discos duros rápidos, procesadores de última generación, etc).
  • Con relación a las prestaciones del servidor el número de modelos y su complejidad en cuanto a relaciones y restricciones es moderado ó bajo.
  • La aplicación requiere frecuentemente del acceso a la mayor parte de los modelos en la mayor parte de los procesos de negocio.

Desventajas de la auto-inicialización:

  • Dependiendo de las condiciones, el acceso a disco puede elevarse considerablemente en cada petición
  • Si los modelos tienen muchas relaciones y restricciones se podría aumentar el consumo de memoria innecesariamente

Se recomienda utilizar inicialización dinámica cuando:

  • El servidor tiene bajas prestaciones ó una concurrencia elevada
  • La aplicación requiere de modelos en forma selectiva sin que haya un patrón definido de acceso a ellos
  • El número de modelos de la aplicación es alto y hay muchas relaciones y restricciones entre ellas.

Desventajas de la inicialización dinámica:

  • Procesos que requieran varios modelos simultaneamente puede elevar los recursos de procesamiento solicitados por la aplicación
  • Solo los modelos inicializados son llevados a las vistas asociadas a la petición
Activar inicialización dinámica
Para activar la inicialización dinámica se debe agregar la sección entities con la variable de configuración autoInitialize = Off al archivo de configuración config/config.ini de la aplicación. Un archivo de configuración config.ini queda así:
Ejemplo: Definir autonicialización de modelos en la configuración
[application]
mode = development
name = "Project Name"
interactive = On
dbdate = YYYY-MM-DD
debug = On
[entities]
autoInitialize = Off