@loktionov
Программист

Почему PDO::lastInsertId возвращает неверный ID при частых вставках в БД?

Существует веб-приложение (Win7x64, MS Sql Server 2012, Apache 2.4, PHP 5.6, Yii 1.1).
Около сотни пользователей постоянно наполняют одну таблицу. В случае, если разные пользователи в одно и тоже время произвели вставку строки, первому возвращается ID вставленной строки второго и дальнейшие действия, например сохранение истории изменений, ведется с неверным ID.
Yii использует PDO, в частности метод lastInsertId для получения айдишника, но описания подобной проблемы в интернете не встретил.
Почему такое происходит и как этого избежать?
Заранее спасибо.

upd.
Упрощенно код выглядит так:
//Контроллер
public function actionCreate(){
    $model = new Pacient();
    $model->attributes = $_POST['Pacient'];
    $model->save();
}
//Модель
public function beforeSave(){
    if (empty($this->NNAPR)) {
            if (!$this->setNNAPR())
                return false;
        }
    return parent::beforeSave();
}
public function afterSave(){
    parent::afterSave();
    $history = new PacientHistory();
    //здесь уже неверный ID
    $history->ID_PACIENT = $this->ID;
    $history->save();
}


Записи добавляются не только через форму, но и экспортируются xml-файлами, но код используется почти один и тот же. Ситуация повторяется и для файлов и для ручного ввода.
На 2000 добавленных записей в день возникает от 0 до 3 таких ошибок.

upd.2
Для чистоты эксперимента, я создал две новые таблица Collision и CollisionHistory:
CREATE TABLE [dbo].[Collision](
	[id] [int] IDENTITY(1,1) NOT NULL,
	[uuid] [varchar](50) NULL,
 CONSTRAINT [PK_Collision] PRIMARY KEY CLUSTERED 
	(
		[id] ASC
	) ON [PRIMARY]
)

CREATE TABLE [dbo].[CollisionHistory](
	[id] [int] IDENTITY(1,1) NOT NULL,
	[collision_id] [int] NULL,
	[uuid] [varchar](50) NULL,
 CONSTRAINT [PK_CollisionHistory] PRIMARY KEY CLUSTERED 
	(
		[id] ASC
	)
)


На их основе в gii генерю модели.
В модель Collision добавляю код:
public function afterSave(){
        parent::afterSave();
        $history = new CollisionHistory();
        $history->collision_id = $this->id;
        $history->uuid = $this->uuid;
        $history->save(false);
    }

Пишу контроллер
class CollisionController extends CController
{

    public function actionTest()
    {
        for ($i = 0; $i < 1000; $i++) {
            $model = new Collision();
            $model->uuid = $this->UUIDv4();
            $model->save(false);
        }
    }

    //метод с php.net
    private function UUIDv4()
    {
        return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',

            // 32 bits for "time_low"
            mt_rand(0, 0xffff), mt_rand(0, 0xffff),

            // 16 bits for "time_mid"
            mt_rand(0, 0xffff),

            // 16 bits for "time_hi_and_version",
            // four most significant bits holds version number 4
            mt_rand(0, 0x0fff) | 0x4000,

            // 16 bits, 8 bits for "clk_seq_hi_res",
            // 8 bits for "clk_seq_low",
            // two most significant bits holds zero and one for variant DCE1.1
            mt_rand(0, 0x3fff) | 0x8000,

            // 48 bits for "node"
            mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
        );
    }

}


Затем на счет три с коллегой запускаем actionTest каждый со своего компьютера.
В итоге на 2000 инсертов в таблицу Collision 419 повторений collision_id в таблице CollisionHistory.

Тест на чистом PDO дал тот же результат, последний айдишник возвращается для другой записи.
  • Вопрос задан
  • 1439 просмотров
Решения вопроса 1
@loktionov Автор вопроса
Программист
Провел тест на чистом PDO, поменял несколько версий, но результат тот же. Метод PDO::lastinsertid возвращает самый последний id для таблицы, как функция MSSQL IDENT_CURRENT()

То есть, если между вставкой и получением id произошла вставка в другой сессии, то возвращается id из второй сессии.

Свою проблему решил созданием хранимой процедуры для вставок в самую часто-используемую таблицу, в которой выходной параметр заполняется функцией SCOPE_IDENTITY().

Кстати, используя выходной параметр в хранимой процедуре, не стоит забывать про инструкцию SET NOCOUNT ON, которая отключает вывод количества затронутых процедурой строк вместо выходного параметра.
Ответ написан
Комментировать
Пригласить эксперта
Ответы на вопрос 3
Sanasol
@Sanasol
нельзя просто так взять и загуглить ошибку
Это невозможно.
lastinsert возвращает последню запись для текущего соединения с базой. Которое никак не может быть у другого пользователя.

Значит ошибка не в этом.
Если конечно вы не проверяли открывая две вкладки в одном бразуере :D
Ответ написан
copist
@copist
Empower people to give
Нет ли у вас такого, что запрос всталяет сразу несколько записей?
Тогда вернётся ID самой последней вставленной

P.S. Кстати, использованный метод UUIDv4 не самый надёжный в плане уникальности ключей
Ответ написан
Пробовали обернуть запросы вставки и получения последнего id в транзакцию?
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы