# Связи между таблицами Zend_Db_Table

## Введение

В реляционной БД таблицы имеют связи друг с другом. Запись в таблице может быть связана с одной или более записей в другой таблице с использованием ограничений ссылочной целостности, определенных в конкретной БД.

Класс Zend_Db_Table_Row имеет методы для запрашивания связанных строк в другой таблице.

## Определение связей

Определите классы для каждой из ваших таблиц, расширяя абстрактный класс Zend_Db_Table_Abstract, как описано в . Также смотрите описание БД, для которой написан приведнный ниже код в

Ниже приведено определение классов для этих таблиц:

    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')
            )
        );

    }

Если вы используете Zend_Db_Table для эмулирования каскадных операций обновления и удаления, то объявите массив `$_dependentTables` в классе для родительской таблицы. Перечислите имена классов всех зависимых таблиц. Используйте имена классов, а не таблиц в БД.

Пропустите объявление массива `$_dependentTables`, если используете ограничения ссылочной на сервере СУРБД для реализации каскадных операций. См. для получения более подробной информации.

Объявите массив `$_referenceMap` во всех классах зависимых таблиц. Это ассоциативный массив "правил связей". Правило связи определяет, какая таблица является родительской в конкретной связи, и какие столбцы в зависимой таблице ссылаются на какие столбцы в родительской таблице.

Ключом правила является строка, используемая как индекс массива `$_referenceMap`. Этот ключ правила используется для идентификации каждой связи. Выбирайте для него описательное имя. Лучше всего использовать строку, которая может быть частью имени метода, как вы увидите позднее.

В примере выше ключами правил являются: `'Reporter'`, `'Engineer'`, `'Verifier'`, и `'Product'`.

Значением каждого правила в массиве `$_referenceMap` является также ассоциативный массив. Элементы этого массива описаны ниже:

- columns => Строка или массив строк с имен(ем/ами) столбцов внешних ключей в зависимой таблице.

  Обычно это один столбец, но некоторые таблицы имеют составные ключи.

- refTableClass => Имя класса родительской таблицы. Используйте имя класса, а не таблицы в БД.

  Обычно зависимые таблицы имеют одну связь со своей родительской таблицей, но некоторые таблицы имеют множественные связи с одной и той же родительской таблицей. В базе данных, которую мы рассматриваем для примера, таблица `bugs` ссылается на таблицу `products`, но имеет также три связи с таблицей `accounts`. Помещайте каждую ссылку в отдельную запись в массиве `$_referenceMap`.

- refColumns => Строка или массив строк, в котором перечислены имена столбцов первичного ключа в родительской таблице.

  Обычно это один столбец, но некоторые таблицы имеют составные ключи. Если ссылка использует составной ключ, то порядок столбцов в элементе `'columns'` должен соответствовать порядку столбцов в элементе `'refColumns'`.

  Этот элемент является опциональным. Если вы не определите `refColumns`, то по умолчанию используются имена столбцов, объявленных как столбцы первичных ключей родительской таблицы.

- onDelete => Правило для действия, выполняемого, когда в родительской таблице удаляется строка. См. для получения более подробной информации.

- onUpdate => Правило для действия, выполняемого, когда изменяются значения в столбцах первичного ключа родительской таблицы. См. для получения более подробной информации.

## Извлечение зависимых строк

Если вы имеете объект Row (строка) в результате запроса к родительской таблице, то можете извлечь строки из зависимых таблиц, ссылающихся на текущую строку. Используйте следующий метод:

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

Этот метод возвращает объект Zend_Db_Table_Rowset_Abstract, содержащий набор строк из зависимой таблицы `$table`, ссылающихся на строку, представленную объектом `$row`.

Первый аргумент `$table` может быть строкой с именем класса зависимой таблицы. Вы можете также определить зависимую таблицу, используя объект класса этой таблицы.

### Извлечение зависимых строк

Этот пример демонстрирует получение объекта строки из таблицы `Accounts` и поиск ошибок в таблице `Bugs`, о которых сообщил данный пользователь.

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

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

Втрой аргумент `$rule` является опциональным. Это строка с ключом правила в массиве `$_referenceMap` класса зависимой таблицы. Если вы не определите правило, то будет использоваться первое правило из массива, ссылающееся на родительскую таблицу. Для того, чтобы использовать правило, отличное от первого, необходимо указать ключ.

В примере выше ключ правила не определен, поэтому используется первое правило, соответствующее родительской таблице. Это будет правило `'Reporter'`.

