Перейти к содержимому
Поиск
Php Разработка

PHP: полный мануал с примерами, функциями и ООП

Практический мануал PHP: от переменных и читаемого кода до безопасных форм, интеграций, базы данных и ООП на примере магазина. Учебный маршрут, поиск функций, подробные примеры, разбор ошибок и исходники для скачивания.

Александр НекрасовИзменено:
Практический мануал PHP: от переменных и функций к объектной модели интернет-магазина
PHP 8.5.11468 глав6256 карточек справочника
Дерево руководства

Глава 52 · После подключения SQLSRV, параметризованных команд и чтения результата

SQLSRV: перевод, резерв и откат без частичных изменений

Перевод со связанной записью, поздний отказ INSERT, неполный резерв и забытый commit: четыре самостоятельных опыта с видимыми остатками и явными правилами завершения.

Зачем связывать несколько записей в одну операцию

В сервисе нужно перевести внутренний баланс, записать квитанцию или зарезервировать сразу несколько товаров. Если первое действие сохранилось, а второе нет, данные противоречат задаче. Транзакция связывает изменения: приложение либо подтверждает подходящий итог, либо отменяет незавершённую группу. В этой главе виден результат каждого варианта — остатки до операции, внутри неё и после завершения.

Одна операция заказа — одна граница сохранения
  1. Начальные данные 10000 / 2500
  2. begin открывает группу
  3. списание 1250 → зачисление 1250 → запись TX-150-A
  4. Все проверки прошли: commit → 8750 / 3750
  5. Есть отказ: rollback → 10000 / 2500

Модель похожа на комплект документов, который принимают вместе. Но сервер не ждёт commit, чтобы начать выполнять SQL: запросы работают сразу, свои изменения читаются в текущем соединении. Другим соединениям доступны данные по правилам изоляции; рисунок не обещает конкретных блокировок и не заменяет изучение конкуренции. Здесь связь одна: все действия выполняются через один $db.

Перед этим маршрутом нужны подключение и ошибки SQLSRV и параметры, строки и счётчики. Переменная $db хранит открытое соединение; результат функции сравнивается с false строго, чтобы не спутать отказ с нулём. try/finally гарантирует выполнение блока завершения при обычном выходе и исключении; сам по себе он не откатывает SQL.

Самая короткая граница

if (sqlsrv_begin_transaction($db) === false) {
    throw new RuntimeException("Транзакция не начата");
}
if (sqlsrv_rollback($db) === false) {
    throw new RuntimeException("Отмена не подтверждена");
}
echo "Пустая группа завершена";

Этот фрагмент требует соединения и ничего не записывает: begin открывает группу, rollback закрывает её отменой, echo показывает результат. Две пары круглых скобок передают $db функциям; === false проверяет отказ. Полные самостоятельные программы ниже добавляют предметные записи, диагностику, защиту от частичного результата и реальные таблицы.

РежимЧто сохраняется
Auto-commitОбычная успешная отдельная команда сохраняется автоматически; так начинается подключение SQLSRV.
Явная группаПосле успешного begin до commit/rollback несколько команд относятся к одной транзакции этого соединения.
CommitУспешное подтверждение завершает группу; затем снова действует auto-commit.
RollbackОтменяет изменения незавершённой группы и возвращает auto-commit. Ранее сохранённые данные остаются.

Управляйте транзакцией через sqlsrv_begin_transaction, sqlsrv_commit и sqlsrv_rollback. Не смешивайте их с отправленным строкой T-SQL BEGIN/COMMIT: драйвер и сервер могут расходиться в представлении о состоянии. Повторный begin внутри активной группы не создаёт независимо подтверждаемый вложенный перевод.

Перевести сумму и сохранить связанную запись журнала

Внутренний баланс сервиса должен перевести 1250 копеек со счёта 1 на счёт 2 и записать номер операции. Потерянное зачисление или запись журнала без перевода недопустимы. Начальные остатки 10000 и 2500; после успешного завершения другой сеанс увидит 8750 и 3750, а в журнале — одну запись TX-150-A.

Что изменяется в этой программе
  1. До: счёт 1 = 10000, счёт 2 = 2500, журнал пуст
  2. begin_transaction открывает одну группу на $db
  3. UPDATE по id 1 уменьшает остаток на 1250 только при достаточной сумме
  4. UPDATE по id 2 прибавляет 1250; INSERT записывает TX-150-A
  5. commit(true) завершает группу
  6. Новое соединение читает 8750 / 3750 и единственную запись журнала

Сначала короткий вызов

$stmt = sqlsrv_query($db,
    "UPDATE $qa SET balance_minor = balance_minor - ? WHERE id = ? AND balance_minor >= ?",
    [1250, 1, 1250]);
// Сначала проверить false, затем rows_affected($stmt) === 1.
// Если условие не выполнено, вся группа должна быть отменена.

$qa — проверенное имя своей таблицы из полного примера, а числа передаются параметрами. WHERE одновременно выбирает счёт и проверяет достаточность остатка. SQLSRV возвращает ресурс команды; rows_affected отвечает, изменилась ли нужная строка. Само условие проверяется на сервере в момент UPDATE. Проверка прежнего остатка в PHP отдельным SELECT оставляет промежуток для чужого изменения.

