TigerZF
🌐Español

27.8. Relaciones de Zend_Db_Table

27.8.1. Introducción

Las tablas tienen relaciones entre sí en una base de datos relacional. Una entidad en una tabla puede estar vinculada a una o más entidades en otra tabla mediante el uso de restricciones de integridad referencial definidas en el esquema de la base de datos.

La clase Zend_Db_Table_Row tiene métodos para consultar filas relacionadas en otras tablas.

27.8.2. Definición de relaciones

Defina clases para cada una de sus tablas, extendiendo la clase abstracta Zend_Db_Table_Abstract, tal como se describe en este capítulo. Vea también este capítulo para una descripción de la base de datos de ejemplo para la cual está diseñado el siguiente código de ejemplo.

A continuación se muestran las definiciones de clase de PHP para estas tablas:

class Accounts extends Zend_Db_Table_Abstract
{
    protected $_name            = 'accounts';
    protected $_dependentTables = array('Bugs');
}

class Products extends Zend_Db_Table_Abstract
{
    protected $_name            = 'products';
    protected $_dependentTables = array('BugsProducts');
}

class Bugs extends Zend_Db_Table_Abstract
{
    protected $_name            = 'bugs';

    protected $_dependentTables = array('BugsProducts');

    protected $_referenceMap    = array(
        'Reporter' => array(
            'columns'           => 'reported_by',
            'refTableClass'     => 'Accounts',
            'refColumns'        => 'account_name'
        ),
        'Engineer' => array(
            'columns'           => 'assigned_to',
            'refTableClass'     => 'Accounts',
            'refColumns'        => 'account_name'
        ),
        'Verifier' => array(
            'columns'           => array('verified_by'),
            'refTableClass'     => 'Accounts',
            'refColumns'        => array('account_name')
        )
    );
}

class BugsProducts extends Zend_Db_Table_Abstract
{
    protected $_name = 'bugs_products';

    protected $_referenceMap    = array(
        'Bug' => array(
            'columns'           => array('bug_id'),
            'refTableClass'     => 'Bugs',
            'refColumns'        => array('bug_id')
        ),
        'Product' => array(
            'columns'           => array('product_id'),
            'refTableClass'     => 'Products',
            'refColumns'        => array('product_id')
        )
    );

}

Si utiliza Zend_Db_Table para emular operaciones en cascada de UPDATE y DELETE, declare el array $_dependentTables en la clase de la tabla padre. Enumere el nombre de clase de cada tabla dependiente. Use el nombre de clase, no el nombre físico de la tabla SQL.

[Note] Nota

Omita la declaración de $_dependentTables si utiliza restricciones de integridad referencial en el servidor RDBMS para implementar operaciones en cascada. Consulte este capítulo para más información.

Declare el array $_referenceMap en la clase de cada tabla dependiente. Este es un array asociativo de "reglas" de referencia. Una regla de referencia identifica cuál es la tabla padre en la relación, y también enumera qué columnas de la tabla dependiente hacen referencia a qué columnas de la tabla padre.

La clave de la regla es una cadena que se usa como índice del array $_referenceMap. Esta clave de regla se utiliza para identificar cada relación de referencia. Elija un nombre descriptivo para esta clave de regla. Es recomendable usar una cadena que pueda formar parte de un nombre de método PHP, como verá más adelante.

En el código PHP del ejemplo anterior, las claves de regla en la clase de la tabla Bugs son: 'Reporter', 'Engineer', 'Verifier' y 'Product'.

