В 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 является технологией, которая связывает БД с концепцией ООП. Реализовываться может с помощью шаблонов программирования, мы выбрали Active Record.
Простую и безопасную работу с объектами БД через объекты PHP.
Создадим таблицу `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, настоятельно рекомендуем поработать с ними.
Используются два механизма - пометка на удаление 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; } });
Связи указываются в модели и позволяют получать быстрый доступ к связанным и зависимым объектам.
Используется, когда у объекта текущей модели есть множество зависимых элементов, в примере одна книга имеет несколько комментариев.
В файл модели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;
Объект зависим от другого объекта и содержит в себе внешний ключ.
В файл модели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(). Если добавляемый объект не был сохранен, то перед добавление он будет автоматически сохранене. Пример добавления нового комментария к книге с 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 . ""; }
При объединении с другими таблицами, в результирующий набор выбираются поля из всех таблиц. Учитывая, что модели могут быть заданы только (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();
Реализация 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);
Использование ленивой загрузки позволяет не загружать данные из таблицы до тех пор, пока они не понадобятся на самом деле. Создание объекта с указанием его идентификатора не приводит к возникновению запроса на выборку данных.
$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);
Конфигурационный файл размещается в modules/core/config/orm.php
и содержит массивом имен кэшей и используемое хранилище, например:
<?php
return array (
'cache' => 'memory',
'columnCache' => 'memcache',
'relationCache' => 'memcache'
);
Используемы кэши:
Поправьте если не так понял документацию. Думаю, что для получения значений массива $aComments должен использоваться метод getAllByActive(1) а не getByActive(1) и код должен выглядеть так // С использованием метода-перехватчика $aComments = Core_Entity::factory('Book')->Comments->getAllByActive(1);
Согласен, в статье ошибка, я бы долго искал причину, если бы не Ваш комментарий.
Дак система для опытных програмистов
Как начсет того, чтобы составить самоучитель по системе, что-то вроде: «HostCMS от А до Я», начиная с понятия переменной и т.д., иначе нужно иметь достаточно высокий уровень знаний в програмировании, чтобы все это понимать. Я бы приобрел такую книгу.
Самоучитель очень был бы полезен. Не всегда всё помнишь. Открыл шпаргалку и освежил знания...)