Мало кто использует этот удивительный класс на полную мощность. О некоторых его скрытых возможностях также мало кто догадывался, как и я до этого момента.
Как известно таблицы в реляционных базах данных связываются отношениями один ко многим и многие ко многим. Возможно кто-то предложит ещё пару-тройку связей, но данная статья не для того чтобы подискутировать по этому поводу, а для того чтобы подсказать и направить мысль в нужное нам русло. Я рассмотрю связь один ко многим в реализации, как вы наверно догадались, Zend Framework'а.
Как известно класс Zend_Db_Table является объектно-ориентированным интерфейсом к таблицам баз данных. Первым полезным открытием было то, что Zend_Db_Table способен производить каскадное удаление и каскадное обновление записей связей, для чего это необходимо, да для того чтобы при удалении/обновлении ссылочного значения удалились/обновились записи в зависимых таблицах. Для примера будем использовать базу данных MySQL с механизмом хранения данных MyISAM, который не поддерживают декларативной ссылочной целостности.
Сейчас небольшое отступление. Нам для примера необходима небольшая база данных состоящая из следующих таблиц: Products, Units и Groups. Речь пойдет о продуктах питания (Products), единицах измерения (Units) и группах продуктов (Groups).
Ниже SQL запросы для создания и заполнения таблиц:
Теперь когда таблицы созданы и заполнены тестовыми данными, приступаем к созданию классов для наших таблиц:
Для простоты я поместил эти классы в контроллер, для реальных проектов этого делать вам крайне не рекомендую. Получить данные в таблице продуктов можно следующим образом:
Как видим метод fetchAll возвращает нам набор строк (rowset), где каждый $row соответствует строке из нашей таблицы. Если добавить следующий код в foreach, то он способен изменить конкретную запись:
К нашей «строке» $row применим метод save из абстрактного класса Zend_Db_Table_Row_Abstract, поскольку foreach разбирает наш rowset, который является экземпляром класса Zend_Db_Table_Rowset, наследуемый от Zend_Db_Table_Row_Abstract на отдельные $row. Сами же $row ничто иное как экземпляры класса Zend_Db_Table_Row. Манипуляции с экземплярами класса Zend_Db_Table_Row не требуют глубоких познаний, поэтому мы не будем на них останавливаться, а поговорим о следующем.
Допустим вам не нравится порядок первичных ключей в таблице groups и вы хотите чтобы они начинались, например со 100, не проблема, но сколько действий надо совершить чтобы это реализовать? Голова начинает идти кругом. Посмотрите на таблицу products, в ней мы предусмотрели внешнюю связь с таблицей groups, по ключу group_id. И после наших изменений продукты должны соответствовать своим группам. Возможно это не тот пример, который вы хотели бы увидеть и он не претендует на овации, но он способен показать каким образом реализован механизм взаимодействия классов Zend_Db_Table в Zend Framework'e.
Итак приступим. В объектной модели для родительских таблиц, необходимо указать зависимые таблицы. В классе Groups необходимо указать зависимый класс Products, добавив следующее свойство
также это необходимо сделать в классе Units. Эти нам говорит о том, что записи в зависимых таблицах будут браться в учёт при изменении родительских. Но для этого нам необходимо в классе Products добавить связи с таблицами Units и Groups. Реализовать это можно несколькими способами, например добавить свойство $_referenceMap в класс Products:
Свойство $_referenceMap позволяет добавить ссылку на внешнюю таблицу, таким образом мы получаем связь многие к одному. В нём необходимо определить следующие параметры
1. ассоциативный ключ
2. имя поля внешнего ключа в ссылающейся таблице
3. имя класса таблицы на которую ссылаемся
4. имя поля в таблице на которую ссылаемся
5. действие которое произойдет при удалении
6. действие которое произойдет при обновлении
Код целиком:
Теперь у нас есть связанные таблицы и мы можем не беспокоиться и спокойно апдейтить наши id ключи.
Кстати, забыл упомянуть, что при удалении какой-нибудь группы продуктов удаляться и сами продукты, которые соответствуют этим группам. Можете поэкспериментировать и удалить любую из них. Если вам интересно, то методы _cascadeUpdate и _cascadeDelete абстрактного класса Zend_Db_Table_Abstract отвечают за обновление и удаление и вызываются они при вызове $row->save() и $row->delete() соответственно. Также вы можете получить родительскую запись (row) вызвав метод findParentRow
Либо получить все зависимые записи (rows) вызвав метод findDependentRowset
С полученными строками (rows) вы можете также свободно работать как с экземплярами класса Zend_Db_Table_Row. Удачи!
p.s. Привет Джос, учтены все твои пожелания.
Как известно таблицы в реляционных базах данных связываются отношениями один ко многим и многие ко многим. Возможно кто-то предложит ещё пару-тройку связей, но данная статья не для того чтобы подискутировать по этому поводу, а для того чтобы подсказать и направить мысль в нужное нам русло. Я рассмотрю связь один ко многим в реализации, как вы наверно догадались, Zend Framework'а.
Как известно класс Zend_Db_Table является объектно-ориентированным интерфейсом к таблицам баз данных. Первым полезным открытием было то, что Zend_Db_Table способен производить каскадное удаление и каскадное обновление записей связей, для чего это необходимо, да для того чтобы при удалении/обновлении ссылочного значения удалились/обновились записи в зависимых таблицах. Для примера будем использовать базу данных MySQL с механизмом хранения данных MyISAM, который не поддерживают декларативной ссылочной целостности.
Сейчас небольшое отступление. Нам для примера необходима небольшая база данных состоящая из следующих таблиц: Products, Units и Groups. Речь пойдет о продуктах питания (Products), единицах измерения (Units) и группах продуктов (Groups).
Ниже SQL запросы для создания и заполнения таблиц:
# таблица единиц измерения продуктов DROP TABLE IF EXISTS `units`; CREATE TABLE `units` ( `unit_id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, `unit_name` VARCHAR(256) NOT NULL DEFAULT '', PRIMARY KEY (`unit_id`) ) ENGINE=MYISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; INSERT INTO `units` SET `unit_name` = 'грамм'; INSERT INTO `units` SET `unit_name` = 'миллилитр'; # таблица групп продуктов DROP TABLE IF EXISTS `groups`; CREATE TABLE `groups` ( `group_id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, `group_name` VARCHAR(256) NOT NULL DEFAULT '', PRIMARY KEY (`group_id`) ) ENGINE=MYISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; INSERT INTO `groups` SET `group_name` = 'Овощи'; INSERT INTO `groups` SET `group_name` = 'Фрукты'; INSERT INTO `groups` SET `group_name` = 'Молочные'; INSERT INTO `groups` SET `group_name` = 'Мясные'; # таблица продуктов DROP TABLE IF EXISTS `products`; CREATE TABLE `products` ( `product_id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, `group_id` INT(10) DEFAULT NULL, `unit_id` INT(10) DEFAULT NULL, `product_name` VARCHAR(256) NOT NULL DEFAULT '', PRIMARY KEY (`product_id`) ) ENGINE=MYISAM DEFAULT CHARSET=utf8; # Овощи INSERT INTO `products` (`group_id`, `unit_id`, `product_name`) VALUES (1, 1, 'Картофель'); INSERT INTO `products` (`group_id`, `unit_id`, `product_name`) VALUES (1, 1, 'Помидор'); # Фрукты INSERT INTO `products` (`group_id`, `unit_id`, `product_name`) VALUES (2, 1, 'Абрикос'); INSERT INTO `products` (`group_id`, `unit_id`, `product_name`) VALUES (2, 1, 'Яблоко'); # Молочные INSERT INTO `products` (`group_id`, `unit_id`, `product_name`) VALUES (3, 1, 'Брынза'); INSERT INTO `products` (`group_id`, `unit_id`, `product_name`) VALUES (3, 2, 'Молоко'); # Мясные INSERT INTO `products` (`group_id`, `unit_id`, `product_name`) VALUES (4, 1, 'Телятина'); INSERT INTO `products` (`group_id`, `unit_id`, `product_name`) VALUES (4, 1, 'Свинина');
Теперь когда таблицы созданы и заполнены тестовыми данными, приступаем к созданию классов для наших таблиц:
class Products extends Zend_Db_Table_Abstract { protected $_name = 'products'; protected $_primary = array('product_id'); } class Units extends Zend_Db_Table_Abstract { protected $_name = 'units'; protected $_primary = array('unit_id'); } class Groups extends Zend_Db_Table_Abstract { protected $_name = 'groups'; protected $_primary = array('group_id'); }
Для простоты я поместил эти классы в контроллер, для реальных проектов этого делать вам крайне не рекомендую. Получить данные в таблице продуктов можно следующим образом:
$productsTable = new Products; $productsRowset = $productsTable->fetchAll(); foreach($productsRowset as $row) { echo '<pre>' . print_r($row->toArray(), true) . '</pre>' . PHP_EOL; }
Как видим метод fetchAll возвращает нам набор строк (rowset), где каждый $row соответствует строке из нашей таблицы. Если добавить следующий код в foreach, то он способен изменить конкретную запись:
if ($row->product_name == 'Свинина') { $row->product_name = 'Шашлык из свинины'; $row->save(); }
К нашей «строке» $row применим метод save из абстрактного класса Zend_Db_Table_Row_Abstract, поскольку foreach разбирает наш rowset, который является экземпляром класса Zend_Db_Table_Rowset, наследуемый от Zend_Db_Table_Row_Abstract на отдельные $row. Сами же $row ничто иное как экземпляры класса Zend_Db_Table_Row. Манипуляции с экземплярами класса Zend_Db_Table_Row не требуют глубоких познаний, поэтому мы не будем на них останавливаться, а поговорим о следующем.
Допустим вам не нравится порядок первичных ключей в таблице groups и вы хотите чтобы они начинались, например со 100, не проблема, но сколько действий надо совершить чтобы это реализовать? Голова начинает идти кругом. Посмотрите на таблицу products, в ней мы предусмотрели внешнюю связь с таблицей groups, по ключу group_id. И после наших изменений продукты должны соответствовать своим группам. Возможно это не тот пример, который вы хотели бы увидеть и он не претендует на овации, но он способен показать каким образом реализован механизм взаимодействия классов Zend_Db_Table в Zend Framework'e.
Итак приступим. В объектной модели для родительских таблиц, необходимо указать зависимые таблицы. В классе Groups необходимо указать зависимый класс Products, добавив следующее свойство
protected $_dependentTables = array('Products');
также это необходимо сделать в классе Units. Эти нам говорит о том, что записи в зависимых таблицах будут браться в учёт при изменении родительских. Но для этого нам необходимо в классе Products добавить связи с таблицами Units и Groups. Реализовать это можно несколькими способами, например добавить свойство $_referenceMap в класс Products:
protected $_referenceMap = array( 'refUnits' => array( self::COLUMNS => 'unit_id', self::REF_TABLE_CLASS => 'Units', self::REF_COLUMNS => 'unit_id', self::ON_DELETE => self::CASCADE, self::ON_UPDATE => self::CASCADE ), 'refGroups' => array( self::COLUMNS => 'group_id', self::REF_TABLE_CLASS => 'Groups', self::REF_COLUMNS => 'group_id', self::ON_DELETE => self::CASCADE, self::ON_UPDATE => self::CASCADE ) );
Свойство $_referenceMap позволяет добавить ссылку на внешнюю таблицу, таким образом мы получаем связь многие к одному. В нём необходимо определить следующие параметры
1. ассоциативный ключ
2. имя поля внешнего ключа в ссылающейся таблице
3. имя класса таблицы на которую ссылаемся
4. имя поля в таблице на которую ссылаемся
5. действие которое произойдет при удалении
6. действие которое произойдет при обновлении
Код целиком:
class Products extends Zend_Db_Table_Abstract { protected $_name = 'products'; protected $_primary = array('product_id'); protected $_referenceMap = array( 'refUnits' => array( self::COLUMNS => 'unit_id', self::REF_TABLE_CLASS => 'Units', self::REF_COLUMNS => 'unit_id', self::ON_DELETE => self::CASCADE, self::ON_UPDATE => self::CASCADE ), 'refGroups' => array( self::COLUMNS => 'group_id', self::REF_TABLE_CLASS => 'Groups', self::REF_COLUMNS => 'group_id', self::ON_DELETE => self::CASCADE, self::ON_UPDATE => self::CASCADE ) ); } class Units extends Zend_Db_Table_Abstract { protected $_name = 'units'; protected $_primary = array('unit_id'); protected $_dependentTables = array('Products'); } class Groups extends Zend_Db_Table_Abstract { protected $_name = 'groups'; protected $_primary = array('group_id'); protected $_dependentTables = array('Products'); }
Теперь у нас есть связанные таблицы и мы можем не беспокоиться и спокойно апдейтить наши id ключи.
Кстати, забыл упомянуть, что при удалении какой-нибудь группы продуктов удаляться и сами продукты, которые соответствуют этим группам. Можете поэкспериментировать и удалить любую из них. Если вам интересно, то методы _cascadeUpdate и _cascadeDelete абстрактного класса Zend_Db_Table_Abstract отвечают за обновление и удаление и вызываются они при вызове $row->save() и $row->delete() соответственно. Также вы можете получить родительскую запись (row) вызвав метод findParentRow
$productsTable = new Products; $productsRowset = $productsTable->fetchAll(); foreach($productsRowset as $row) { echo '-----------------------' . PHP_EOL; echo '<pre>' . print_r($row->toArray(), true) . '</pre>' . PHP_EOL; echo '<pre>' . print_r($row->findParentRow('Units')->toArray(), true) . '</pre>' . PHP_EOL; echo '<pre>' . print_r($row->findParentRow('Groups')->toArray(), true) . '</pre>' . PHP_EOL; echo '-----------------------' . PHP_EOL; }
Либо получить все зависимые записи (rows) вызвав метод findDependentRowset
$groupsRowset = $groupsTable->fetchAll(); foreach($groupsRowset as $row) { echo '-----------------------' . PHP_EOL; echo '<pre>' . print_r($row->toArray(), true) . '</pre>' . PHP_EOL; echo '<pre>' . print_r($row->findDependentRowset('Products')->toArray(), true) . '</pre>' . PHP_EOL; echo '-----------------------' . PHP_EOL; }
С полученными строками (rows) вы можете также свободно работать как с экземплярами класса Zend_Db_Table_Row. Удачи!
p.s. Привет Джос, учтены все твои пожелания.