El valor de cada entrada de regla en el array $_referenceMap es también un array asociativo. Los elementos de esta entrada de regla se describen a continuación:

  • columns => Una cadena o un array de cadenas que nombran las columnas de clave foránea en la tabla dependiente.

    Es común que sea una sola columna, pero algunas tablas tienen claves de múltiples columnas.

  • refTableClass => El nombre de clase de la tabla padre. Use el nombre de clase, no el nombre físico de la tabla SQL.

    Es común que una tabla dependiente tenga solo una referencia a su tabla padre, pero algunas tablas tienen múltiples referencias a la misma tabla padre. En la base de datos de ejemplo, hay una referencia de la tabla bugs a la tabla products, pero tres referencias de la tabla bugs a la tabla accounts. Coloque cada referencia en una entrada separada del array $_referenceMap.

  • refColumns => Una cadena o un array de cadenas que nombran las columnas de clave primaria en la tabla padre.

    Es común que sea una sola columna, pero algunas tablas tienen claves de múltiples columnas. Si la referencia usa una clave de múltiples columnas, el orden de las columnas en la entrada 'columns' debe coincidir con el orden de las columnas en la entrada 'refColumns'.

    [Note] Nota

    Se recomienda que el elemento refColumns se declare siempre, ya que las operaciones en cascada no funcionarán a menos que lo haga.

  • onDelete => La regla para una acción a ejecutar si se elimina una fila en la tabla padre. Consulte este capítulo para más información.

  • onUpdate => La regla para una acción a ejecutar si se actualizan los valores de las columnas de clave primaria en la tabla padre. Consulte este capítulo para más información.

27.8.3. Obtención de un rowset dependiente

Si tiene un objeto Row como resultado de una consulta sobre una tabla padre, puede obtener filas de tablas dependientes que hacen referencia a la fila actual. Use el método:

$row->findDependentRowset($table, [$rule]);

Este método devuelve un objeto Zend_Db_Table_Rowset_Abstract, que contiene un conjunto de filas de la tabla dependiente $table que hacen referencia a la fila identificada por el objeto $row.

El primer argumento $table puede ser una cadena que especifica la tabla dependiente por su nombre de clase. También puede especificar la tabla dependiente usando un objeto de esa clase de tabla.

Ejemplo 27.145. Obtención de un rowset dependiente

Este ejemplo muestra cómo obtener un objeto Row de la tabla Accounts, y encontrar los Bugs reportados por esa cuenta.

$accountsTable = new Accounts();
$accountsRowset = $accountsTable->find(1234);
$user1234 = $accountsRowset->current();

$bugsReportedByUser = $user1234->findDependentRowset('Bugs');

El segundo argumento $rule es opcional. Es una cadena que nombra la clave de regla en el array $_referenceMap de la clase de la tabla dependiente. Si no especifica una regla, se utiliza la primera regla del array que hace referencia a la tabla padre. Si necesita usar una regla distinta de la primera, debe especificar la clave.

En el código de ejemplo anterior, no se especifica la clave de regla, por lo que la regla utilizada por defecto es la primera que coincide con la tabla padre. Esta es la regla 'Reporter'.

Ejemplo 27.146. Obtención de un rowset dependiente mediante una regla específica

Este ejemplo muestra cómo obtener un objeto Row de la tabla Accounts, y encontrar los Bugs asignados para ser corregidos por el usuario de esa cuenta. La cadena de la clave de regla que corresponde a esta relación de referencia en este ejemplo es 'Engineer'.

$accountsTable = new Accounts();
$accountsRowset = $accountsTable->find(1234);
$user1234 = $accountsRowset->current();

$bugsAssignedToUser = $user1234->findDependentRowset('Bugs', 'Engineer');

También puede agregar criterios, ordenamiento y límites a sus relaciones usando el objeto select de la fila padre.

Ejemplo 27.147. Obtención de un rowset dependiente usando un Zend_Db_Table_Select

Este ejemplo muestra cómo obtener un objeto Row de la tabla Accounts, y encontrar los Bugs asignados para ser corregidos por el usuario de esa cuenta, limitados solo a 3 filas y ordenados por nombre.

$accountsTable = new Accounts();
$accountsRowset = $accountsTable->find(1234);
$user1234 = $accountsRowset->current();
$select = $accountsTable->select()->order('name ASC')
                                  ->limit(3);

$bugsAssignedToUser = $user1234->findDependentRowset('Bugs',
                                                     'Engineer',
                                                     $select);

Alternativamente, puede consultar filas de una tabla dependiente usando un mecanismo especial llamado "método mágico". Zend_Db_Table_Row_Abstract invoca el método: findDependentRowset('<TableClass>', '<Rule>') si invoca un método en el objeto Row que coincida con alguno de los siguientes patrones:

  • $row->find<TableClass>()

  • $row->find<TableClass>By<Rule>()

