====== Отслеживание одновременного редактирования объектов в 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'' добавьте: 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'', в котором в сериализованном виде хранить изменившиеся значения полей у объекта (и его листов). После создания такой функциональности берегитесь своей бурной фантазии :).