### Извлечение зависимых строк по определенному правилу

Этот пример демонстрирует получение строки из таблицы `Accounts` и поиск ошибок в таблице `Bugs`, устранение которых назначено данному пользователю. Ключ правила, соответствующий этой связи в данном примере - `'Engineer'`.

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

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

### Извлечение зависимых строк с использованием Zend_Db_Table_Select

Этот пример демонстрирует получение объекта строки из таблицы `Accounts` и поиск ошибок в таблице `Bugs`, устранение которых назначено данному пользователю. При этом извлекается не более 3-х строк и они должны быть отсортированы по имени.

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

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

Вы можете также запрашивать строки из зависимой таблицы, используя специальный механизм -так называемые "магические методы". Zend_Db_Table_Row_Abstract вызывает метод: `findDependentRowset('<TableClass>', '<Rule>')`, если вы вызываете метод объекта строки, соответствующий одному из следующих шаблонов:

- `$row->find<TableClass>()`

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

В этих шаблонах `<TableClass>` и `<Rule>` являются именем класса зависимой таблицы и ключом правила зависимой таблицы, ссылающегося на родительскую таблицу.

Некоторые фреймворки приложений, такие, как Ruby on Rails, используют механизм, называемый "инфлексией" (inflection), и состоящий в изменении написания идентификаторов в зависимости от использования. Для простоты Zend_Db_Table_Row не предоставляет никакого механизма инфлексии. Имя таблицы и ключ правила в вызовах методов должны в точности соответствовать написанию имени класса таблицы и ключа правила при объявлении.

### Извлечение зависимых строк с использованием магического метода

Этот пример демонстрирует поиск зависимых строк, эквивалентный тому, что был в предыдущих примерах. В данном случае приложение использует вызов магического метода вместо передачи имени таблицы и ключа правила в качестве аргументов.

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

    // Используется правило связи по умолчанию
    $bugsReportedBy = $user1234->findBugs();

    // Задается правило связи
    $bugsAssignedTo = $user1234->findBugsByEngineer();

## Извлечение родительской строки

Если вы имеете объект Row в результате запроса к зависимой таблице, то можете извлечь ту строку из родительской таблицы, на которую ссылается зависимая строка. Используйте метод:

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

Зависимая строка всегда должна ссылаться только на одну строку в родительской таблице, поэтому этот метод возвращает объект Row, а не Rowset.

Первый аргумент `$table` может быть строкой с именем класса родительской таблицы. Вы можете также задавать родительскую таблицу, используя объект класса этой таблицы.

### Извлечение родительской строки

Этот пример демонстрирует получение объекта Row из таблицы `Bugs` (для примера, одна из этих ошибок имеет статус 'NEW') и поиск строки в таблице `Accounts`, соответствующей пользователю, сообщившем об этой ошибке.

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

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

Второй аргумент `$rule` является опциональным. Это строка с ключом правила в массиве `$_referenceMap` класса зависимой таблицы. Если вы не определите правило, то будет использоваться первое правило в массиве, ссылающееся на родительскую таблицу. Для того, чтобы использовать правило, отличное от первого, необходимо указать ключ.

В примере кода выше ключ правила не определен, поэтому используется первое правило, соответствующее родительской таблице. Это будет правило `'Reporter'`.

### Извлечение родительской строки по определенному правилу

Этот пример демонстрирует получение объекта Row из таблицы `Bugs` и поиск аккаунта пользователя, которому назначено исправление этой ошибки. Ключ правила, соответствующего этой связи в данном примере - `'Engineer'`.

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

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

Вы можете также запрашивать строки из родительской таблицы, используя "магический метод". Zend_Db_Table_Row_Abstract вызывает метод: `findParentRow('<TableClass>', '<Rule>')`, если вы вызываете метод объекта Row, соответствующий одному из следующих шаблонов:

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

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

В этих шаблонах `<TableClass>` и `<Rule>` - соответственно имя класса родительской таблицы и ключ правила зависимой таблицы, ссылающегося на родительскую таблицу.

Имя таблицы и ключ правила в вызовах методов должны в точности соответствовать написанию имени класса таблицы и ключа правила при объявлении.

### Извлечение родительской строки с использованием магического метода

Этот пример демонстрирует поиск родительской строки, эквивалентный тому, что был в предыдущих примерах. В данном случае используется вызов магического метода вместо передачи имени таблицы и ключа правила в качестве аргументов.

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

    // Используется правило связи по умолчанию
    $reporter = $bug1->findParentAccounts();

    // Задается правило связи
    $engineer = $bug1->findParentAccountsByEngineer();