Какой результат получится

НаблюдениеРезультат
Подтверждение commit успешнода
Счёт 1, копейки8750
Счёт 2, копейки3750
Сумма двух остатков, копейки12500
Номер записи журналаTX-150-A
Записей этого перевода1
Проверено новым соединениемда
Перевести сумму и сохранить связанную запись журналаСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=$config=null;$owned=$removed=$rows=$checks=$observations=[];
$runtime=s150Runtime();
try {

    $config=s150Config();$db=s150Connect($config);$runtime=s150Runtime($db,$config);

    $accounts=s150OwnTable($db,$owned,'commit_accounts','(id int NOT NULL PRIMARY KEY, balance_minor int NOT NULL CHECK(balance_minor >= 0))');
    $journal=s150OwnTable($db,$owned,'commit_journal','(operation_id nvarchar(40) NOT NULL PRIMARY KEY, from_id int NOT NULL, to_id int NOT NULL, amount_minor int NOT NULL CHECK(amount_minor > 0))');
    $qa=s150Qualified($accounts);$qj=s150Qualified($journal);
    s150Command($db,"INSERT INTO $qa (id,balance_minor) VALUES (1,10000),(2,2500)");
    s150Command($db,'SET NOCOUNT OFF');
    $amount=1250;$from=1;$to=2;$operation='TX-150-A';$active=false;$committed=false;
    if ($amount<=0 || $from===$to) {throw new InvalidArgumentException('Нужны положительная сумма и разные счета');}
    try {
        if (sqlsrv_begin_transaction($db)===false) {s150Failure('Начало перевода');}
        $active=true;
        $debit=s150Query($db,"UPDATE $qa SET balance_minor=balance_minor-? WHERE id=? AND balance_minor>=?",[$amount,$from,$amount]);
        try {$debited=sqlsrv_rows_affected($debit); if ($debited===false) {s150Failure('Количество списанных строк');}} finally {sqlsrv_free_stmt($debit);}
        if ($debited!==1) {throw new RuntimeException('Нет счёта или недостаточно средств');}
        $credit=s150Query($db,"UPDATE $qa SET balance_minor=balance_minor+? WHERE id=?",[$amount,$to]);
        try {$credited=sqlsrv_rows_affected($credit); if ($credited===false) {s150Failure('Количество зачисленных строк');}} finally {sqlsrv_free_stmt($credit);}
        if ($credited!==1) {throw new RuntimeException('Счёт получателя не найден');}
        s150Command($db,"INSERT INTO $qj (operation_id,from_id,to_id,amount_minor) VALUES (?,?,?,?)",[$operation,$from,$to,$amount]);
        if (sqlsrv_commit($db)===false) {s150Failure('Подтверждение перевода');}
        $active=false;$committed=true;
    } finally {
        if ($active && sqlsrv_rollback($db)===false) {s150Failure('Откат незавершённого перевода');}
    }
    // A second connection reads only after COMMIT: this is a fresh receipt, not a PHP copy.
    $reader=s150Connect($config);
    try {
        $balances=s150Read($reader,"SELECT id,balance_minor FROM $qa WHERE id IN (?,?) ORDER BY id",[$from,$to]);
        $receipt=s150Read($reader,"SELECT operation_id,from_id,to_id,amount_minor FROM $qj WHERE operation_id=?",[$operation]);
    } finally {sqlsrv_close($reader);}
    $sum=array_sum(array_column($balances,'balance_minor'));
    $rows=[['Подтверждение commit успешно',$committed],['Счёт 1, копейки',$balances[0]['balance_minor']],
        ['Счёт 2, копейки',$balances[1]['balance_minor']],['Сумма двух остатков, копейки',$sum],
        ['Номер записи журнала',$receipt[0]['operation_id']],['Записей этого перевода',count($receipt)],['Проверено новым соединением',true]];
    $checks=['commit_succeeded'=>$committed,'debit_one'=>$debited===1,'credit_one'=>$credited===1,
        'sender_balance'=>$balances[0]['balance_minor']===8750,'receiver_balance'=>$balances[1]['balance_minor']===3750,
        'sum_conserved'=>$sum===12500,'receipt_once'=>count($receipt)===1,'receipt_amount'=>$receipt[0]['amount_minor']===$amount,
        'receipt_parties'=>$receipt[0]['from_id']===$from && $receipt[0]['to_id']===$to];
    $observations=['balances_after_commit'=>$balances,'journal_after_commit'=>$receipt,'money_unit'=>'minor units, kopecks'];

} finally {
    if (is_resource($db)) {try {$removed=s150Cleanup($db,$owned);} finally {sqlsrv_close($db);}}
}