En los patrones anteriores, <TableClass> y <Rule> son cadenas que corresponden al nombre de clase de la tabla dependiente, y a la clave de regla de la tabla dependiente que hace referencia a la tabla padre.

[Note] Nota

Algunos frameworks de aplicaciones, como Ruby on Rails, utilizan un mecanismo llamado "inflexión" para permitir que la ortografía de los identificadores cambie según el uso. Por simplicidad, Zend_Db_Table_Row no proporciona ningún mecanismo de inflexión. La identidad de la tabla y la clave de regla nombradas en la llamada al método deben coincidir exactamente con la ortografía de la clase y la clave de regla.

Ejemplo 27.148. Obtención de rowsets dependientes usando el método mágico

Este ejemplo muestra cómo encontrar rowsets dependientes equivalentes a los de los ejemplos anteriores. En este caso, la aplicación usa la invocación del método mágico en lugar de especificar la tabla y la regla como cadenas.

$accountsTable = new Accounts();
$accountsRowset = $accountsTable->find(1234);
$user1234 = $accountsRowset->current();

// Use the default reference rule
$bugsReportedBy = $user1234->findBugs();

// Specify the reference rule
$bugsAssignedTo = $user1234->findBugsByEngineer();

27.8.4. Obtención de una fila padre

Si tiene un objeto Row como resultado de una consulta sobre una tabla dependiente, puede obtener la fila en el padre a la que hace referencia la fila dependiente. Use el método:

$row->findParentRow($table, [$rule]);

Siempre debe existir exactamente una fila en la tabla padre referenciada por una fila dependiente, por lo tanto este método devuelve un objeto Row, no un objeto Rowset.

El primer argumento $table puede ser una cadena que especifica la tabla padre por su nombre de clase. También puede especificar la tabla padre usando un objeto de esa clase de tabla.

Ejemplo 27.149. Obtención de la fila padre

Este ejemplo muestra cómo obtener un objeto Row de la tabla Bugs (por ejemplo, uno de esos bugs con estado 'NEW'), y encontrar la fila en la tabla Accounts del usuario que reportó el bug.

$bugsTable = new Bugs();
$bugsRowset = $bugsTable->fetchAll(array('bug_status = ?' => 'NEW'));
$bug1 = $bugsRowset->current();

$reporter = $bug1->findParentRow('Accounts');

El segundo argumento $rule es opcional. Es una cadena que nombra la clave de regla en el array $_referenceMap de la clase de la tabla dependiente. Si no especifica una regla, se utiliza la primera regla del array que hace referencia a la tabla padre. Si necesita usar una regla distinta de la primera, debe especificar la clave.

En el ejemplo anterior, no se especifica la clave de regla, por lo que la regla utilizada por defecto es la primera que coincide con la tabla padre. Esta es la regla 'Reporter'.

Ejemplo 27.150. Obtención de una fila padre mediante una regla específica

Este ejemplo muestra cómo obtener un objeto Row de la tabla Bugs, y encontrar la cuenta del ingeniero asignado para corregir ese bug. La cadena de la clave de regla que corresponde a esta relación de referencia en este ejemplo es 'Engineer'.

$bugsTable = new Bugs();
$bugsRowset = $bugsTable->fetchAll(array('bug_status = ?', 'NEW'));
$bug1 = $bugsRowset->current();

$engineer = $bug1->findParentRow('Accounts', 'Engineer');

Alternativamente, puede consultar filas de una tabla padre usando un "método mágico". Zend_Db_Table_Row_Abstract invoca el método: findParentRow('<TableClass>', '<Rule>') si invoca un método en el objeto Row que coincida con alguno de los siguientes patrones:

  • $row->findParent<TableClass>([Zend_Db_Table_Select $select])

  • $row->findParent<TableClass>By<Rule>([Zend_Db_Table_Select $select])