## Извлечение строк через связи "многие-ко-многим"

Если вы имеете объект Row в результате выполнения запроса к одной из таблиц, находящихся в связи "многие-ко-многим" (в рамках данного примера будем называть эту таблицу "исходной"), вы можете извлечь соответствующие строки в другой таблице ("целевая" таблица) через таблицу пересечений. Используйте метод:

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

Этот метод возвращает объект Zend_Db_Table_Rowset_Abstract, содержащий строки из таблицы `$table`, соответствующие связи "многие-ко-многим". Текущий объект строки `$row` исходной таблицы используется в поиске строк в таблице пересечений и производится объединение с целевой таблицей.

Первый аргумент `$table` может быть именем класса целевой таблицы в связи "многие-ко-многим". Вы можете также задавать целевую таблицу, используя объект класса этой таблицы.

Второй аргумент `$intersectionTable` может быть именем класса таблицы пересечений между двумя таблицами в связи "многие-ко-многим". Вы можете также задавать таблицу пересечений, используя объект класса этой таблицы.

### Извлечение строк через метод для связей "многие-ко-многим"

Этот пример демонстрирует получение объекта Row из исходной таблицы `Accounts` и поиск строк в целевой таблице `Products`, соответствующих продуктам, об ошибках в которых сообщил этот пользователь.

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

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

Третий и четвертый аргументы - `$rule1` и `$rule2` - являются опциональными. Это строки с ключами правил в массиве `$_referenceMap` класса таблицы пересечений.

`$rule1` должен содержать ключ правила для ссылок таблицы пересечений на исходную таблицу. В данном примере это связь между таблицами `BugsProducts` и `Bugs`.

`$rule2` должен содержать ключ правила для ссылок таблицы пересечений на целевую таблицу. В данном примере это связь между таблицами `Bugs` и `Products`

Как и в случае использования методов для извлечения родительских и зависимых строк, если вы не зададите правило, то метод использует первое правило в массиве `$_referenceMap`, соответствующее таблицам в связи. Если нужно использовать другое правило, то необходимо указать ключ.

В примере кода выше ключ правила не указан, поэтому по умолчанию используются первые подходящие правила из массива. В данном случае для правила `$rule1` будет использоваться `'Reporter'`, для правила `$rule2` - `'Product'`.

### Извлечение строк через метод для связей "многие-ко-многим" по определенному правилу

Этот пример демонстрирует получение объекта Row из исходной таблицы `Bugs` и поиск строк в целевой таблице, `Products`, соответствующих продуктам, к которым относится данная ошибка.

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

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

Вы можете также запрашивать строки из целевой таблицы в связи "многие-ко-многим", используя "магический метод". Zend_Db_Table_Row_Abstract вызывает метод `findManyToManyRowset('<TableClass>', '<IntersectionTableClass>', '<Rule1>', '<Rule2>')`, если вы вызываете метод, соотвествующий одному из следующих шаблонов:

- `$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])`

В этих шаблонах `<TableClass>` и `<IntersectionTableClass>` являются именами классов целевой таблицы и таблицы пересечений соответственно. `<Rule1>` и `<Rule2>` являются ключами правил в таблице пересечений, соответствующими исходной таблице и целевой таблице, соответственно.

Имя таблицы и ключ правила в вызовах методов должны в точности соответствовать написанию имени класса таблицы и ключа правила при объявлении.

### Извлечение строк с использованием магического метода для связей "многие-ко-многим"

Этот пример демонстрирует поиск в целевой таблице в связи "многие-ко многим" строк, соответствующих продуктам, к которым относится данная ошибка.

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

    // Используется правило связи по умолчанию
    $products = $bug1234->findProductsViaBugsProducts();

    // Задается правило связи
    $products = $bug1234->findProductsViaBugsProductsByBug();

## Каскадные операции записи

### Объявление DRI в БД

Объявление каскадных операций в Zend_Db_Table предназначено только для тех СУРБД, которые не поддерживают декларативной ссылочной целостности (declarative referential integrity - сокр. DRI).

Например, если вы используете механизм хранения MyISAM в MySQL или SQLite, не поддерживающие DRI, то для вас может быть полезным объявить каскадные операции через Zend_Db_Table.

