В реляционной БД таблицы имеют связи друг с другом. Запись в таблице может быть связана с одной или более записей в другой таблице с использованием ограничений ссылочной целостности, определенных в конкретной БД.
Класс Zend_Db_Table_Row имеет методы для запрашивания связанных строк в другой таблице.
В этом разделе мы будем использовать для примера базу данных, которая служит для отслеживания ошибок в проекте разработки ПО. Эта база данных содержит три таблицы:
Accounts
(учетные записи) хранит информацию
о всех пользователях системы отслеживания ошибок.
Products
(продукты) хранит информацию о всех
продуктах, для которых могут вестись журналы ошибок.
Bugs
(ошибки) хранит информацию об ошибках,
включая: продукт, в котором обнаружена ошибка, лицо,
сообщившее об ошибке, лицо, которому назначено устранение
ошибки и лицо, которому назначена проверка устранения
ошибки.
Следующий псевдокод для определения данных SQL описывает таблицу в этой БД.
CREATE TABLE accounts ( account_id PRIMARY KEY, name VARCHAR ); CREATE TABLE products ( product_id PRIMARY KEY, product_name VARCHAR ); CREATE TABLE bugs ( bug_id PRIMARY KEY, product_id FOREIGN KEY REFERENCES products(product_id), reported_by FOREIGN KEY REFERENCES accounts(account_id), assigned_to FOREIGN KEY REFERENCES accounts(account_id), verified_by FOREIGN KEY REFERENCES accounts(account_id), bug_description VARCHAR, bug_status VARCHAR );
Таблица products
имеет связь "один-ко-многим" с
таблицей bugs
.
Таблица accounts
также имеет связь "один-ко-многим" с
таблицей bugs
.
Таблица products
имеет связь "многие-ко-многим" с
таблицей accounts
.
Также обратите внимание, что таблица bugs
содержит
несколько внешних ключей, ссылающихся на таблицу
accounts
. Для данной ошибки эти внешние ключи могут
ссылаться на разные строки в таблице accounts
.
Определите классы для каждой из этих таблиц, расширяя абстрактный
класс Zend_Db_Table_Abstract, как описано в Раздел 9.5.2, «Определение класса таблицы». В классах объявите связи между
таблицами в массивах protected $_referenceMap
и protected $_dependentTables
.
Ниже приведено определение классов для этих таблиц:
<?php class Accounts extends Zend_Db_Table_Abstract { protected $_name = 'accounts'; protected $_primary = array('account_id'); protected $_dependentTables = array('Bugs'); } class Products extends Zend_Db_Table_Abstract { protected $_name = 'products'; protected $_primary = array('product_id'); protected $_dependentTables = array('Bugs'); } class Bugs extends Zend_Db_Table_Abstract { protected $_name = 'bugs'; protected $_referenceMap = array( 'Reporter' => array( 'columns' => 'reported_by', 'refTableClass' => 'Accounts', 'refColumns' => 'account_id' ), 'Engineer' => array( 'columns' => 'assigned_to', 'refTableClass' => 'Accounts', 'refColumns' => 'account_id' ), 'Verifier' => array( 'columns' => array('verified_by'), 'refTableClass' => 'Accounts', 'refColumns' => array('account_id') ), 'Product' => array( 'columns' => array('product_id'), 'refTableClass' => 'Products', 'refColumns' => array('product_id') ) ); } ?>
Объявите массив $_dependentTables
в классе для
родительской таблицы. Перечислите имена классов всех зависимых
таблиц. Используйте имена классов, а не таблиц в БД.
Объявите массив $_referenceMap
во всех классах
зависимых таблиц. Это ассоциативный массив правил связей. Правило
связи определяет, какая таблица является родительской в связи и
какие столбцы в зависимой таблице ссылаются на какие столбцы в
родительской таблице.
Ключом правила является строка, используемая как индекс массива
$_referenceMap
. Этот ключ правила используется для
идентификации каждой связи. Выбирайте для него описательное имя.
Лучше всего использовать строку, которая может быть частью имени
метода, как вы увидите позднее.
В примере PHP-кода выше ключами правил являются:
'Reporter'
,
'Engineer'
,
'Verifier'
, и
'Product'
.
Значением каждого правила в массиве $_referenceMap
является также ассоциативный массив. Элементы этого массива описаны
ниже:
columns => Строка или массив строк, в котором перечислены имена столбцов внешних ключей в зависимой таблице.
Обычно это один столбец, но некоторые таблицы имеют составные ключи (из нескольких столбцов).
refTableClass => Имя класса родительской таблицы. Используйте имя класса, а не таблицы в БД.
Обычно зависимые таблицы имеют одну связь со своей
родительской таблицей, но некоторые таблицы имеют
множественные связи с одной и той же родительской таблицей.
В базе данных, которую мы рассматриваем для примера, таблица
bugs
ссылается на таблицу
products
, но имеет также три связи с таблицей
accounts
.
Помещайте каждую ссылку в отдельную запись в массиве
$_referenceMap
.
refColumns => Строка или массив строк, в котором перечислены имена столбцов первичного ключа в родительской таблице.
Обычно это один столбец, но некоторые таблицы имеют
составные ключи. Если ссылка использует составной ключ, то
порядок столбцов в элементе 'columns'
должен
соответствовать порядку столбцов в элементе
'refColumns'
.
Этот элемент является опциональным. Если вы не определите
refColumns
, то по умолчанию используются имена
столбцов, объявленных как столбцы первичных ключей
родительской таблицы.
onDelete => Правило для действия, выполняемого, когда в родительской таблице удаляется строка. За более подробной информацией см. "Раздел 9.8.6, «Каскадные операции записи»".
onUpdate => Правило для действия, выполняемого, когда изменяются значения в столбцах первичного ключа родительской таблицы. За более подробной информацией см. "Раздел 9.8.6, «Каскадные операции записи»".
Если вы имеете объект Row (строка) в результате запроса к родительской таблице, то вы можете извлечь строки из зависимых таблиц, ссылающихся на текущую строку. Используйте этот метод:
$row->findDependentRowset($table, [$rule])
Этот метод возвращает объект Zend_Db_Table_Rowset_Abstract,
содержащий набор строк из зависимой таблицы $table
,
ссылающихся на строку, представленную объектом $row
.
Первый аргумент $table
может быть строкой с именем
класса зависимой таблицы. Вы можете также определить зависимую
таблицу, используя объект класса этой таблицы.
Пример 9.92. Извлечение зависимых строк
Этот пример демонстрирует получение объекта строки из таблицы
Accounts
и поиск ошибок в таблице
Bugs
, о которых сообщил данный пользователь.
<?php $accountsTable = new Accounts(); $accountsRowset = $accountsTable->find(1234); $user1234 = $accountsRowset->current(); $bugsReportedByUser = $user1234->findDependentRowset('Bugs'); ?>
Втрой аргумент $rule
является опциональным. Это строка
с ключом правила в массиве $_referenceMap
класса
зависимой таблицы.
Если вы не определите правило, то будет использоваться первое
правило в массиве, ссылающееся на родительскую таблицу.
Для того, чтобы использовать правило, отличное от первого,
необходимо указать ключ.
В примере выше ключ правила не определен, поэтому используется
первое правило, соответствующее родительской таблице. Это правило
'Reporter'
.
Пример 9.93. Извлечение зависимых строк по определенному правилу
Этот пример демонстрирует получение объекта строки из таблицы
Accounts
и поиск ошибок в таблице
Bugs
, устранение которых назначено данному
пользователю. Ключ правила, соответствующий этой связи в данном
примере - 'Engineer'
.
<?php $accountsTable = new Accounts(); $accountsRowset = $accountsTable->find(1234); $user1234 = $accountsRowset->current(); $bugsAssignedToUser = $user1234->findDependentRowset('Bugs', 'Engineer'); ?>
Вы можете также запрашивать строки из зависимой таблицы, используя
специальный механизм, называемый "магический метод".
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 не предоставляет никакого механизма инфлексии. Имя таблицы и ключ правила в вызовах методов должны в точности соответствовать написанию имени класса таблицы и ключа правила при объявлении. |
Пример 9.94. Извлечение зависимых строк с использованием магического метода
Этот пример демонстрирует поиск зависимых строк, эквивалентный тому, что был в предыдущих примерах. В данном случае приложение использует вызов магического метода вместо передачи имени таблицы и ключа правила в качестве аргументов.
<?php $accountsTable = new Accounts(); $accountsRowset = $accountsTable->find(1234); $user1234 = $accountsRowset->current(); // Используется правило связи по умолчанию $bugsReportedBy = $user1234->findBugs(); // Задается правило связи $bugsAssignedTo = $user1234->findBugsByEngineer(); ?>
Если вы имеете объект Row в результате запроса к зависимой таблице, то можете извлечь ту строку из родительской таблицы, на которую ссылается зависимая строка. Используйте метод:
$row->findParentRow($table, [$rule])
Зависимая строка всегда должна ссылаться только на одну строку в родительской таблице, поэтому этот метод возвращает объект Row, а не Rowset.
Первый аргумент $table
может быть строкой с именем
класса родительской таблицы. Вы можете также задавать родительскую
таблицу, используя объект класса этой таблицы.
Пример 9.95. Извлечение родительской строки
Этот пример демонстрирует получение объекта Row из таблицы
Bugs
(для примера, одна из этих ошибок имеет статус
'NEW'), и поиск строки в таблице Accounts
,
соответствующей пользователю, сообщившем об этой ошибке.
<?php $bugsTable = new Bugs(); $bugsRowset = $bugsTable->fetchAll('bug_status = ?', 'NEW'); $bug1 = $bugsRowset->current(); $reporter = $bug1->findParentRow('Accounts'); ?>
Второй аргумент $rule
является опциональным. Это строка
с ключом правила в массиве $_referenceMap
класса
зависимой таблицы.
Если вы не определите правило, то будет использоваться первое
правило в массиве, ссылающееся на родительскую таблицу.
Для того, чтобы использовать правило, отличное от первого,
необходимо указать ключ.
В примере кода выше ключ правила не определен, поэтому используется
первое правило, соответствующее родительской таблице. Это правило
'Reporter'
.
Пример 9.96. Извлечение родительской строки по определенному правилу
Этот пример демонстрирует получение объекта Row из таблицы
Bugs
и поиск аккаунта пользователя, которому
назначено исправление этой ошибки. Ключ правила,
соответствующего этой связи в данном примере -
'Engineer'
.
<?php $bugsTable = new Bugs(); $bugsRowset = $bugsTable->fetchAll('bug_status = ?', 'NEW'); $bug1 = $bugsRowset->current(); $engineer = $bug1->findParentRow('Accounts', 'Engineer'); ?>
Вы можете также запрашивать строки из родительской таблицы,
используя "магический метод".
Zend_Db_Table_Row_Abstract вызывает метод:
findParentRow('<TableClass>', '<Rule>')
,
если вы вызываете метод объекта Row, соответствующий одному из
следующих шаблонов:
$row->findParent<TableClass>()
$row->findParent<TableClass>By<Rule>()
В этих шаблонах <TableClass>
и
<Rule>
- имя класса родительской таблицы и ключ
правила зависимой таблицы, ссылающегося на родительскую таблицу.
Замечание | |
---|---|
Имя таблицы и ключ правила в вызовах методов должны в точности соответствовать написанию имени класса таблицы и ключа правила при объявлении. |
Пример 9.97. Извлечение родительской строки с использованием магического метода
Этот пример демонстрирует поиск родительской строки, эквивалентный тому, что был в предыдущих примерах. В данном случае используется вызов магического метода вместо передачи имени таблицы и ключа правила в качестве аргументов.
<?php $bugsTable = new Bugs(); $bugsRowset = $bugsTable->fetchAll('bug_status = ?', 'NEW'); $bug1 = $bugsRowset->current(); // Используется правило связи по умолчанию $reporter = $bug1->findParentAccounts(); // Задается правило связи $engineer = $bug1->findParentAccountsByEngineer(); ?>
Если вы имеете объект Row в результате выполнения запроса к одной из таблиц, находящихся в связи "многие-ко-многим" (в рамках данного примера будем называть эту таблицу "исходной"), вы можете извлечь соответствующие строки в другой таблице ("целевая" таблица) через таблицу пересечений. Используйте метод:
$row->findManyToManyRowset($table, $intersectionTable, [$rule1, [$rule2]])
Этот метод возвращает объект Zend_Db_Table_Rowset_Abstract,
содержащий строки из таблицы $table
, соответствующие
связи "многие-ко-многим". Текущий объект строки $row
исходной таблицы используется в поиске строк в таблице пересечений и
производится объединение с целевой таблицей.
Первый аргумент $table
может быть именем класса целевой
таблицы в связи "многие-ко-многим". Вы можете также задавать целевую
таблицу, используя объект класса этой таблицы.
Второй аргумент $intersectionTable
может быть именем
класса таблицы пересечений между двумя таблицами в связи
"многие-ко многим". Вы можете также задавать таблицу пересечений,
используя объект класса этой таблицы.
Пример 9.98. Извлечение строк через метод для связей "многие-ко-многим"
Этот пример демонстрирует получение объекта Row из исходной
таблицы Accounts
и поиск строк в целевой таблице
Products
, соответствующих продуктам, об ошибках в
которых сообщил этот пользователь.
<?php $accountsTable = new Accounts(); $accountsRowset = $accountsTable->find(1234); $user1234 = $accountsRowset->current(); $productsRowset = $user1234->findManyToManyRowset('Products', 'Bugs'); ?>
Третий и четвертый аргументы $rule1
и
$rule2
являются опциональными. Это строки с ключами
правил в массиве $_referenceMap
класса таблицы
пересечений.
$rule1
должен содержать ключ правила для ссылок таблицы
пересечений на исходную таблицу. В данном примере это связь между
таблицами Bugs
и Accounts
.
$rule2
должен содержать ключ правила для ссылок таблицы
пересечений на целевую таблицу. В данном примере это связь между
таблицами Bugs
и Products
Как и в случае методов для извлечения родительских и зависимых
строк, если вы не зададите правило, то метод использует первое
правило в массиве $_referenceMap
, соответствующее
таблицам в связи. Если нужно использовать правило, отличное от
первого, то необходимо указать ключ.
В примере кода выше ключ правила не указан, поэтому по умолчанию
используются первые подходящие правила из массива. В данном случае
для правила $rule1
будет использоваться
'Reporter'
, для правила $rule2
-
'Product'
.
Пример 9.99. Извлечение строк через метод для связей "многие-ко-многим" по определенному правилу
Этот пример демонстрирует получение объекта Row из исходной
таблицы Accounts
и поиск строк в целевой таблице,
Products
, соответствующих продуктам, ошибки в
которых должен исправить этот пользователь.
<?php $accountsTable = new Accounts(); $accountsRowset = $accountsTable->find(1234); $user1234 = $accountsRowset->current(); $productsRowset = $user1234->findManyToManyRowset('Products', 'Bugs', 'Engineer'); ?>
Вы можете также запрашивать строки из целевой таблицы в связи
"многие-ко-многим", используя "магический метод".
Zend_Db_Table_Row_Abstract вызывает метод
findManyToManyRowset('<TableClass>', '<IntersectionTableClass>', '<Rule1>', '<Rule2>')
, если вы вызываете метод, соотвествующий
одному из следующих шаблонов:
$row->find<TableClass>Via<IntersectionTableClass>()
$row->find<TableClass>Via<IntersectionTableClass>By<Rule1>()
$row->find<TableClass>Via<IntersectionTableClass>By<Rule1>And<Rule2>()
В этих шаблонах <TableClass>
и
<IntersectionTableClass>
являются именами классов
целевой таблицы и таблицы пересечений
соответственно. <Rule1>
и
<Rule2>
являются ключами правил в таблице
пересечений, соответствующими исходной таблице и целевой таблице
соответственно.
Замечание | |
---|---|
Имя таблицы и ключ правила в вызовах методов должны в точности соответствовать написанию имени класса таблицы и ключа правила при объявлении. |
Пример 9.100. Извлечение строк с использованием магического метода для связей "многие-ко-многим"
Этот пример демонстрирует поиск строк в целевой таблице в связи "многие-ко многим", соответствующих продуктам, об ошибках в которых сообщил этот пользователь и для которых этот пользователь должен устранить ошибки.
<?php $accountsTable = new Accounts(); $accountsRowset = $accountsTable->find(1234); $user1234 = $accountsRowset->current(); // Используется правило связи по умолчанию $productsReporting = $user1234->findProductsViaBugs(); // Задается правило связи $productsFixing = $user1234->findProductsViaBugsByEngineer(); ?>
Объявление DRI в БД | |
---|---|
Объявление каскадных операций в Zend_Db_Table предназначено только для тех СУРБД, которые не поддерживают декларативной ссылочной целостности (declarative referential integrity - сокр. DRI). Например, если вы используете механизм хранения MyISAM в MySQL или SQLite, не поддерживающие DRI, то для вас может быть полезным объявить каскадные операции через Zend_Db_Table.
Если ваша СУРБД реализует DRI и поддерживает предложения
Что еще более важно, не объявляйте каскадные операции одновременно в СУРБД и в классе Zend_Db_Table. |
Вы можете объявить каскадные операции для их выполнения в зависимой
таблице при применении операций UPDATE
и
DELETE
к строкам в родительской таблице.
Пример 9.101. Пример каскадного удаления
Этот пример демонстрирует удаление строки в таблице
Products
, которая была сконфигурирована для
автоматического удаления зависимых строк в таблице
Bugs
.
<?php $productsTable = new Products(); $productsRowset = $productsTable->find(1234); $product1234 = $productsRowset->current(); $product1234->delete(); // Автоматически выполняется каскадное // удаление зависимых строк в таблице Bugs ?>
Аналогично, если вы используете UPDATE
для изменения
значения первичного ключа в родительской таблице, то при этом часто
требуется, чтобы значение внешнего ключа в зависимой таблице также
изменялось на новое, и таким образом поддерживалась ссылочная
целостность.
Обычно нет необходимости в том, чтобы изменять значение первичного ключа, которое генерируется последовательностью (sequence) или другим механизмом. Но если вы используете естетственные ключи, которые иногда могут изменять свое значение, то, скорее всего, нужно будет использовать каскадное обновление зависимых таблиц.
Для объявления каскадных связей в Zend_Db_Table, отредактируйте
правила в массиве $_referenceMap
. Установите в
ассоциативного массиве под ключами 'onDelete'
и
'onUpdate'
значение 'cascade' (или константу
self::CASCADE
). До того, как строка будет удалена из
родительской таблицы или изменится значение ее первичного ключа,
будут удалены или обновлены любые строки в зависимой таблице,
ссылающиеся на эту строку родительской таблицы.
Пример 9.102. Пример объявления каскадных операций
В примере ниже строки в таблице Bugs
автоматически
удаляются, если строка в таблице Products
, на
которую они ссылаются, удаляется. Элемент
'onDelete'
записи в массиве связей установлен в
self::CASCADE
.
В примере ниже не выполняется каскадное обновление, если
изменяется значение первичного ключа. Элемент
'onUpdate'
записи в массиве связей установлен в
self::RESTRICT
. Вы можете получить тот же самый
результат, используя значение self::NO_ACTION
или
пропустив элемент 'onUpdate'
.
<?php class Bugs 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
. Вы должны добавить строку в родительской таблице в одной операции и
добавить строки в зависимой таблице в другой операции.