En los patrones anteriores, <TableClass> y <Rule> son cadenas que corresponden al nombre de clase de la tabla padre, y a la clave de regla de la tabla dependiente que hace referencia a la tabla padre.

[Note] Nota

La identidad de la tabla y la clave de regla nombradas en la llamada al método deben coincidir con la ortografía de la clase y la clave de regla exactamente.

Ejemplo 27.151. Obtención de la fila padre usando el método mágico

Este ejemplo muestra cómo encontrar filas padre equivalentes a las de los ejemplos anteriores. En este caso, la aplicación usa la invocación del método mágico en lugar de especificar la tabla y la regla como cadenas.

$bugsTable = new Bugs();
$bugsRowset = $bugsTable->fetchAll(array('bug_status = ?', 'NEW'));
$bug1 = $bugsRowset->current();

// Use the default reference rule
$reporter = $bug1->findParentAccounts();

// Specify the reference rule
$engineer = $bug1->findParentAccountsByEngineer();

27.8.5. Obtención de un rowset a través de una relación de muchos a muchos

Si tiene un objeto Row como resultado de una consulta sobre una tabla en una relación de muchos a muchos (a efectos del ejemplo, llame a esta la tabla "origen"), puede obtener las filas correspondientes en la otra tabla (llame a esta la tabla "destino") a través de una tabla de intersección. Use el método:

$row->findManyToManyRowset($table,
                           $intersectionTable,
                           [$rule1,
                               [$rule2,
                                   [Zend_Db_Table_Select $select]
                               ]
                           ]);

Este método devuelve un Zend_Db_Table_Rowset_Abstract que contiene filas de la tabla $table, que satisfacen la relación de muchos a muchos. El objeto Row actual $row de la tabla origen se usa para encontrar filas en la tabla de intersección, que se une a la tabla destino.

El primer argumento $table puede ser una cadena que especifica la tabla destino en la relación de muchos a muchos por su nombre de clase. También puede especificar la tabla destino usando un objeto de esa clase de tabla.

El segundo argumento $intersectionTable puede ser una cadena que especifica la tabla de intersección entre las dos tablas en la relación de muchos a muchos por su nombre de clase. También puede especificar la tabla de intersección usando un objeto de esa clase de tabla.

Ejemplo 27.152. Obtención de un rowset con el método de muchos a muchos

Este ejemplo muestra cómo obtener un objeto Row de la tabla origen Bugs, y encontrar filas de la tabla destino Products, que representan los productos relacionados con ese bug.

$bugsTable = new Bugs();
$bugsRowset = $bugsTable->find(1234);
$bug1234 = $bugsRowset->current();

$productsRowset = $bug1234->findManyToManyRowset('Products',
                                                 'BugsProducts');

El tercer y cuarto argumento $rule1 y $rule2 son opcionales. Son cadenas que nombran las claves de regla en el array $_referenceMap de la tabla de intersección.

La clave $rule1 nombra la regla para la relación desde la tabla de intersección hacia la tabla origen. En este ejemplo, esta es la relación de BugsProducts a Bugs.

La clave $rule2 nombra la regla para la relación desde la tabla de intersección hacia la tabla destino. En este ejemplo, esta es la relación de Bugs a Products.

De forma similar a los métodos para encontrar filas padre y dependientes, si no especifica una regla, el método utiliza la primera regla en el array $_referenceMap que coincide con las tablas en la relación. Si necesita usar una regla distinta de la primera, debe especificar la clave.

En el código de ejemplo anterior, no se especifica la clave de regla, por lo que las reglas utilizadas por defecto son las primeras que coinciden. En este caso, $rule1 es 'Reporter' y $rule2 es 'Product'.

Ejemplo 27.153. Obtención de un rowset con el método de muchos a muchos mediante una regla específica

Este ejemplo muestra cómo obtener un objeto Row de la tabla origen Bugs, y encontrar filas de la tabla destino Products, que representan los productos relacionados con ese bug.

$bugsTable = new Bugs();
$bugsRowset = $bugsTable->find(1234);
$bug1234 = $bugsRowset->current();

