ORM
В HostCMS реализован быстрый и гибкий ORM на основе паттерна Active Record с поддержкой ленивой загрузки. Имена таблиц и полей задаются в нижнем регистре, разделителем является подчеркивание (андерскор).
ORM поддерживает следующие виды связей: hasOne (в том числе через промежуточную таблицу), hasMany (в том числе через промежуточную таблицу) и belongsTo.
В системе управления ORM реализован классом Core_ORM, от которого порожден класс Core_Entity с реализацией дополнительной логики работы, например, генерации XML.
Все модели сущностей порождены от Core_Entity, сам Core_ORM явно не используется.
Имена таблиц для моделей преобразуются во множественное число по правилам английского языка, например, для модели Book_Model используется таблица books, для Property_Model таблица properties.
Что такое ORM?
ORM является технологией, которая связывает БД с концепцией ООП. Реализовываться может с помощью шаблонов программирования, мы выбрали Active Record.
Что мне это дает?
Простую и безопасную работу с объектами БД через объекты PHP.
Примеры работы с ORM
Создадим таблицу `books` со столбцами:
id INT(11) PK name VA(255) value VA(100) deleted tinyint(1)
Создадим модель Book_Model, которую поместим в modules/book/model.php со следующим кодом:
class Book_Model extends Core_Entity
{
}
Получение существующего элемента или создание нового осуществляется через фабрику (более подробно о фабриках можно прочитать в сети по запросу "Порождающий шаблон проектирования Фабричный метод").
Далее идут несколько примеров работы с ORM, настоятельно рекомендуем поработать с ними.
Атрибут deleted
Используются два механизма - пометка на удаление markDeleted() и окончательное удаление delete(), все стандартные выборки с использованием findAll() выбирают не помеченные на удаление объекты. Окончательно удалить или восстановить объект можно через модуль "Корзина".
В случае, если вы не хотите использовать для модели пометку на удаление, в файл модели добавьте свойство $_marksDeleted:
/** * Disable markDeleted() * @var mixed */ protected $_marksDeleted = NULL;
Если вы хотите получить по связи элементы, включая удаленные, то используйте метод setMarksDeleted(NULL), например:
// Получим все книги, включая помеченные на удаление
$aBook = Core_Entity::factory('Book')->setMarksDeleted(NULL)->findAll();
Создание объектов
Создание нового объекта Book_Model
$oBook = Core_Entity::factory('Book');
Создание объекта Book_Model и загрузка в объект данных из таблицы, где первичный ключ равен 1
$oBook = Core_Entity::factory('Book', 1);
Для создания нового объекта используется фабрика с указанием модели объекта, затем заполняются атрибуты объекта и вызывается метод save():
// Новый объект
$oBook = Core_Entity::factory('Book');
// Заполняем свойства объекта $oBook->value = 123;
$oBook->name = 'Новая книга';
// Сохраняем $oBook->save();
// После сохранение у объекта будет заполнен ID
echo "Новый ID: ", $oBook->id;
Изменение и сохранение объекта
Изменение свойства объекта возможно двумя способами: установкой значения свойства или вызовом метода с названием свойства и передачей ему аргумента в качестве нового значения:
$oBook = Core_Entity::factory('Book', 1);
$oBook->value = 123;
$oBook->save();
Второй способ с method chaining:
$oBook = Core_Entity::factory('Book', 1);
$oBook->value(123)->save();
Поиск объектов
Получим все объекты модели Book_Model с использованием метода findAll(). Метод принимает необязательный параметр кэширования, по умолчанию TRUE (кэширование включено).
$aBooks = Core_Entity::factory('Book')->findAll();
foreach ($aBooks as $oBook)
{
// do something
echo "<p>" . $oBook->name . "</p>";
}
Поиск объектов с заданием дополнительного условия через QueryBuilder и отключением кэширования результатов запроса
$oBooks = Core_Entity::factory('Book');
$oBooks->queryBuilder()
->where('value', '=', 99);
$aBooks = $oBooks->findAll(FALSE);
foreach ($aBooks as $oBook)
{
// do something
echo "<p>" . $oBook->name . "</p>";
}
Получение данных частями
Метод chunk() используется при обработке больших объемов данных, он получает заданное количество записей за раз и отправляет массив объектов в замыкание для последующей обработки.
Если вы хотите прервать выполнение последующих блоков, то верните FALSE.
Core_Entity::factory('Book')->chunk(50, function ($aObjects, $step) {
foreach ($aObjects as $oObject)
{
echo $oObject;
}
});
Связи с использованием ORM
Связи указываются в модели и позволяют получать быстрый доступ к связанным и зависимым объектам.
Связь один-ко-многим
Используется, когда у объекта текущей модели есть множество зависимых элементов, в примере одна книга имеет несколько комментариев.
В файл моделиmodules/book/model.php добавим описание связи:
// Relation one-to-many for Book-Comments:
protected $_hasMany = array('comment' => array());
Аналогичная связь один-ко-многим с подробным заполнением параметров связи
// Equivalence relation one-to-many for Book-Comments with detailed conditions:
// comment - Model name
// foreign_key - Foreign key
protected $_hasMany = array('comment' => array(
'foreign_key' => 'book_id'
));
Для получения элементов из связи 1:М указывается название связи с большой буквы и во множественной форме, например, для comment будет Comments:
$oBook = Core_Entity::factory('Book', 1);
$aComments = $oBook->Comments->findAll();
Связь многие-ко-многим
Множеству объектов текущей модели могут соответствовать множество элементов другой, в примере один и тот же автор может быть у разных книг, и у одной книги может быть несколько авторов.
В файл моделиmodules/book/model.php добавим описание связи:
// Relation many-to-many for Book-Authorss through model 'books_author':
protected $_hasMany = array('author' => array(
'through' => 'books_author'
));
Аналогичная связь многие-ко-многим с подробным заполнением параметров связи
// Relation many-to-many for Book-Authorss through model 'books_author' with detailed conditions:
protected $_hasMany = array('author' => array(
'foreign_key' => 'book_id,
'through' => 'books_author',
'dependent_key' => 'author_id'
));
Для получения элементов из связи М:М указывается название связи с большой буквы и во множественной форме, например:
// Получение авторов книги c ID 17
$oBook = Core_Entity::factory('Book', 17); $aAuthors = $oBook->Authors->findAll();
// Получение книг автора с ID 19
$oAuthor = Core_Entity::factory('Author', 19); $aBooks = $oAuthor->Books->findAll();
Связь один-к-одному
В файл моделиmodules/book/model.php добавим описание связи:
// Relation one-to-one for Book-Comment:
protected $_hasOne = array('comment' => array());
Аналогичная связь один-к-одному с подробным заполнением параметров связи
// Equivalence relation one-to-one for Book-Comment with detailed conditions:
// comment - Model name
// foreign_key - Foreign key
protected $_hasOne = array('comment' => array(
'foreign_key' => 'book_id'
));
Для получения элементов из связи 1:1 указывается название связи с большой буквы, например:
$oBook = Core_Entity::factory('Book', 1);
$oComment = $oBook->Comment;
Связь "belongs to"
Объект зависим от другого объекта и содержит в себе внешний ключ.
В файл моделиmodules/book/model.php добавим описание связи с таблицей sections (в таблице books должен быть внешний ключ section_id):
// Belongs to relation for Section-Book:
protected $_belongsTo = array('section' => array());
Аналогичная связь "belongs to" с подробным заполнением параметров связи
// Equivalence belongs to relation for Section-Book with detailed conditions:
// section - Model name
// foreign_key - Foreign key
// primary_key - Primary key in the parent table
protected $_belongsTo = array('section' => array(
'foreign_key' => 'section_id',
'primary_key' => 'id'
))
Для получения элементов из связи указывается название связи с большой буквы, например:
$oBook = Core_Entity::factory('Book', 1);
$oSection = $oBook->Section;
Методы add($object) и remove($object)
Если элементы имеют связи между собой (включая связи через промежуточную таблицу), то добавлять один объект другому можно с использованием метода add(). Если добавляемый объект не был сохранен, то перед добавление он будет автоматически сохранене. Пример добавления нового комментария к книге с ID 1:
// Существующая книга с ID 1
$oBook = Core_Entity::factory('Book', 1);
// Новый комментарий
$oComment = Core_Entity::factory('Comment');
$oComment->title = 'Заголовок комментария';
$oComment->author = 'Петров Иван';
// Сохранит объект комментария и добавить запись в промежуточную таблицу
$oBook->add($oComment);
Удаление связи осуществляется методом remove().
Методы-перехватчики
Core_Entity поддерживает удобные методы-перехватчики (где «FieldName» — имя столбца в таблице, «$value» — значение):
getByFieldName($value, $bCache = TRUE, $compare = '=')— возвращает один найденный объект или NULL;getAllByFieldName($value, $bCache = TRUE, $compare = '=')— возвращает массив с элементами или пустой массив;getCountByFieldName($value— возвращает количество найденных элементов без выборки самих элементов., $bCache = TRUE, $compare = '=')
//Implement methods like getByXXX($value, $cache = TRUE) where XXX is the field name
$object = Core_Entity::factory('Book')->getByName('The Catcher in the Rye');
//Implement methods like getAllByXXX($value, $cache = TRUE) where XXX is the field name
$aObject = Core_Entity::factory('Book')->getAllByName('The Catcher in the Rye');
Методы-перехватчики можно также использовать для связи. Получим активные комментарии книги с использование QueryBuilder:
// С использованием QueryBuilder
$oComments = Core_Entity::factory('Book', 1)->Comments;
$oComments->queryBuilder()
->where('active', '=', 1);
$aComments = $oComments->findAll();
и с использованием метода-перехватчика:
// С использованием метода-перехватчика
$aComments = Core_Entity::factory('Book', 1)->Comments->getAllByActive(1);
Получение количества объектов
Метод getCount() используется для получения количества найденных элементов без выбора самих элементов. Дополнительный метод-перехватчик getCountByFieldName($value) используется для получения количества найденных объектов с применением дополнительного ограничения по столбцу и значению.
// Количество комментариев к книге
$iCount = Core_Entity::factory('Book', 1)->Comments->getCount();
и с использованием метода-перехватчика:
// Количество активных комментариев к книге
$iCount = Core_Entity::factory('Book', 1)->Comments->getCountByActive(1);
Подсчет количества найденных объектов
Для подсчета количества найденных элементов при использовании ограничений выборки limit()/offset() используется метод sqlCalcFoundRows(). До вызова findAll() необходимо загрузить структуру модели через getTableColumns(), в противном случае запрос на получение структуры модели может стать между запросом на выбору и FOUND_ROWS()
$oBooks = Core_Entity::factory('Book');
// Загружаем структуру модели до FOUND_ROWS()
Core_Entity::factory('Book')->getTableColumns();
// Включаем подсчет количества всех объектов, подходящих под ограничения, ограничиваем выборку
$oBooks->queryBuilder()
->sqlCalcFoundRows()
->offset(0)
->limit(10);
// Выбираем элементы
$aBooks = $oBooks->findAll();
// Подсчет количества найденных элементов с версии 7.0.0
$iCount = Core_QueryBuilder::select()->getFoundRows();
// Подсчет количества найденных элементов в предыдущих версиях
// $row = Core_QueryBuilder::select(array('FOUND_ROWS()', 'count'))->execute()->asAssoc()->current();
// $iCount = $row['count'];
echo "<p>Всего найдено: {$iCount}";
// Вывод найденных объектов
foreach ($aBooks as $oBook)
{
// do something
echo "<p>" . $oBook->name . "";
}
Объединения с другими таблицами через queryBuilder
При объединении с другими таблицами, в результирующий набор выбираются поля из всех таблиц. Учитывая, что модели могут быть заданы только (1) её свойства, (2) data-атрибуты, (3) public-свойства модели, (4) свойства, перехваченные через хуки, то при попытке установить несуществующее свойство будет сгенерировано исключение Exception: Could not call class constructor ..._Model::__construct().
Учитывая вышеизложенное, при объединении таблиц необходимо явно указывать таблицу через ->clearSelect()->select('таблица.*'), из которой нужно выбирать значения, например:
$oBooks = Core_Entity::factory('Book');
$oBooks->queryBuilder()
->join('book_items', 'book_items.book_id', '=', 'books.id')
// ... здесь дополнительные условия, которые требуются
->clearSelect()
->select('books.*');
// Выбираем элементы
$aBooks = $oBooks->findAll();
Пользовательские data-атрибуты
Реализация ORM не позволяет задавать моделям атрибуты, не соответствующие полям в таблице базы данных, за исключением public-свойств или добавленных через хуки обработок собственных полей.
Однако объектам допускается задание и получение пользовательских полей, начинающихся со слова data, например dataMyField. Данные, установленные в data-поле не сохраняются в базу данных и могут быть утрачены при получении одного и того же объекта в разных методах в случае вытеснения объекта, которому они были установлены, из ObjectWatcher.
// Установить значение, вaриант 1, в поле будет содержаться 123
$oObject->dataMyField = 123;
// Установить значение, вaриант 2, в поле будет содержаться abc
$oObject->dataMyField('abc');
// Прочитать и распечатать, распечатает abc
echo $oObject->dataMyField;
Реализация полезна при написании запросов к БД, в которых необходимо получить и сохранить агрегированные данные, например ->select(array('COUNT(id)', 'datacount'))
Кэширование
Метод findAll(), getCount() и методы-перехватчики имеют встроенный кэш, исключающий повторное выполнение запросов при указании одних и тех же условий выборки. В случае, если Вы произвели вставку или удаление объекта, содержащего в предыдущей выборке, может понадобится игнорирование кэша. Для этого в метод findAll() первым параметром, а в методы-перехватчики вторым параметром передайте значение FALSE.
$aBooks = Core_Entity::factory('Book')->findAll(FALSE);
//Implement methods like getByXXX($value, $cache = TRUE) where XXX is the field name
$object = Core_Entity::factory('Book')->getByName('The Catcher in the Rye', FALSE);
//Implement methods like getAllByXXX($value, $cache = TRUE) where XXX is the field name
$aObject = Core_Entity::factory('Book')->getAllByName('The Catcher in the Rye', FALSE);
Ленивая загрузка / Lazy Load
Использование ленивой загрузки позволяет не загружать данные из таблицы до тех пор, пока они не понадобятся на самом деле. Создание объекта с указанием его идентификатора не приводит к возникновению запроса на выборку данных.
$oEntity = Core_Entity::factory('Entity', 123);
// Покажет 123, эти даныне уже заданы и не требуется обращаться к таблице
var_dump($oEntity->id);
// Выполнит запрос к таблице, если данные в объект не были загружены ранее
// Отобразит значение или NULL в случае, если объекта с указанным ID не существует
// При этом если объекта не существует, то ID также будет сброшен в NULL
var_dump($oEntity->name);
Создать объект с загрузкой данных из таблицы можно через метод find():
// Выполнит запрос к таблице
$oEntity = Core_Entity::factory('Entity')->find(123);
// Покажет 123, если объект с указанным ID существует, в противном случае ID будет сброшен в NULL
var_dump($oEntity->id);
Расширение связей существующих моделей
Расширить существующие связи _hasMany, _hasOne, _belongsTo для собственных моделей вы можете через событие modelname.onAfterRelations, устанавливать новое рассчитанное значение методом setRelations(). Очистить ранее закэшированные связи можно методами clearRelationCache() и clearRelationModelCache().
Конфигурационный файл ORM
Конфигурационный файл размещается в modules/core/config/orm.php и содержит массивом имен кэшей и используемое хранилище, например:
<?php
return array (
'cache' => 'memory',
'columnCache' => 'memcache',
'relationCache' => 'memcache'
);
Используемы кэши:
- cache — внутренний кэш ORM, используйте для хранения только memory;
- columnCache — кэш структуры модели, рекомендуем выносить в быстрый кэш, например memcache, если он настроен;
- relationCache — кэш связей модели, рекомендуем выносить в быстрый кэш, например memcache, если он настроен.
Комментарии
-
Без темы
Поправьте если не так понял документацию. Думаю, что для получения значений массива $aComments должен использоваться метод getAllByActive(1) а не getByActive(1) и код должен выглядеть так // С использованием метода-перехватчика $aComments = Core_Entity::factory('Book')->Comments->getAllByActive(1);
Спасибо!
Согласен, в статье ошибка, я бы долго искал причину, если бы не Ваш комментарий.
-
Без темы
Дак система для опытных програмистов
10.12.2012 13:41:29Сергей
-
Подробности
Как начсет того, чтобы составить самоучитель по системе, что-то вроде: «HostCMS от А до Я», начиная с понятия переменной и т.д., иначе нужно иметь достаточно высокий уровень знаний в програмировании, чтобы все это понимать. Я бы приобрел такую книгу.
15.11.2012 23:03:53sersh
Поддерживаю
Самоучитель очень был бы полезен. Не всегда всё помнишь. Открыл шпаргалку и освежил знания...)