Если ваша СУРБД реализует DRI и поддерживает предложения `ON DELETE` и `ON UPDATE`, то вам следует объявить эти предложения в вашей БД вместо использования каскадных возможностей Zend_Db_Table. Объявление каскадных правил DRI в СУРБД лучше в плане производительности, стабильности работы с БД и целостности данных.

Тем более, не объявляйте каскадные операции одновременно в СУРБД и в классе Zend_Db_Table.

Вы можете объявить каскадные операции для их выполнения в зависимой таблице при применении операций `UPDATE` и `DELETE` к строкам в родительской таблице.

### Пример каскадного удаления

Этот пример демонстрирует удаление строки в таблице `Products`, которая была сконфигурирована для автоматического удаления зависимых строк в таблице `Bugs`.

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

    $product1234->delete();
    // Автоматически выполняется каскадное
    // удаление зависимых строк в таблице Bugs

Аналогично, если вы используете `UPDATE` для изменения значения первичного ключа в родительской таблице, то при этом часто требуется, чтобы значение внешнего ключа в зависимой таблице также изменялось на новое, и таким образом поддерживалась ссылочная целостность.

Обычно нет необходимости в том, чтобы изменять значение первичного ключа, которое генерируется последовательностью (sequence) или другим механизмом. Но если вы используете естетственные ключи, которые иногда могут изменять свое значение, то, скорее всего, нужно будет использовать каскадное обновление зависимых таблиц.

Для объявления каскадных связей в Zend_Db_Table отредактируйте правила в массиве `$_referenceMap`. Установите в ассоциативного массиве под ключами `'onDelete'` и `'onUpdate'` значение 'cascade' (или константу `self::CASCADE`). До того, как строка будет удалена из родительской таблицы или изменится значение ее первичного ключа, будут удалены или обновлены любые строки в зависимой таблице, ссылающиеся на эту строку родительской таблицы.

### Пример объявления каскадных операций

В примере ниже строки в таблице `Bugs` автоматически удаляются, если строка в таблице `Products`, на которую они ссылаются, удаляется. Элемент `'onDelete'` записи в массиве связей установлен в `self::CASCADE`.

В примере ниже не выполняется каскадное обновление, если изменяется значение первичного ключа. Элемент `'onUpdate'` записи в массиве связей установлен в `self::RESTRICT`. Вы можете получить тот же самый результат, используя значение `self::NO_ACTION` или пропустив элемент `'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
            ),
            ...
        );
    }

### Примечания относительно каскадных операций

Каскадные операции, вызываемые Zend_Db_Table, не являются атомарными.

Это означает, что если ваша БД реализует ограничения ссылочной целостности и принуждает к их использованию, то каскадное обновление, выполняемое классом Zend_Db_Table, конфликтует с этими ограничениями и результатом будет нарушение ссылочной целостности. Вы можете использовать каскадное обновление в Zend_Db_Table только если когда ваша БД не принуждает к использованию ограничений ссылочной целостности.

Каскадное удаление меньше страдает от проблем нарушения ссылочной целостности. Вы можете удалить зависимые строки в неатомарном действии до удаления родительской строки, на которую они ссылаются.

Тем не менее, неатомарность операций изменения и удаления в БД приводит к тому, что есть риск того, что другой пользователь БД будет видеть противоречивые данные. Например, если вы удалите строку и все зависимые строки, то есть небольшая вероятность того, что другой клиент может сделать запрос к БД после того, как вы удалили зависимые строки, но до того, как вы удалите родительскую строку. Эта клиентская программа может увидеть родительскую строку без зависимых строк и решить, что это задуманное состояние данных.

Проблема неатомарных измнений может быть частично решена использованием транзакций для изолирования ваших изменений. Но некоторые СУРБД не поддерживают транзакции или позволяют клиентам читать "грязные" изменения в БД, которые не были еще зафиксированы.

Каскадные операции в Zend_Db_Table вызываются только классом Zend_Db_Table

Каскадные операции удаления и добавления, объявленные в ваших классах Zend_Db_Table выполняются, если вы выполняете методы `save()` и `delete()` класса Row. Но если вы обновляете или удаляете данные, используя другой интерфейс, например, утилиту запросов или другое приложение, то каскадные операции не выполняются. Даже когда используются методы `update()` и `delete()` класса Zend_Db_Adapter, каскадные операции, определенные в ваших классах Zend_Db_Table, не выполняются.

Не существует каскадного добавления `INSERT`.

Не поддерживается каскадное добавление `INSERT`. Вы должны добавить строку в родительской таблице в одной операции и добавить строки в зависимой таблице в другой операции.