$productsRowset = $bug1234->findManyToManyRowset('Products',
                                                 'BugsProducts',
                                                 'Bug');

Alternativamente, puede consultar filas de la tabla destino en una relación de muchos a muchos usando un "método mágico". Zend_Db_Table_Row_Abstract invoca el método: findManyToManyRowset('<TableClass>', '<IntersectionTableClass>', '<Rule1>', '<Rule2>') si invoca un método que coincida con cualquiera de los siguientes patrones:

  • $row->find<TableClass>Via<IntersectionTableClass> ([Zend_Db_Table_Select $select])

  • $row->find<TableClass>Via<IntersectionTableClass>By<Rule1> ([Zend_Db_Table_Select $select])

  • $row->find<TableClass>Via<IntersectionTableClass>By<Rule1>And<Rule2> ([Zend_Db_Table_Select $select])

En los patrones anteriores, <TableClass> y <IntersectionTableClass> son cadenas que corresponden a los nombres de clase de la tabla destino y de la tabla de intersección, respectivamente. <Rule1> y <Rule2> son cadenas que corresponden a las claves de regla en la tabla de intersección que hacen referencia a la tabla origen y a la tabla destino, respectivamente.

[Note] Nota

Las identidades de tabla y las claves de regla nombradas en la llamada al método deben coincidir con la ortografía de la clase y la clave de regla exactamente.

Ejemplo 27.154. Obtención de rowsets usando el método mágico de muchos a muchos

Este ejemplo muestra cómo encontrar filas en la tabla destino de una relación de muchos a muchos que representan los productos relacionados con un bug determinado.

$bugsTable = new Bugs();
$bugsRowset = $bugsTable->find(1234);
$bug1234 = $bugsRowset->current();

// Use the default reference rule
$products = $bug1234->findProductsViaBugsProducts();

// Specify the reference rule
$products = $bug1234->findProductsViaBugsProductsByBug();

27.8.6. Operaciones de escritura en cascada

[Note] Declare la DRI en la base de datos:

La declaración de operaciones en cascada en Zend_Db_Table está pensada únicamente para las marcas de RDBMS que no admiten integridad referencial declarativa (DRI).

Por ejemplo, si utiliza el motor de almacenamiento MyISAM de MySQL o MariaDB, o SQLite, estas soluciones no admiten DRI. Puede resultarle útil declarar las operaciones en cascada con Zend_Db_Table.

Si su RDBMS implementa DRI y las cláusulas ON DELETE y ON UPDATE, debería declarar estas cláusulas en el esquema de su base de datos, en lugar de usar la función de cascada de Zend_Db_Table. Declarar las reglas de DRI en cascada en el RDBMS es mejor para el rendimiento, la consistencia y la integridad de la base de datos.

Lo más importante: no declare operaciones en cascada tanto en el RDBMS como en su clase Zend_Db_Table.

Puede declarar operaciones en cascada para ejecutar sobre una tabla dependiente cuando aplica un UPDATE o un DELETE a una fila en una tabla padre.

Ejemplo 27.155. Ejemplo de una eliminación en cascada

Este ejemplo muestra cómo eliminar una fila en la tabla Products, que está configurada para eliminar automáticamente las filas dependientes en la tabla Bugs.

$productsTable = new Products();
$productsRowset = $productsTable->find(1234);
$product1234 = $productsRowset->current();

$product1234->delete();
// Automatically cascades to Bugs table
// and deletes dependent rows.

De forma similar, si usa UPDATE para cambiar el valor de una clave primaria en una tabla padre, es posible que desee que el valor de las claves foráneas de las tablas dependientes se actualice automáticamente para que coincida con el nuevo valor, de modo que dichas referencias se mantengan actualizadas.

Por lo general, no es necesario actualizar el valor de una clave primaria que fue generada por una secuencia u otro mecanismo. Pero si utiliza una clave natural que puede cambiar de valor ocasionalmente, es más probable que necesite aplicar actualizaciones en cascada a las tablas dependientes.

