Инструменты пользователя

Инструменты сайта


eaze:samples:одновременное_редактирование_объектов

Отслеживание одновременного редактирования объектов в VT

По умолчанию в VT при сохранении объектов действует правило: «кто последний нажал сохранить, того и форма :)». Попробуем исправить это, поставив следующую задачу:

В VT при редактировании объекта давать сохранять именно ту версию объекта, которая была открыта на тот момент.
Если страница редактирования одновременно была открыта два раза, то при нажатии кнопки «сохранить» во второй раз система не должна позволить сохранить изменения, так как в первый раз уже было произведено сохранение.

Эта схема называется "Optimistic Concurrency Control"

Реализация

Решение данной задачи можно рассматривать в различных вариантах, но мы, как программисты, определим свои ограничения заранее. У объекта могут быть связанные объекты (листы), обычно они редактируются на одной странице, поэтому нам важен сам факт нажатия кнопки «Сохранить и закрыть» или «Применить», а не то, что изменилось в объекте. Если в объекте ничего не поменялось, мы не учитываем это. По своей сути задача сводится к записи времени последнего редактирования объекта.

Попытаемся решить задачу в общем виде.

База данных

Создадим табличку objectHistory, в которую будут записываться все изменения.

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

Утилита ObjectHistoryUtility.phps будет работать только с теми объектами, у которых уже есть Factory. Переменная $InsertsOnly отвечает за режим работы. Если значение true, то в objectHistory будет хронология всех изменений, иначе - только последнее изменение.

Перейдем к интеграции с SaveAction.

Save<Class>Action.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 добавьте:

    <objectHistory>
        <alreadyUpdated><![CDATA[Объект уже был обновлен ранее в %s. Вы не можете применить свои изменения. Снова <a href="%s">переоткройте</a> эту страницу.]]></alreadyUpdated>
        <empty><![CDATA[Не удалось определить время открытия страницы. Вы не можете применить свои изменения. Снова <a href="%s">переоткройте</a> эту страницу.]]></empty>
    </objectHistory>

Результат

При одновременном сохранении объекта второй получит вот такую ошибку:

Плюсы

  • Универсальное решение для любого объекта в VT.
  • Минимальные правки в код.
  • Простая интеграция в существующие проекты.

Минусы

  • Если открыть страницу, ничего не менять, и нажать кнопку «сохранить изменения», то дата последнего изменения обновится, хотя по логике вещей не должна - мы же ничего не меняли:).

Дальнейшее развитие

  • Можно добавить кнопку на яваскрипте - «Все равно сохранить мою версию» (кнопка меняет значение ohCreatedAt на now() и делает submit формы).
  • Можно добавить в таблицу objectHistory поле changes, в котором в сериализованном виде хранить изменившиеся значения полей у объекта (и его листов). После создания такой функциональности берегитесь своей бурной фантазии :).
eaze/samples/одновременное_редактирование_объектов.txt · Последние изменения: 2014/03/23 13:36 — sergeyfast