====== Отслеживание одновременного редактирования объектов в VT ======
По умолчанию в VT при сохранении объектов действует правило: "кто последний нажал сохранить, того и форма :)". Попробуем исправить это, поставив следующую задачу:
>В VT при редактировании объекта давать сохранять именно ту версию объекта, которая была открыта на тот момент.
>Если страница редактирования одновременно была открыта два раза, то при нажатии кнопки "сохранить" во второй раз система не должна позволить сохранить изменения, так как в первый раз уже было произведено сохранение.
Эта схема называется [[https://en.wikipedia.org/wiki/Optimistic_concurrency_control|"Optimistic Concurrency Control"]]
===== Реализация =====
Решение данной задачи можно рассматривать в различных вариантах, но мы, как программисты, определим свои ограничения заранее.
У объекта могут быть связанные объекты (листы), обычно они редактируются на одной странице, поэтому нам важен сам факт нажатия кнопки "Сохранить и закрыть" или "Применить", а не то, что изменилось в объекте.
Если в объекте ничего не поменялось, мы не учитываем это. По своей сути задача сводится к записи времени последнего редактирования объекта.
Попытаемся решить задачу в общем виде.
==== База данных ====
Создадим табличку ''objectHistory'', в которую будут записываться все изменения.
{{:eaze:samples:objecthistory.png? |}}
Create table "objectHistory"
(
"objectHistoryId" Serial NOT NULL,
"objectId" Integer NOT NULL,
"objectType" Varchar(64) NOT NULL,
"userId" Integer NOT NULL,
"userType" Varchar(64) NOT NULL,
"createdAt" Timestamp NOT NULL Default now(),
primary key ("objectHistoryId")
) Without Oids;
Create index "IX_objectHistory_objectIdType" on "objectHistory" using btree ("objectId","objectType");
Create index "IX_objectHistory_createdAt_desc" on "objectHistory" using btree ("createdAt" DESC);
Эта таблица универсальна: мы можем хранить как хронологию изменений, так и только последнее изменение.
==== ObjectHistoryUtility.php ====
Утилита {{:eaze:samples:objecthistoryutility.phps|ObjectHistoryUtility.phps}} будет работать только с теми объектами, у которых уже есть Factory.
Переменная ''$InsertsOnly'' отвечает за режим работы. Если значение ''true'', то в ''objectHistory'' будет хронология всех изменений, иначе - только последнее изменение.
Перейдем к интеграции с SaveAction.
==== SaveAction.php ====
=== beforeAction() ===
/**
* @var DateTime
*/
private $ohCreatedAt;
/**
* @var User
*/
private $user;
/**
* Before Action
*/
protected function beforeAction() {
$this->user = AuthUtility::GetCurrentUser( 'User' );
$this->ohCreatedAt = Request::getDateTime( 'ohCreatedAt' );
// initialize ohCreatedAt
if ( !$this->action && !$this->ohCreatedAt && $this->originalObject ) {
$this->ohCreatedAt = DateTimeWrapper::Now();
} else if ( $this->action == BaseSaveAction::DeleteAction ) {
$this->ohCreatedAt = DateTimeWrapper::Now();
}
Response::setParameter( 'ohCreatedAt', $this->ohCreatedAt );
}
В данном куске кода мы определили переменные ''user'' (текущий пользователь в VT) , ''ohCreatedAt'' (дата открытия страницы редактирования, инициализируем её при первом открытии).
Не забудьте добавить в массив опций ''options'' : ''WithReturningKeys => true'' в конструкторе.
=== validate() ===
Добавим в ''validate()'' следующую проверку перед ''return $errors'':
ObjectHistoryUtility::ValidateObject( $this->originalObject, $this->ohCreatedAt, $errors );
Метод ''ObjectHistoryUtility::ValidateObject'' модифицирует массив ''$errors'' в случае необходимости, проверка идет при заполненном поле ''$this->originalObject''.
=== add() ===
protected function add( $object ) {
ConnectionFactory::BeginTransaction();
$result = parent::$factory->Add( $object, $this->options );
$result = $result && ObjectHistoryUtility::Save( $object, $this->user );
ConnectionFactory::CommitTransaction( $result );
return $result;
}
=== update() ===
protected function update( $object ) {
ConnectionFactory::BeginTransaction();
$result = parent::$factory->Update( $object );
$result = $result && ObjectHistoryUtility::Save( $object, $this->user );
if ( $result ) {
Response::setParameter( 'ohCreatedAt', ObjectHistoryUtility::$LastCreatedAt );
}
ConnectionFactory::CommitTransaction( $result );
return $result;
}
Обновление ''ohCreatedAt'' необходимо для кнопки "Применить".
code php
==== edit.tmpl.php ====
Перед подключением шаблона ''data.tmpl.php'' добавьте:
= FormHelper::FormHidden( 'ohCreatedAt', !empty( $ohCreatedAt ) ? $ohCreatedAt->format('c') : '' ); ?>
==== data.tmpl.php ====
После ''$errors["fatal"]'' добавьте:
// Render Errors from Object History
echo ObjectHistoryUtility::RenderErrors( $errors );
==== ru.xml ====
Перед закрытием тега ''errors'' добавьте:
переоткройте эту страницу.]]>
переоткройте эту страницу.]]>
===== Результат =====
При одновременном сохранении объекта второй получит вот такую ошибку:
{{ :eaze:samples:objecthistory-tv.png? |}}
==== Плюсы ====
* Универсальное решение для любого объекта в VT.
* Минимальные правки в код.
* Простая интеграция в существующие проекты.
==== Минусы ====
* Если открыть страницу, ничего не менять, и нажать кнопку "сохранить изменения", то дата последнего изменения обновится, хотя по логике вещей не должна - мы же ничего не меняли:).
==== Дальнейшее развитие ====
* Можно добавить кнопку на яваскрипте - "Все равно сохранить мою версию" (кнопка меняет значение ohCreatedAt на now() и делает submit формы).
* Можно добавить в таблицу ''objectHistory'' поле ''changes'', в котором в сериализованном виде хранить изменившиеся значения полей у объекта (и его листов). После создания такой функциональности берегитесь своей бурной фантазии :).