Para declarar una relación en cascada en Zend_Db_Table, edite las reglas en $_referenceMap. Establezca las claves del array asociativo 'onDelete' y 'onUpdate' con una de estas opciones:

  • Cascade: Esta opción configura una cascada de un solo nivel (la tabla padre más todas las tablas directamente dependientes). Para habilitar esta opción, establezca la clave correspondiente en $_referenceMap con la cadena 'cascade' o use la constante self::CASCADE.

  • Recursive Cascade: Esta opción configura una cascada recursiva completa comenzando por la tabla padre. Para habilitar esta opción, establezca la clave correspondiente en $_referenceMap con la cadena 'cascadeRecurse' o use la constante self::CASCADE_RECURSE.

Antes de que se elimine una fila de la tabla padre, o se actualicen sus valores de clave primaria, se eliminan o actualizan primero las filas en la tabla dependiente que hacen referencia a la fila del padre.

Ejemplo 27.156. Ejemplo de declaración de operaciones en cascada

En el ejemplo siguiente, las filas de la tabla Bugs se eliminan automáticamente si se elimina la fila de la tabla Products a la que hacen referencia. El elemento 'onDelete' de la entrada del mapa de referencias se establece en self::CASCADE.

No se realiza ninguna actualización en cascada en el ejemplo siguiente si se cambia el valor de la clave primaria en la clase padre. El elemento 'onUpdate' de la entrada del mapa de referencias es self::RESTRICT. Puede obtener el mismo resultado omitiendo la entrada 'onUpdate'.

class BugsProducts extends Zend_Db_Table_Abstract
{
    ...
    protected $_referenceMap = array(
        'Product' => array(
            'columns'           => array('product_id'),
            'refTableClass'     => 'Products',
            'refColumns'        => array('product_id'),
            'onDelete'          => self::CASCADE,
            'onUpdate'          => self::RESTRICT
        ),
        ...
    );
}

27.8.6.1. Notas sobre las operaciones en cascada

Las operaciones en cascada invocadas por Zend_Db_Table no son atómicas.

Esto significa que si su base de datos implementa y aplica restricciones de integridad referencial, un UPDATE en cascada ejecutado por una clase Zend_Db_Table entra en conflicto con la restricción, y da lugar a una violación de integridad referencial. Puede usar UPDATE en cascada en Zend_Db_Table únicamente si su base de datos no aplica esa restricción de integridad referencial.

El DELETE en cascada sufre menos el problema de violaciones de integridad referencial. Puede eliminar las filas dependientes como una acción no atómica antes de eliminar la fila padre a la que hacen referencia.

Sin embargo, tanto para UPDATE como para DELETE, cambiar la base de datos de forma no atómica también crea el riesgo de que otro usuario de la base de datos vea los datos en un estado inconsistente. Por ejemplo, si elimina una fila y todas sus filas dependientes, existe una pequeña posibilidad de que otro programa cliente de la base de datos consulte la base de datos después de que usted haya eliminado las filas dependientes, pero antes de eliminar la fila padre. Ese programa cliente puede ver la fila padre sin filas dependientes, y suponer que este es el estado deseado de los datos. No hay forma de que ese cliente sepa que su consulta leyó la base de datos en medio de un cambio.

El problema del cambio no atómico se puede mitigar usando transacciones para aislar su cambio. Pero algunas marcas de RDBMS no admiten transacciones, o permiten a los clientes leer cambios "sucios" que aún no se han confirmado.

Las operaciones en cascada de Zend_Db_Table solo se invocan mediante Zend_Db_Table.

Las eliminaciones y actualizaciones en cascada definidas en sus clases Zend_Db_Table se aplican si ejecuta los métodos save() o delete() en la clase Row. Sin embargo, si actualiza o elimina datos usando otra interfaz, como una herramienta de consulta u otra aplicación, las operaciones en cascada no se aplican. Incluso cuando se usan los métodos update() y delete() de la clase Zend_Db_Adapter, las operaciones en cascada definidas en sus clases Zend_Db_Table no se ejecutan.

Sin INSERT en cascada.

No hay soporte para un INSERT en cascada. Debe insertar una fila en una tabla padre en una operación, e insertar filas en una tabla dependiente en una operación separada.