s150Report('sqlsrv150-transfer-commit','Перевести сумму и сохранить связанную запись журнала',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Подтверждение commit успешно — да
Счёт 1, копейки — 8750
Счёт 2, копейки — 3750
Сумма двух остатков, копейки — 12500
Номер записи журнала — TX-150-A
Записей этого перевода — 1
Проверено новым соединением — да

Запустите в отдельной папке по README. lab.php открывает ограниченный учебный LocalDB, создаёт только собственные таблицы и сохраняет результат; сама транзакция и её завершение видны в example.php. Учебные таблицы после чтения удаляются.

Таблица сохранённого запуска · Данные и наблюдения JSON.

Границы и ошибки

Это учебный перевод внутри одной базы, не платёж во внешнем банке. Целые копейки выбраны для двухзначной денежной шкалы; допустимый диапазон int столбца и размер суммы нужно определить в приложении. Уникальный operation_id внутри той же транзакции не позволяет сохранить второй перевод с тем же номером. При повторном запросе сначала сверяйте сохранённую квитанцию и параметры; нельзя выдавать чужую операцию за успех. Тест не моделирует параллельную нагрузку или сбой питания. Если commit вернул false, код не сообщает «перевод готов»: причина сохраняется, незавершённая группа пытается откатиться. При обрыве связи в момент подтверждения результат следует выяснять по уникальному номеру, а не безусловно повторять перевод.

Практическая ситуация: несколько записей уже внесены, а последняя команда не удалась. Разбор исходной проблемы на Stack Overflow. Связываем действия одним открытым соединением и одной явной транзакцией; подтверждаем только после проверок всех команд. Удаление ранее добавленных строк вручную не заменяет rollback.

Измените одно требование

Поменяйте сумму на 10001 при начальном остатке 10000. Какие строки не должны измениться?

Как проверить изменение

Условный UPDATE не изменит строку, проверка количества остановит операцию, finally выполнит rollback. Второй счёт и журнал должны остаться прежними. Для штатного отказа интерфейс показывает «недостаточно средств»; неожиданный отказ драйвера обрабатывается отдельно.

Откатить перевод, если журнал отверг последний INSERT

Две команды изменения остатков уже выполнились, но последний INSERT столкнулся с занятым номером журнала. Покажем именно поздний отказ: внутри транзакции видны 8750 / 3750, после rollback возвращаются 10000 / 2500. Запись, которая существовала ещё до begin_transaction, остаётся нетронутой.

Что изменяется в этой программе
  1. До begin: остатки 10000 / 2500, номер TX-150-BUSY уже занят
  2. Два UPDATE временно дают 8750 / 3750
  3. INSERT того же ключа получает отказ ограничения PRIMARY KEY
  4. Сразу копируем SQLSTATE/code в $saved
  5. rollback отменяет оба UPDATE этой группы
  6. Новый SELECT видит исходные остатки и прежнюю запись 77

Сначала короткий вызов

$stmt = sqlsrv_query($db, "INSERT INTO $qj (operation_id, amount_minor) VALUES (?, ?)", $params);
if ($stmt === false) {
    $saved = sqlsrv_errors(SQLSRV_ERR_ALL); // До следующей операции драйвера.
    $ok = sqlsrv_rollback($db);
    // Проверить $ok; сохранённая причина находится в $saved.
}

PRIMARY KEY запрещает повторное значение ключа. false относится к этой команде; предыдущие команды не становятся автоматически «не выполненными». Явный rollback отменяет группу. $saved — отдельный массив PHP, поэтому следующая операция драйвера не стирает нашу копию причины. Полная программа публикует только SQLSTATE и числовые коды, не сырой текст с именами сервера.

Какой результат получится

НаблюдениеРезультат
До отказа внутри транзакции8750 / 3750
Последний INSERT отклонёнда
Причина сохранена до rollbackда
Откат выполненда
После отката, счёт 110000
После отката, счёт 22500
Прежняя запись журнала сохраненада
Откатить перевод, если журнал отверг последний INSERTСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=$config=null;$owned=$removed=$rows=$checks=$observations=[];
$runtime=s150Runtime();
try {

    $config=s150Config();$db=s150Connect($config);$runtime=s150Runtime($db,$config);

    $accounts=s150OwnTable($db,$owned,'rollback_accounts','(id int NOT NULL PRIMARY KEY, balance_minor int NOT NULL CHECK(balance_minor >= 0))');
    $journal=s150OwnTable($db,$owned,'rollback_journal','(operation_id nvarchar(40) NOT NULL PRIMARY KEY, amount_minor int NOT NULL)');
    $qa=s150Qualified($accounts);$qj=s150Qualified($journal);
    s150Command($db,"INSERT INTO $qa (id,balance_minor) VALUES (1,10000),(2,2500)");
    s150Command($db,"INSERT INTO $qj (operation_id,amount_minor) VALUES (?,?)",['TX-150-BUSY',77]);
    $active=false;$failed=false;$rolledBack=false;$saved=[];
    try {
        if (sqlsrv_begin_transaction($db)===false) {s150Failure('Начало отказавшего перевода');}
        $active=true;
        s150Command($db,"UPDATE $qa SET balance_minor=balance_minor-? WHERE id=?",[1250,1]);
        s150Command($db,"UPDATE $qa SET balance_minor=balance_minor+? WHERE id=?",[1250,2]);
        $inside=s150Read($db,"SELECT id,balance_minor FROM $qa ORDER BY id");
        $duplicate=sqlsrv_query($db,"INSERT INTO $qj (operation_id,amount_minor) VALUES (?,?)",['TX-150-BUSY',1250],['QueryTimeout'=>10]);
        $saved=s150Diagnostics(sqlsrv_errors(SQLSRV_ERR_ALL));
        $failed=$duplicate===false;
        if (is_resource($duplicate)) {sqlsrv_free_stmt($duplicate);}
        if (!$failed) {throw new RuntimeException('Ожидаемый дубликат не был отклонён');}
        if (sqlsrv_rollback($db)===false) {s150Failure('Откат после отказа журнала');}
        $active=false;$rolledBack=true;
    } finally {
        if ($active && sqlsrv_rollback($db)===false) {s150Failure('Аварийный откат примера');}
    }
    $after=s150Read($db,"SELECT id,balance_minor FROM $qa ORDER BY id");
    $journalRows=s150Read($db,"SELECT operation_id,amount_minor FROM $qj ORDER BY operation_id");
    $codes=array_column($saved,'code');
    $rows=[['До отказа внутри транзакции',$inside[0]['balance_minor'].' / '.$inside[1]['balance_minor']],
        ['Последний INSERT отклонён',$failed],['Причина сохранена до rollback',count($saved)>0],['Откат выполнен',$rolledBack],
        ['После отката, счёт 1',$after[0]['balance_minor']],['После отката, счёт 2',$after[1]['balance_minor']],
        ['Прежняя запись журнала сохранена',$journalRows===[['operation_id'=>'TX-150-BUSY','amount_minor'=>77]]]];
    $checks=['pending_balances_visible'=>$inside[0]['balance_minor']===8750 && $inside[1]['balance_minor']===3750,
        'duplicate_rejected'=>$failed,'diagnostic_saved'=>count($saved)>0,
        'unique_violation_code'=>in_array(2627,$codes,true)||in_array(2601,$codes,true),
        'rollback_succeeded'=>$rolledBack,'balances_restored'=>$after===[['id'=>1,'balance_minor'=>10000],['id'=>2,'balance_minor'=>2500]],
        'old_journal_preserved'=>$journalRows===[['operation_id'=>'TX-150-BUSY','amount_minor'=>77]]];
    $observations=['inside_before_failure'=>$inside,'saved_diagnostic_codes'=>$saved,'after_rollback'=>$after,'journal'=>$journalRows];

} finally {
    if (is_resource($db)) {try {$removed=s150Cleanup($db,$owned);} finally {sqlsrv_close($db);}}
}

s150Report('sqlsrv150-transfer-rollback','Откатить перевод, если журнал отверг последний INSERT',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
До отказа внутри транзакции — 8750 / 3750
Последний INSERT отклонён — да
Причина сохранена до rollback — да
Откат выполнен — да
После отката, счёт 1 — 10000
После отката, счёт 2 — 2500
Прежняя запись журнала сохранена — да

Запустите в отдельной папке по README. lab.php открывает ограниченный учебный LocalDB, создаёт только собственные таблицы и сохраняет результат; сама транзакция и её завершение видны в example.php. Учебные таблицы после чтения удаляются.

Таблица сохранённого запуска · Данные и наблюдения JSON.

Границы и ошибки

Каждая команда здесь отправляется отдельно, поэтому отказ проверяется непосредственно после неё. Для пакета из нескольких SQL-команд успешный первый результат ещё не доказывает успех последующих: их нужно последовательно прочитать и проверить. rollback отменяет изменения текущей транзакции; он не удаляет старые записи и не отменяет уже подтверждённый commit. Этот опыт намеренно создаёт конфликт номера, а не утверждает, что любое исключение означает дубликат. CHECK, внешние ключи, таймауты и deadlock имеют другие причины и правила повторения.

В одной отправленной строке SQL первая команда успешна, а ошибка последующей не видна по первому ресурсу. Разбор исходной проблемы на Stack Overflow. Для первого знакомства используем отдельные проверяемые команды и API транзакций драйвера. Пакеты требуют обработки всех результатов; успешный первый результат не равен успешной всей операции. T-SQL BEGIN/COMMIT из обсуждения не переносим в PHP вместо API драйвера.

Измените одно требование

Замените новый номер на TX-150-FREE. Что изменить в ветвлении, чтобы получить успешный перевод?

Как проверить изменение

INSERT должен пройти; вместо ожидания ошибки требуется проверенный commit. Тогда остатки 8750 / 3750 сохранятся, а журнал будет содержать две разные записи. Нельзя просто удалить проверку и всё равно всегда вызывать rollback.

Отменить весь резерв, если второй позиции не хватает

Заказу нужны 3 единицы первой позиции и 2 второй. На складе 8 и 1. Первая команда резервирования подходит, вторая корректна по SQL, но не находит строку с достаточным остатком. Это штатный отказ по правилу заказа. Если частичная комплектация не разрешена, возвращаем первую позицию откатом всей группы.

Что изменяется в этой программе
  1. Остатки 8/1; заказ просит 3/2
  2. begin открывает резерв заказа
  3. UPDATE id1 с available>=3 даёт 5, изменена 1 строка
  4. UPDATE id2 с available>=2 даёт 0 строк, ошибка SQL не возникает
  5. Проверка приложения отказывает всему резерву
  6. rollback → вновь 8/1, отрицательных остатков нет

Сначала короткий вызов

$stmt = sqlsrv_query($db,
    "UPDATE $q SET available = available - ? WHERE id = ? AND available >= ?",
    [$qty, $id, $qty]);
// false — отказ SQL; rows_affected === 0 — условие не выполнено.
// При запрете частичного заказа обе ветки отменяют весь резерв.

Здесь ноль — осмысленное количество, а не false. Успешная команда могла не найти ни одной строки. SET NOCOUNT OFF в полном файле делает счётчик доступным; неизвестное -1 нельзя принимать за успешное резервирование. В реальном запросе ноль также возможен для несуществующего ID, поэтому интерфейс выбирает корректное общее сообщение либо отдельно уточняет причину.

Какой результат получится

НаблюдениеРезультат
Первая команда изменила строк1
Вторая команда изменила строк0
SQL второй команды выполнилсяда
До отката, остаток первой позиции5
Резерв отклонён целикомда
После отката, позиция 18
После отката, позиция 21
Отменить весь резерв, если второй позиции не хватаетСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=$config=null;$owned=$removed=$rows=$checks=$observations=[];
$runtime=s150Runtime();
try {

    $config=s150Config();$db=s150Connect($config);$runtime=s150Runtime($db,$config);

    $stock=s150OwnTable($db,$owned,'stock_refusal','(id int NOT NULL PRIMARY KEY, available int NOT NULL CHECK(available >= 0))');
    $q=s150Qualified($stock);
    s150Command($db,"INSERT INTO $q (id,available) VALUES (1,8),(2,1)");
    s150Command($db,'SET NOCOUNT OFF');
    $active=false;$refused=false;$rolledBack=false;
    try {
        if (sqlsrv_begin_transaction($db)===false) {s150Failure('Начало резерва');}
        $active=true;
        $first=s150Query($db,"UPDATE $q SET available=available-? WHERE id=? AND available>=?",[3,1,3]);
        try {$count1=sqlsrv_rows_affected($first); if ($count1===false) {s150Failure('Счётчик первой позиции');}} finally {sqlsrv_free_stmt($first);}
        if ($count1!==1) {throw new RuntimeException('Первая учебная позиция должна резервироваться');}
        $second=s150Query($db,"UPDATE $q SET available=available-? WHERE id=? AND available>=?",[2,2,2]);
        try {$count2=sqlsrv_rows_affected($second); if ($count2===false) {s150Failure('Счётчик второй позиции');}} finally {sqlsrv_free_stmt($second);}
        $refused=$count2===0;
        if (!$refused) {throw new RuntimeException('Ожидался отказ по остатку второй позиции');}
        $inside=s150Read($db,"SELECT id,available FROM $q ORDER BY id");
        if (sqlsrv_rollback($db)===false) {s150Failure('Откат неполного резерва');}
        $active=false;$rolledBack=true;
    } finally {
        if ($active && sqlsrv_rollback($db)===false) {s150Failure('Аварийный откат резерва');}
    }
    $after=s150Read($db,"SELECT id,available FROM $q ORDER BY id");
    $rows=[['Первая команда изменила строк', $count1],['Вторая команда изменила строк', $count2],
        ['SQL второй команды выполнился',true],['До отката, остаток первой позиции',$inside[0]['available']],
        ['Резерв отклонён целиком',$refused && $rolledBack],['После отката, позиция 1',$after[0]['available']],['После отката, позиция 2',$after[1]['available']]];
    $checks=['first_changed'=>$count1===1,'second_zero'=>$count2===0,'business_refusal'=>$refused,
        'first_was_decreased'=>$inside[0]['available']===5,'second_unchanged'=>$inside[1]['available']===1,
        'rollback_succeeded'=>$rolledBack,'full_stock_restored'=>$after===[['id'=>1,'available'=>8],['id'=>2,'available'=>1]]];
    $observations=['requested'=>[['id'=>1,'qty'=>3],['id'=>2,'qty'=>2]],'inside_before_rollback'=>$inside,'after_rollback'=>$after];

} finally {
    if (is_resource($db)) {try {$removed=s150Cleanup($db,$owned);} finally {sqlsrv_close($db);}}
}

s150Report('sqlsrv150-stock-refusal','Отменить весь резерв, если второй позиции не хватает',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Первая команда изменила строк — 1
Вторая команда изменила строк — 0
SQL второй команды выполнился — да
До отката, остаток первой позиции — 5
Резерв отклонён целиком — да
После отката, позиция 1 — 8
После отката, позиция 2 — 1

Запустите в отдельной папке по README. lab.php открывает ограниченный учебный LocalDB, создаёт только собственные таблицы и сохраняет результат; сама транзакция и её завершение видны в example.php. Учебные таблицы после чтения удаляются.

Таблица сохранённого запуска · Данные и наблюдения JSON.

Границы и ошибки

qty проверяется как положительное целое до начала транзакции; в примере это фиксированные 3 и 2. Условный UPDATE устраняет схему «прочитать все остатки → проверить в PHP → записать устаревшее значение». Транзакция нужна дополнительно, чтобы связать разные позиции. Для большого заказа меняйте строки в согласованном порядке и держите группу короткой; не ждите ответа пользователя внутри неё. Этот пример не является тестом конкурентной нагрузки. Если разрешён частичный заказ, это другое правило продукта: нужно явно сохранить получившийся состав, а не незаметно заменить отказ частичным успехом.

Измените одно требование

Разрешите второй позиции начальный остаток 2. Как должен выглядеть успешный резерв?

Как проверить изменение

Обе команды изменят по одной строке; итог должен быть 5 / 0. Замените ветку ожидаемого отказа на проверку обеих квитанций и подтверждённый commit. При прежнем безусловном rollback данные останутся 8 / 2 — тест должен обнаружить это.

Увидеть, почему UPDATE без commit исчезает после закрытия

Страница пишет «UPDATE выполнен», но после завершения запроса значение осталось прежним. Воспроизведём один конкретный вариант: приложение открыло явную транзакцию и забыло commit. Сначала собственный SELECT увидит 7500, затем новый вход после закрытия увидит 10000. Отдельный UPDATE без begin_transaction сохранит 9900 автоматически.

Что изменяется в этой программе
  1. В таблице подтверждённое значение 10000
  2. begin → UPDATE 7500; свой SELECT уже видит 7500
  3. close без commit завершает соединение и отменяет его транзакцию
  4. Новое соединение читает 10000
  5. UPDATE 9900 без явного begin работает в auto-commit
  6. Ещё одно соединение читает 9900

Сначала короткий вызов

sqlsrv_begin_transaction($db); // В полном коде проверяется false.
// UPDATE выполнен; чтение на этом соединении уже видит новое значение.
sqlsrv_close($db);             // Здесь намеренно нет commit.
// Новый вход проверит сохранённое состояние базы.

begin не откладывает выполнение запросов до commit. Команды выполняются сразу, а своё соединение видит свои изменения. commit завершает и подтверждает группу. В эксперименте закрытие заменяет ожидаемое подтверждение откатом. После sqlsrv_close сохранённый дескриптор нельзя использовать для следующего SELECT; программа получает новое соединение.

Какой результат получится

НаблюдениеРезультат
До закрытия своё соединение видит7500
Соединение закрытода
Новое соединение после незавершённой транзакции10000
Новое изменение без begin_transaction9900
Увидеть, почему UPDATE без commit исчезает после закрытияСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=$config=null;$owned=$removed=$rows=$checks=$observations=[];
$runtime=s150Runtime();
try {

    $config=s150Config();$db=s150Connect($config);$runtime=s150Runtime($db,$config);

    $account=s150OwnTable($db,$owned,'close_rollback','(id int NOT NULL PRIMARY KEY, balance_minor int NOT NULL)');
    $q=s150Qualified($account);
    s150Command($db,"INSERT INTO $q (id,balance_minor) VALUES (1,10000)");
    $active=false;
    try {
        if (sqlsrv_begin_transaction($db)===false) {s150Failure('Начало незавершённого изменения');}
        $active=true;
        s150Command($db,"UPDATE $q SET balance_minor=? WHERE id=?",[7500,1]);
        $inside=s150Read($db,"SELECT balance_minor FROM $q WHERE id=?",[1])[0]['balance_minor'];
        // Intentionally omit COMMIT: closing this connection rolls its transaction back.
        $closed=sqlsrv_close($db);
        if ($closed!==true) {s150Failure('Закрытие учебного соединения');}
        $active=false;$db=null;
    } finally {
        if ($active && is_resource($db) && sqlsrv_rollback($db)===false) {s150Failure('Аварийный откат опыта закрытия');}
    }
    $db=s150Connect($config);
    $after=s150Read($db,"SELECT balance_minor FROM $q WHERE id=?",[1])[0]['balance_minor'];
    // This new statement is not inside an explicit transaction: demonstrate auto-commit.
    s150Command($db,"UPDATE $q SET balance_minor=? WHERE id=?",[9900,1]);
    $reader=s150Connect($config);
    try {$auto=s150Read($reader,"SELECT balance_minor FROM $q WHERE id=?",[1])[0]['balance_minor'];}
    finally {sqlsrv_close($reader);}
    $rows=[['До закрытия своё соединение видит',$inside],['Соединение закрыто',$closed],
        ['Новое соединение после незавершённой транзакции',$after],['Новое изменение без begin_transaction',$auto]];
    $checks=['own_pending_value'=>$inside===7500,'connection_closed'=>$closed===true,
        'uncommitted_change_discarded'=>$after===10000,'new_connection_autocommit'=>$auto===9900];
    $observations=['inside_explicit_transaction'=>$inside,'after_close_new_connection'=>$after,'autocommit_visible_to_another_connection'=>$auto];

} finally {
    if (is_resource($db)) {try {$removed=s150Cleanup($db,$owned);} finally {sqlsrv_close($db);}}
}

s150Report('sqlsrv150-close-rollback','Увидеть, почему UPDATE без commit исчезает после закрытия',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
До закрытия своё соединение видит — 7500
Соединение закрыто — да
Новое соединение после незавершённой транзакции — 10000
Новое изменение без begin_transaction — 9900

Запустите в отдельной папке по README. lab.php открывает ограниченный учебный LocalDB, создаёт только собственные таблицы и сохраняет результат; сама транзакция и её завершение видны в example.php. Учебные таблицы после чтения удаляются.

Таблица сохранённого запуска · Данные и наблюдения JSON.

Границы и ошибки

Закрытие — страховочное завершение незаконченной транзакции, а не удобный способ сообщить пользователю о штатном отказе. При ожидаемой неудаче вызывайте rollback явно и проверяйте результат. Обновление в режиме auto-commit не нуждается в последующем sqlsrv_commit; вызов commit без начатой драйвером транзакции возвращает false. Записи, подтверждённые до begin, остаются. Учебная таблица создана до транзакции и удаляется явно только после всех наблюдений. В рабочем приложении таблицу и данные удалять не нужно.

UPDATE сообщает об исполнении и изменённой строке, но после запроса данные не сохраняются. Разбор исходной проблемы на Stack Overflow. Разделяем выполнение и подтверждение: проверяем открытие явной транзакции, commit и чтение через новое соединение. В уроке воспроизведён забытый commit; это не универсальное объяснение любого исчезнувшего изменения.

Измените одно требование

Перед close добавьте проверенный commit. Какое значение увидит первый повторный вход?

Как проверить изменение

Он должен увидеть 7500. Следующий отдельный UPDATE всё так же сохранит 9900. Если итоговый SELECT по-прежнему показывает 10000, нужно проверять именно результат commit, выбранную базу и строку, а не прятать проблему повторной записью.

Выбрать начало, подтверждение или отмену

ФункцияКогда нужна
sqlsrv_begin_transactionОбъединить несколько изменений одного соединения в подтверждаемую группу.
sqlsrv_commitПодтвердить завершённую группу только после успешных проверок.
sqlsrv_rollbackОтменить изменения незавершённой группы при ошибке SQL или отказе правила.

Перенести границы операции в своё приложение

Для следующего своего сценария запишите правило целостности: какие изменения должны сохраниться вместе, какой отказ считается штатным и по какому номеру проверяется уже выполненная операция. Проверяйте результаты команд и количество подходящих строк; держите одну короткую границу на одном соединении. Подтверждённая запись в базе и успешная отправка внешнего письма — разные события: транзакция SQLSRV не отменяет письмо и не превращает несколько внешних систем в одну базу.

Получайте нужные поля и строки на сервере. В примерах ORDER BY делает порядок квитанции явным; маленький массив содержит только два учебных счёта. Нельзя переносить этот размер на весь каталог: SELECT * с последующей сортировкой сотен тысяч строк в PHP не нужен для экрана одного заказа. Принцип запроса под нужный экран общий, хотя синтаксис пагинации и драйверы SQL Server отличаются от MySQL.

Дальше изучите чтение курсора и нескольких результатов. Они нужны, чтобы подтверждать операцию после обработки всей команды, а не принимать первый полученный ресурс за окончательный успех.

Функции и методы этой темы

Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.

sqlsrv_begin_transactionОбъединить несколько изменений одного соединения в подтверждаемую группу.

SQLSRV: точная первая версия не указана в PHP Manual; проверенный профиль указан в README · sqlsrv

sqlsrv_begin_transaction(resource $conn): bool

Объединить несколько изменений одного соединения в подтверждаемую группу.

Граница действия функции
  1. Соединение по умолчанию: отдельные команды auto-commit
  2. begin($conn) → группа открыта
  3. Команды на этом же $conn входят в группу
  4. commit или rollback завершает группу
ПараметрНазначение
$connОткрытый ресурс sqlsrv_connect; тот же $conn используется для всех команд группы.

true — группа начата; false — начать её не удалось. При false сохраните sqlsrv_errors и не продолжайте предполагаемые транзакционные записи.

Связать перевод и журнал

Начинаем группу до первой операции, которая должна отменяться вместе с остальными.

if (sqlsrv_begin_transaction($db) === false) {
    $errors = sqlsrv_errors(SQLSRV_ERR_ALL);
    throw new RuntimeException("Перевод не начат");
}
// Теперь выполнить и проверить списание, зачисление и журнал.

Это контекстный фрагмент с уже открытым соединением. Полная программа, подготовка и результат.

Связать позиции одного заказа

Начало транзакции относится к заказу целиком. Новая транзакция на каждую позицию закрепила бы частичный резерв.

if (sqlsrv_begin_transaction($db) === false) {
    throw new RuntimeException("Резерв не начат");
}
// Проверять каждое условное UPDATE; при отказе отменить всю группу.

Это контекстный фрагмент с уже открытым соединением. Полная программа, подготовка и результат.

Повторный begin до завершения текущей транзакции на том же соединении возвращает false с ошибкой Already in Transaction. Это не API вложенных независимых транзакций. Начатая группа не распространяется автоматически на другое соединение, файл, письмо или внешний HTTP-запрос. Используйте функции драйвера, а не текст BEGIN TRANSACTION вместе с ними.

Связанные действия: sqlsrv_commit, sqlsrv_rollback; сохранение диагностики; количество изменённых строк.

sqlsrv_commitПодтвердить завершённую группу только после успешных проверок.

SQLSRV: точная первая версия не указана в PHP Manual; проверенный профиль указан в README · sqlsrv

sqlsrv_commit(resource $conn): bool

Подтвердить завершённую группу только после успешных проверок.

Граница действия функции
  1. Проверенные команды дали нужный предметный результат
  2. commit($conn) вызывается один раз
  3. true → можно показать подтверждённую квитанцию
  4. false → сохранить причину; не объявлять успех
ПараметрНазначение
$connСоединение с активной транзакцией, ранее начатой sqlsrv_begin_transaction.

true — подтверждение успешно; false — драйвер сообщил отказ. Успешный commit возвращает соединение к auto-commit. Без активной начатой драйвером транзакции возвращается false и диагностика Not in Transaction.

Показать квитанцию перевода

Сообщение об успехе находится после проверенного commit, а не после первого UPDATE.

if (sqlsrv_commit($db) === false) {
    $errors = sqlsrv_errors(SQLSRV_ERR_ALL);
    throw new RuntimeException("Подтверждение не получено");
}
echo "Перевод подтверждён";

Это контекстный фрагмент с уже открытым соединением. Полная программа, подготовка и результат.

Исправить исчезающее изменение

Чтобы эксперимент с 7500 сохранил значение после закрытия, подтвердите активную группу перед close. В исходном опыте commit намеренно отсутствует.

if (sqlsrv_commit($db) === false) {
    throw new RuntimeException("Значение не подтверждено");
}
sqlsrv_close($db);

Это контекстный фрагмент с уже открытым соединением. Полная программа, подготовка и результат.

Сигнал успеха отдельного UPDATE не подменяет успех commit. При отказе требуется сохранить диагностику и, если соединение и транзакция ещё позволяют, выполнить проверенный rollback. При утрате соединения во время подтверждения нельзя обещать, что запись точно не состоялась: сверяйте номер операции. Успешный commit нельзя позже отменить rollback той же завершённой группы.

Связанные действия: sqlsrv_begin_transaction, sqlsrv_rollback; сохранение диагностики; количество изменённых строк.

sqlsrv_rollbackОтменить изменения незавершённой группы при ошибке SQL или отказе правила.

SQLSRV: точная первая версия не указана в PHP Manual; проверенный профиль указан в README · sqlsrv

sqlsrv_rollback(resource $conn): bool

Отменить изменения незавершённой группы при ошибке SQL или отказе правила.

Граница действия функции
  1. UPDATE уже изменил данные внутри своей группы
  2. Поздний отказ INSERT либо недостаточный остаток
  3. rollback($conn) отменяет изменения этой группы
  4. Новый SELECT показывает подтверждённое исходное состояние
ПараметрНазначение
$connТо же открытое соединение с активной транзакцией sqlsrv_begin_transaction.

true — текущая группа отменена, соединение возвращено в auto-commit. false — отменить не удалось; сохраните диагностику. Без активной начатой драйвером транзакции функция возвращает false с Not in Transaction.

Сохранить причину отказа журнала

Диагностику отказавшего INSERT сохраняют перед rollback, потому что следующий вызов относится уже к другой операции.

$saved = sqlsrv_errors(SQLSRV_ERR_ALL);
if (sqlsrv_rollback($db) === false) {
    $rollbackErrors = sqlsrv_errors(SQLSRV_ERR_ALL);
    throw new RuntimeException("Откат не подтверждён");
}

Это контекстный фрагмент с уже открытым соединением. Полная программа, подготовка и результат.

Отказать всему заказу без ошибки SQL

Нулевой счётчик второй команды не является синтаксической ошибкой. Продуктовое правило всё равно может требовать отмены первой позиции.

if ($affected === 0) {
    if (sqlsrv_rollback($db) === false) {
        throw new RuntimeException("Резерв не отменён");
    }
    echo "Полного комплекта нет";
}

Это контекстный фрагмент с уже открытым соединением. Полная программа, подготовка и результат.

Откат охватывает текущую транзакцию, а не всю историю базы. Ранее подтверждённые записи и изменения другого соединения он не убирает. Не посылайте rollback вслепую после уже успешного commit. PHP-массив с прочитанными данными тоже сам по себе не возвращается к прежнему значению: после отмены перечитайте нужную строку, если её нужно показать.

Связанные действия: sqlsrv_begin_transaction, sqlsrv_commit; сохранение диагностики; количество изменённых строк.

Источники и обсуждения

Код и руководство с собой

Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.

Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.

Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.