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» — значение):

//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);
Не злоупотребляйте использованием метода find(), используется только в случае прямой необходимости

Конфигурационный файл ORM

Конфигурационный файл размещается в 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);

    17.03.2013 19:38:57
    krapivam
    krapivam

    Спасибо!

    Согласен, в статье ошибка, я бы долго искал причину, если бы не Ваш комментарий.

    14.04.2015 13:56:07
    Levsha
    Levsha
  • Без темы

    Дак система для опытных програмистов

    10.12.2012 13:41:29
    Сергей
  • Подробности

    Как начсет того, чтобы составить самоучитель по системе, что-то вроде: «HostCMS от А до Я», начиная с понятия переменной и т.д., иначе нужно иметь достаточно высокий уровень знаний в програмировании, чтобы все это понимать. Я бы приобрел такую книгу.

    15.11.2012 23:03:53
    sersh

    Поддерживаю

    Самоучитель очень был бы полезен. Не всегда всё помнишь. Открыл шпаргалку и освежил знания...)

    26.01.2018 13:20:41
    Atlantis
    Atlantis