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

Дерево руководства
Глава 132 · После таблиц, запросов и первых транзакций X DevAPI
MySQL X DevAPI: команды таблицы, остатки и очередь без двойной обработки
Создать команды CRUD через фабрики, получить SQL-отчёт и согласовать последние остатки, совместное чтение и выдачу заданий.
От сохранённого каталога к согласованной работе
Нужно исправить остатки товара, построить отчёт по нужным полям и затем защитить работу двух заказов или двух исполнителей. На каждом шаге полезно знать, какой объект мы держим: доступ к таблице, ещё не выполненную команду, квитанцию записи или набор строк. Начнём с видимого каталога LED=7, BOX=3 и только после этого добавим конкурирующих участников.
- Schema::getTable → Table каталога
- Table::update → TableUpdate: LED, stock 7
- execute → Result: запись выполнена
- Table::select → execute → RowResult → LED 7
| Объект | Что он делает | Что само по себе ещё не произошло |
|---|---|---|
| Table | Обращается к выбранной таблице | Таблица не создана и строки не прочитаны |
| TableInsert/Select/Update/Delete | Хранит настроенную команду | Команда не отправлена до execute |
| Result | Сообщает о выполненной записи | Содержимое строки не прочитано |
| RowResult / SqlStatementResult | Предоставляет ответ запроса | Не отменяет и не фиксирует транзакцию |
| ColumnResult | Описывает поле результата | Не является самим значением товара |
Конструктор — специальный метод создания объекта; private означает закрытый доступ. Здесь библиотека создаёт корректные объекты через публичные методы-фабрики. Аналогия — заполненный бланк заказа: пустой бланк, отправленный заказ и ответ склада относятся к разным этапам. Это только модель последовательности, а не описание внутренней памяти PHP.
Перед полной программой нужны вызов функции и возвращаемое значение, объект и метод, подключение X DevAPI и строки, таблицы и условия выбора. Новые фабрики и блокировки объясняются здесь до большого файла. Соседняя глава о Client и Session показывает самостоятельные соединения и освобождение ресурсов.
- Остаток 1
- A прочитал 1; B прочитал 1
- Без согласования оба могут решить «доступно»
- Locking read + проверка + запись + commit: второй видит актуальный итог
Транзакция объединяет чтение и изменение; блокировка согласует конфликтующие действия разных транзакций. Это не запрет всех чтений и не замена ограничений, связей или бизнес-правил. Минимальные уроки используют существующий PRIMARY KEY и короткие шаги. Они не держат lock во время работы пользователя или обращения к внешнему сервису.
| Уже удерживается на строке | Новый shared | Новый exclusive |
|---|---|---|
| Shared другой транзакции | Совместим | Конфликт |
| Exclusive другой транзакции | Конфликт | Конфликт |
При конфликте DEFAULT допускает ожидание; NOWAIT возвращает ошибку; SKIP LOCKED пропускает занятые строки. Совместимый shared не становится ошибкой лишь из-за NOWAIT. Все блокировки транзакции освобождаются при commit или rollback; обычное consistent read может читать снимок независимо от них.
Архив каждого урока содержит example.php, lab.php и README.txt. Нужны PHP с mysql_xdevapi и подготовленный локальный MySQL с X Protocol; данные соединения берутся из PHPMAN_MYSQLX_URI и PHPMAN_MYSQLX_SCHEMA. В используемом профиле: PHP 8.3.35, PECL 8.0.30, MySQL 8.4.13. Скрипт создаёт одну собственную таблицу mysql144_, сохраняет result.html и result.json и удаляет только её после завершения своих транзакций. result.json содержит исходные наблюдения, типы и список очистки, metrics.json — сводные проверки. Пароль в архив и отчёт не входит.
Короткие записи на странице — объясняющие фрагменты подготовленного контекста. Полный самостоятельный файл выполняет подготовку и уборку. В сценариях конкуренции создаются настоящие отдельные Session; хронометраж или sleep не используются как доказательство блокировки.
Получить товар, команду и результат через их фабрики
Оператор исправляет остаток лампы LED с 5 до 7, сохраняет коробки BOX и убирает только пробную строку DRAFT. Вместе с реальными товарами разберём, откуда берутся объекты TableUpdate и RowResult: команда готовит действие, квитанция сообщает о выполнении, а результат чтения приносит данные.
- Schema → getTable(name): доступ к каталогу
- update() → TableUpdate: stock = 7, sku = LED
- execute() → Result: обновлена 1 строка
- select(...)->execute() → RowResult → LED 7, BOX 3
Небольшая запись и её смысл
Здесь $catalog хранит объект доступа; -> вызывает его метод. update() возвращает новую команду, set указывает поле и значение, where задаёт условие, а bind передаёт значение для :sku. execute отправляет уже собранную команду. Полученный Result — квитанция, поэтому реальные сохранённые строки читаются отдельной выборкой. __construct — имя конструктора класса, private означает, что пользовательский код не вызывает его через new. Метод, который возвращает настроенный объект, здесь называется фабрикой. Для начала достаточно показанных четырёх действий; самостоятельный файл добавляет исходные товары и точное удаление DRAFT.
$catalog = $schema->getTable($name);
$command = $catalog->update()->set('stock', 7)->where('sku = :sku')->bind(['sku' => 'LED']);
$receipt = $command->execute();
$saved = $catalog->select('sku', 'stock')->where('sku = :sku')->bind(['sku' => 'LED'])->execute()->fetchAll();Что получается на исходных данных
| Наблюдение | Результат |
|---|---|
| Сохранённый LED | 7 |
| Сохранённый BOX | 3 |
| Удалено временных строк | 1 |
| Команда обновления | mysql_xdevapi\TableUpdate |
| Квитанция записи | mysql_xdevapi\Result |
| Результат чтения | mysql_xdevapi\RowResult |
Самостоятельная программа и архив
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$config = x144Config();
$session = mysql_xdevapi\getSession($config['uri']);
$schema = null;
$owned = [];
$removed = [];
$rows = $checks = $observations = [];
try {
$schema = $session->getSchema($config['schema']);
if (!$schema->existsInDatabase()) {
throw new RuntimeException('Учебная схема не существует: подготовьте её отдельно.');
}
$runtime = ['php'=>PHP_VERSION, 'extension'=>phpversion('mysql_xdevapi'),
'server_version'=>$session->getServerVersion(), 'schema'=>$config['schema'],
'host'=>$config['host'], 'port'=>$config['port']];
$name=x144OwnTable($session,$schema,$owned,'factory_catalog',
'(id INT AUTO_INCREMENT PRIMARY KEY, sku VARCHAR(20) NOT NULL UNIQUE, stock INT NOT NULL) ENGINE=InnoDB');
$catalog=$schema->getTable($name);
$insert=$catalog->insert('sku','stock')->values(['LED',5],['BOX',3]);
$insertResult=$insert->execute();
$temporary=$catalog->insert('sku','stock')->values(['DRAFT',0])->execute();
$draftId=$temporary->getAutoIncrementValue();
$update=$catalog->update()->set('stock',7)->where('sku = :sku')->bind(['sku'=>'LED']);
$updateResult=$update->execute();
$delete=$catalog->delete()->where('id = :id')->bind(['id'=>$draftId]);
$deleteResult=$delete->execute();
$select=$catalog->select('sku','stock')->orderby('id');
$result=$select->execute();
$saved=$result->fetchAll();
$types=['table'=>get_debug_type($catalog),'insert'=>get_debug_type($insert),
'update'=>get_debug_type($update),'delete'=>get_debug_type($delete),
'select'=>get_debug_type($select),'write_result'=>get_debug_type($updateResult),
'rows'=>get_debug_type($result)];
$rows=[['Сохранённый LED',$saved[0]['stock']],['Сохранённый BOX',$saved[1]['stock']],
['Удалено временных строк',$deleteResult->getAffectedItemsCount()],
['Команда обновления',$types['update']],['Квитанция записи',$types['write_result']],
['Результат чтения',$types['rows']]];
$checks=['saved_catalog_exact'=>$saved===[['sku'=>'LED','stock'=>7],['sku'=>'BOX','stock'=>3]],
'write_receipts'=>$insertResult->getAffectedItemsCount()===2 && $updateResult->getAffectedItemsCount()===1 && $deleteResult->getAffectedItemsCount()===1,
'factory_types'=>$catalog instanceof mysql_xdevapi\Table && $insert instanceof mysql_xdevapi\TableInsert && $update instanceof mysql_xdevapi\TableUpdate && $delete instanceof mysql_xdevapi\TableDelete && $select instanceof mysql_xdevapi\TableSelect && $updateResult instanceof mysql_xdevapi\Result && $result instanceof mysql_xdevapi\RowResult];
$observations=['types'=>$types,'saved_rows'=>$saved,'temporary_id'=>$draftId,
'inserted'=>$insertResult->getAffectedItemsCount(),'updated'=>$updateResult->getAffectedItemsCount(),'deleted'=>$deleteResult->getAffectedItemsCount()];
} finally {
try {
if ($schema instanceof mysql_xdevapi\Schema) {
$removed = x144Cleanup($schema, $owned);
}
} finally {
$session->close();
}
}
x144Report('mysql144-x-factory-table-workflow', 'Получить товар, команду и результат через их фабрики', $rows, $checks, $observations, $runtime, $owned, $removed);
Ожидаемый результат
Сохранённый LED — 7
Сохранённый BOX — 3
Удалено временных строк — 1
Команда обновления — mysql_xdevapi\TableUpdate
Квитанция записи — mysql_xdevapi\Result
Результат чтения — mysql_xdevapi\RowResult
CLI-программа создаёт одну собственную таблицу InnoDB в подготовленной учебной схеме, выполняет запросы, сохраняет данные readback и удаляет только свой объект. Новые участники используют отдельные соединения; все транзакции завершаются до уборки. Это не HTTP-обработчик.
Таблица фактического запуска · Данные, типы и диагностика JSON.
Границы и типичные ошибки
getTable не выполняет CREATE TABLE. Таблица создаётся отдельно в начале самостоятельного файла; затем фабрики работают с существующим источником. Конструкторы Table, TableInsert, TableSelect, TableUpdate, TableDelete, Result и RowResult закрыты. Не следует заменять их Reflection::newInstanceWithoutConstructor: такой объект не приобретёт связь с таблицей и серверным результатом. Квитанция с affected=1 не описывает содержимое строки; readback нужен отдельно. Удаление ограничено id именно вставленной временной строки. Условие age > 42 в старом официальном примере TableSelect::__construct не может вернуть age=42: в наших примерах значения выводятся из фактического условия.
Измените требование
Замените stock=7 на stock=9. Какие значения должны измениться, а какие остаться прежними?
Разбор следующего шага
Изменятся сохранённый остаток LED и ожидаемая строка результата. BOX останется 3, удалён будет только DRAFT, типы команд и результатов останутся прежними. Получать команду через new TableUpdate по-прежнему не нужно.
Получить заполненный SQL-отчёт и описание его полей
Нужен отчёт по продажам от двух единиц: название товара, количество и сумма в копейках. MySQL отбирает строки и вычисляет сумму до передачи в PHP. Метаданные помогают связать заголовки отчёта с реально выбранными полями, а не со всеми столбцами таблицы.
- 3 продажи → WHERE qty >= 2
- SELECT product, qty, total_kopecks → 2 строки
- Session::sql → SqlStatement → execute → SqlStatementResult
- getColumns → 3 ColumnResult; fetchAll → данные LED и BOX
Небольшая запись и её смысл
В короткой записи $qualifiedTable — заранее процитированное полное имя собственной таблицы, например `php_manual_x139`.`mysql144_...`; в полном файле его создаёт x144Qualified($schema, $name). sql получает строку SQL и создаёт SqlStatement. Знак ? занимает место значения, bind(2) передаёт порог; имя таблицы уже выбрано из собственного объекта и процитировано отдельно. AS product задаёт подпись столбца результата. execute без ASYNC возвращает SqlStatementResult, getColumns — массив ColumnResult. Каждый его элемент описывает одно поле: подпись, исходное имя, тип. fetchAll получает строки с ключами product, qty и total_kopecks. Числа 2400 и 1200 — копейки, а не рубли; вычисление qty * price_kopecks выполнено сервером.
$statement = $session->sql('SELECT sku AS product, qty, qty * price_kopecks AS total_kopecks FROM ' . $qualifiedTable . ' WHERE qty >= ? ORDER BY id');
$result = $statement->bind(2)->execute();
$columns = $result->getColumns();
$rows = $result->fetchAll();Что получается на исходных данных
| Наблюдение | Результат |
|---|---|
| Товары отчёта | LED, BOX |
| LED: сумма в копейках | 2400 |
| BOX: сумма в копейках | 1200 |
| Подписи полей | product, qty, total_kopecks |
| SQL-команда | mysql_xdevapi\SqlStatement |
| SQL-результат | mysql_xdevapi\SqlStatementResult |
| Объект столбца | mysql_xdevapi\ColumnResult |
Самостоятельная программа и архив
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$config = x144Config();
$session = mysql_xdevapi\getSession($config['uri']);
$schema = null;
$owned = [];
$removed = [];
$rows = $checks = $observations = [];
try {
$schema = $session->getSchema($config['schema']);
if (!$schema->existsInDatabase()) {
throw new RuntimeException('Учебная схема не существует: подготовьте её отдельно.');
}
$runtime = ['php'=>PHP_VERSION, 'extension'=>phpversion('mysql_xdevapi'),
'server_version'=>$session->getServerVersion(), 'schema'=>$config['schema'],
'host'=>$config['host'], 'port'=>$config['port']];
$name=x144OwnTable($session,$schema,$owned,'factory_report',
'(id INT PRIMARY KEY, sku VARCHAR(20) NOT NULL, qty INT NOT NULL, price_kopecks INT NOT NULL) ENGINE=InnoDB');
$sales=$schema->getTable($name);
$sales->insert('id','sku','qty','price_kopecks')->values([1,'LED',2,1200],[2,'BOX',3,400],[3,'TAPE',1,350])->execute();
$statement=$session->sql('SELECT sku AS product, qty, qty * price_kopecks AS total_kopecks FROM '
.x144Qualified($schema,$name).' WHERE qty >= ? ORDER BY id')->bind(2);
$result=$statement->execute();
$columns=$result->getColumns();
$metadata=[];
foreach ($columns as $column) {
$metadata[]=['class'=>get_debug_type($column),'label'=>$column->getColumnLabel(),
'name'=>$column->getColumnName(),'type'=>$column->getType()];
}
$saved=$result->fetchAll();
$labels=array_column($metadata,'label');
$rows=[['Товары отчёта',implode(', ',array_column($saved,'product'))],
['LED: сумма в копейках',$saved[0]['total_kopecks']],['BOX: сумма в копейках',$saved[1]['total_kopecks']],
['Подписи полей',implode(', ',$labels)],['SQL-команда',get_debug_type($statement)],
['SQL-результат',get_debug_type($result)],['Объект столбца',get_debug_type($columns[0])]];
$checks=['report_rows_exact'=>$saved===[['product'=>'LED','qty'=>2,'total_kopecks'=>2400],['product'=>'BOX','qty'=>3,'total_kopecks'=>1200]],
'metadata_matches_projection'=>$labels===['product','qty','total_kopecks'] && count($columns)===3,
'sql_factory_types'=>$statement instanceof mysql_xdevapi\SqlStatement && $result instanceof mysql_xdevapi\SqlStatementResult && $columns[0] instanceof mysql_xdevapi\ColumnResult];
$observations=['rows'=>$saved,'metadata'=>$metadata,'statement_type'=>get_debug_type($statement),'result_type'=>get_debug_type($result)];
} finally {
try {
if ($schema instanceof mysql_xdevapi\Schema) {
$removed = x144Cleanup($schema, $owned);
}
} finally {
$session->close();
}
}
x144Report('mysql144-x-factory-sql-report', 'Получить заполненный SQL-отчёт и описание его полей', $rows, $checks, $observations, $runtime, $owned, $removed);
Ожидаемый результат
Товары отчёта — LED, BOX
LED: сумма в копейках — 2400
BOX: сумма в копейках — 1200
Подписи полей — product, qty, total_kopecks
SQL-команда — mysql_xdevapi\SqlStatement
SQL-результат — mysql_xdevapi\SqlStatementResult
Объект столбца — mysql_xdevapi\ColumnResult
CLI-программа создаёт одну собственную таблицу InnoDB в подготовленной учебной схеме, выполняет запросы, сохраняет данные readback и удаляет только свой объект. Новые участники используют отдельные соединения; все транзакции завершаются до уборки. Это не HTTP-обработчик.
Таблица фактического запуска · Данные, типы и диагностика JSON.
Границы и типичные ошибки
Конструкторы SqlStatement и SqlStatementResult в PHP Manual представлены почти пустыми private-страницами, но рабочие объекты получают через Session::sql и execute. ColumnResult приходит из getColumns, а не из new. В старом примере ColumnResult::__construct SQL-ветка показывает FieldMetadata; в выбранном PECL 8.0.30 оба вида результата возвращают ColumnResult. Это проверяемый факт профиля, а не обещание любого старого коннектора. Выражение умножения здесь использует малые целые числа; для денежных DECIMAL и больших сумм нужен отдельный разбор точности. Метаданные описывают форму результата, не выполняют валидацию пользовательского ввода. Не заменяйте выбор трёх полей на SELECT * и последующую фильтрацию всего массива.
Измените требование
Добавьте условие минимальной суммы 2000 копеек. Как должен измениться отчёт?
Разбор следующего шага
Добавьте в WHERE qty * price_kopecks >= ? и второй последовательный bind(2000), сохранив bind(2) для первого placeholder. Останется LED с суммой 2400; список выбранных полей и их подписи останутся теми же. Значение порога передаётся параметром, а не склеивается в SQL.
Зарезервировать последнюю единицу без второго владельца
Два заказа хотят последнюю лампу. Обычный SELECT, проверка stock и более поздний UPDATE оставляют промежуток, в котором оба заказа могут увидеть единицу. Здесь первый участник удерживает право изменить конкретную строку до commit; второй сначала получает понятный конфликт, а после завершения первого видит нулевой остаток и отказывает в резерве.
- ORDER-A: startTransaction → id 1, stock 1, exclusive-lock
- ORDER-B: exclusive NOWAIT → конфликт 3572 в цепочке исключений
- ORDER-A: stock 0, reserved_by ORDER-A → commit
- ORDER-B: новое locking read → stock 0 → резерв не создаётся
Небольшая запись и её смысл
startTransaction начинает транзакцию — несколько действий, которые завершаются вместе. lockExclusive настраивает исключительное блокирующее чтение, execute выполняет его, fetchOne получает строку. Пока транзакция открыта, другой участник не может получить конфликтующий lock этой строки. DEFAULT — обычное ожидание при конфликте; первый запрос здесь свободен и не ждёт. NOWAIT выбирает немедленный отказ при конфликтующем row-lock. commit сохраняет изменения и освобождает блокировки; rollback отменяет изменения и также освобождает их. Короткий фрагмент показывает успешный путь, полный файл ниже содержит обработку отказов и finally. reserved_by здесь хранит владельца единственного резерва; для многих заказов нужна отдельная таблица резервов и связь с товарами.
$session->startTransaction();
$row = $catalog->select('stock')->where('id = 1')->lockExclusive()->execute()->fetchOne();
if ($row['stock'] >= 1) {
$catalog->update()->set('stock', $row['stock'] - 1)->set('reserved_by', 'ORDER-A')->where('id = 1')->execute();
}
$session->commit();Что получается на исходных данных
| Наблюдение | Результат |
|---|---|
| Первый прочитал остаток | 1 |
| Обычное чтение до записи | 1 |
| NOWAIT нашёл конфликт | да |
| Второй прочитал после commit | 0 |
| Второй создал резерв | нет |
| Финальный остаток | 0 |
| Владелец резерва | ORDER-A |
Самостоятельная программа и архив
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$config = x144Config();
$session = mysql_xdevapi\getSession($config['uri']);
$schema = null;
$owned = [];
$removed = [];
$rows = $checks = $observations = [];
try {
$schema = $session->getSchema($config['schema']);
if (!$schema->existsInDatabase()) {
throw new RuntimeException('Учебная схема не существует: подготовьте её отдельно.');
}
$runtime = ['php'=>PHP_VERSION, 'extension'=>phpversion('mysql_xdevapi'),
'server_version'=>$session->getServerVersion(), 'schema'=>$config['schema'],
'host'=>$config['host'], 'port'=>$config['port']];
$name=x144OwnTable($session,$schema,$owned,'exclusive_stock',
'(id INT PRIMARY KEY, sku VARCHAR(20) NOT NULL, stock INT NOT NULL, reserved_by VARCHAR(20) NULL) ENGINE=InnoDB');
$catalog=$schema->getTable($name);
$catalog->insert('id','sku','stock','reserved_by')->values([1,'LED',1,null])->execute();
$other=mysql_xdevapi\getSession($config['uri']);
$otherSchema=$other->getSchema($config['schema']);
$otherCatalog=$otherSchema->getTable($name);
$aOpen=$bOpen=false;
try {
$connectionA=$session->sql('SELECT CONNECTION_ID() AS id')->execute()->fetchOne()['id'];
$connectionB=$other->sql('SELECT CONNECTION_ID() AS id')->execute()->fetchOne()['id'];
$session->startTransaction(); $aOpen=true;
$command=$catalog->select('id','stock')->where('id = 1')->lockExclusive();
$lockedResult=$command->execute(); $first=$lockedResult->fetchOne();
$ordinary=$otherCatalog->select('stock')->where('id = 1')->execute()->fetchOne();
$other->startTransaction(); $bOpen=true;
$conflict=x144Failure(static function () use ($otherCatalog): void {
$otherCatalog->select('id','stock')->where('id = 1')->lockExclusive(MYSQLX_LOCK_NOWAIT)->execute()->fetchAll();
});
$other->rollback(); $bOpen=false;
if ($first['stock'] < 1) {throw new RuntimeException('Нет остатка для первого резерва.');}
$receipt=$catalog->update()->set('stock',$first['stock']-1)->set('reserved_by','ORDER-A')->where('id = 1')->execute();
if ($receipt->getAffectedItemsCount()!==1) {throw new RuntimeException('Первый резерв не записан.');}
$session->commit(); $aOpen=false;
$other->startTransaction(); $bOpen=true;
$second=$otherCatalog->select('id','stock','reserved_by')->where('id = 1')->lockExclusive(MYSQLX_LOCK_NOWAIT)->execute()->fetchOne();
$secondReserved=false;
if ($second['stock'] >= 1) {
$change=$otherCatalog->update()->set('stock',$second['stock']-1)->set('reserved_by','ORDER-B')->where('id = 1')->execute();
if ($change->getAffectedItemsCount()!==1) {throw new RuntimeException('Второй резерв не записан.');}
$secondReserved=true;
}
$other->commit(); $bOpen=false;
$saved=$catalog->select('sku','stock','reserved_by')->where('id = 1')->execute()->fetchOne();
$codes=array_column($conflict ?? [],'code');
$rows=[['Первый прочитал остаток',$first['stock']],['Обычное чтение до записи',$ordinary['stock']],
['NOWAIT нашёл конфликт',in_array(3572,$codes,true) ? 'да' : 'нет'],
['Второй прочитал после commit',$second['stock']],['Второй создал резерв',$secondReserved ? 'да' : 'нет'],
['Финальный остаток',$saved['stock']],['Владелец резерва',$saved['reserved_by']]];
$checks=['independent_connections'=>$connectionA!==$connectionB,
'nowait_conflict_observed'=>in_array(3572,$codes,true),
'ordinary_read_is_not_blocked'=>$ordinary===['stock'=>1],
'last_item_reserved_once'=>$saved===['sku'=>'LED','stock'=>0,'reserved_by'=>'ORDER-A'] && $second['stock']===0 && !$secondReserved,
'lock_builder_and_result'=>$command instanceof mysql_xdevapi\TableSelect && $lockedResult instanceof mysql_xdevapi\RowResult];
$observations=['connection_ids'=>[$connectionA,$connectionB],'first'=>$first,'ordinary'=>$ordinary,'nowait_error_chain'=>$conflict,
'second'=>$second,'saved'=>$saved,'builder_type'=>get_debug_type($command),'result_type'=>get_debug_type($lockedResult)];
} finally {
try {if ($bOpen) {$other->rollback();}} finally {
try {if ($aOpen) {$session->rollback();}} finally {$other->close();}
}
}
} finally {
try {
if ($schema instanceof mysql_xdevapi\Schema) {
$removed = x144Cleanup($schema, $owned);
}
} finally {
$session->close();
}
}
x144Report('mysql144-x-table-exclusive-reservation', 'Зарезервировать последнюю единицу без второго владельца', $rows, $checks, $observations, $runtime, $owned, $removed);
Ожидаемый результат
Первый прочитал остаток — 1
Обычное чтение до записи — 1
NOWAIT нашёл конфликт — да
Второй прочитал после commit — 0
Второй создал резерв — нет
Финальный остаток — 0
Владелец резерва — ORDER-A
CLI-программа создаёт одну собственную таблицу InnoDB в подготовленной учебной схеме, выполняет запросы, сохраняет данные readback и удаляет только свой объект. Новые участники используют отдельные соединения; все транзакции завершаются до уборки. Это не HTTP-обработчик.
Таблица фактического запуска · Данные, типы и диагностика JSON.
Границы и типичные ошибки
Блокировка устанавливается при execute, а не при построении цепочки. Она не запрещает всякое чтение: в нашем профиле другой обычный SELECT читает доступную версию, что видно в отчёте. Выбран существующий id по PRIMARY KEY, чтобы не смешивать урок с диапазонными блокировками. Константы MYSQLX_LOCK_* глобальные, без префикса mysql_xdevapi. В PECL 8.0.30 NOWAIT приходит как внешнее исключение 10000 с предыдущим серверным 3572; нельзя проверять только внешний getCode или считать любую ошибку отсутствием остатка. В full-коде цепочка сохранена через x144Failure, URI/пароль удаляются. Это демонстрация отказа, не бесконечный цикл повторов. В production неожиданная ошибка должна завершать транзакцию и обрабатываться отдельно от stock=0.
Автор ожидал, что блокирующее чтение автоматически запретит любое чтение другой сессией, и не понимал границы транзакции. Наш сценарий отдельно показывает обычное чтение и конфликтующий NOWAIT. Семантика взята из документации InnoDB и собственного запуска; утверждения ответов не подменяют её. Обсуждение: Почему после FOR UPDATE всё ещё виден обычный SELECT.
Измените требование
Нужно резервировать по две единицы. Достаточно поменять stock - 1 на stock - 2?
Разбор следующего шага
Нет: в той же защищённой транзакции проверяют stock >= 2 и уменьшают на 2. Для нескольких успешных заказов одной строки reserved_by уже недостаточно: добавляют таблицу резервов с order_id, product_id и qty, а уменьшение остатка и INSERT выполняют вместе. Блокировка не заменяет модель связей.
Дать двум проверяющим прочитать цену и отложить её изменение
Два автоматических процесса сверяют цену 1200 копеек: один готовит строку счёта, другой проверяет правило каталога. На короткое время обоим нужно сохранить прочитанное значение от изменения. Shared-lock позволяет этим читателям работать вместе; редактор с exclusive-lock получает отказ и меняет цену только после завершения проверок.
- Читатель A: shared-lock цены 1200
- Читатель B: shared NOWAIT → тоже 1200
- Редактор C: exclusive NOWAIT → конфликт
- A и B завершаются → C записывает 1350 → readback
Небольшая запись и её смысл
lockShared задаёт совместный режим чтения строки. Другая независимая транзакция тоже может получить shared-lock той же строки. Попытка получить exclusive-lock конфликтует, пока совместные держатели не завершатся. Сам метод возвращает TableSelect; после execute читатель получает RowResult. В полной программе три отдельных getSession создают три участника с разными CONNECTION_ID. Объект Session, полученный через $table->getSession(), не сделал бы нового участника: он обращается к соединению таблицы.
$session->startTransaction();
$price = $prices->select('price_kopecks')->where('id = 1')->lockShared()->execute()->fetchOne();
// Здесь короткая проверка значения цены.
$session->commit();Что получается на исходных данных
| Наблюдение | Результат |
|---|---|
| Первый проверяющий | 1200 |
| Второй проверяющий | 1200 |
| Изменение во время проверки | отложено |
| Цена перед разрешённым изменением | 1200 |
| Сохранённая новая цена | 1350 |
Самостоятельная программа и архив
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$config = x144Config();
$session = mysql_xdevapi\getSession($config['uri']);
$schema = null;
$owned = [];
$removed = [];
$rows = $checks = $observations = [];
try {
$schema = $session->getSchema($config['schema']);
if (!$schema->existsInDatabase()) {
throw new RuntimeException('Учебная схема не существует: подготовьте её отдельно.');
}
$runtime = ['php'=>PHP_VERSION, 'extension'=>phpversion('mysql_xdevapi'),
'server_version'=>$session->getServerVersion(), 'schema'=>$config['schema'],
'host'=>$config['host'], 'port'=>$config['port']];
$name=x144OwnTable($session,$schema,$owned,'shared_price',
'(id INT PRIMARY KEY, price_kopecks INT NOT NULL) ENGINE=InnoDB');
$prices=$schema->getTable($name);
$prices->insert('id','price_kopecks')->values([1,1200])->execute();
$reader=mysql_xdevapi\getSession($config['uri']);
$editor=null; $aOpen=$bOpen=$cOpen=false;
try {
$editor=mysql_xdevapi\getSession($config['uri']);
$readerSchema=$reader->getSchema($config['schema']); $readerPrices=$readerSchema->getTable($name);
$editorSchema=$editor->getSchema($config['schema']); $editorPrices=$editorSchema->getTable($name);
$ids=[$session->sql('SELECT CONNECTION_ID() AS id')->execute()->fetchOne()['id'],
$reader->sql('SELECT CONNECTION_ID() AS id')->execute()->fetchOne()['id'],
$editor->sql('SELECT CONNECTION_ID() AS id')->execute()->fetchOne()['id']];
$session->startTransaction(); $aOpen=true;
$a=$prices->select('price_kopecks')->where('id = 1')->lockShared()->execute()->fetchOne();
$reader->startTransaction(); $bOpen=true;
$b=$readerPrices->select('price_kopecks')->where('id = 1')->lockShared(MYSQLX_LOCK_NOWAIT)->execute()->fetchOne();
$editor->startTransaction(); $cOpen=true;
$conflict=x144Failure(static function () use ($editorPrices): void {
$editorPrices->select('id')->where('id = 1')->lockExclusive(MYSQLX_LOCK_NOWAIT)->execute()->fetchAll();
});
$editor->rollback(); $cOpen=false;
$session->commit(); $aOpen=false;
$reader->commit(); $bOpen=false;
$editor->startTransaction(); $cOpen=true;
$available=$editorPrices->select('price_kopecks')->where('id = 1')->lockExclusive(MYSQLX_LOCK_NOWAIT)->execute()->fetchOne();
$receipt=$editorPrices->update()->set('price_kopecks',1350)->where('id = 1')->execute();
if ($receipt->getAffectedItemsCount()!==1) {throw new RuntimeException('Новая цена не записана.');}
$editor->commit(); $cOpen=false;
$saved=$prices->select('price_kopecks')->where('id = 1')->execute()->fetchOne();
$codes=array_column($conflict ?? [],'code');
$rows=[['Первый проверяющий',$a['price_kopecks']],['Второй проверяющий',$b['price_kopecks']],
['Изменение во время проверки',in_array(3572,$codes,true) ? 'отложено' : 'не защищено'],
['Цена перед разрешённым изменением',$available['price_kopecks']],['Сохранённая новая цена',$saved['price_kopecks']]];
$checks=['three_independent_sessions'=>count(array_unique($ids))===3,
'shared_readers_compatible'=>$a===['price_kopecks'=>1200] && $b===$a,
'exclusive_conflicts_with_readers'=>in_array(3572,$codes,true),
'write_after_readers_finish'=>$available===$a && $saved===['price_kopecks'=>1350]];
$observations=['connection_ids'=>$ids,'reader_a'=>$a,'reader_b'=>$b,'nowait_error_chain'=>$conflict,'after_release'=>$available,'saved'=>$saved];
} finally {
try {if ($cOpen && $editor instanceof mysql_xdevapi\Session) {$editor->rollback();}} finally {
try {if ($bOpen) {$reader->rollback();}} finally {
try {if ($aOpen) {$session->rollback();}} finally {
try {if ($editor instanceof mysql_xdevapi\Session) {$editor->close();}} finally {$reader->close();}
}
}
}
}
} finally {
try {
if ($schema instanceof mysql_xdevapi\Schema) {
$removed = x144Cleanup($schema, $owned);
}
} finally {
$session->close();
}
}
x144Report('mysql144-x-table-shared-review', 'Дать двум проверяющим прочитать цену и отложить её изменение', $rows, $checks, $observations, $runtime, $owned, $removed);
Ожидаемый результат
Первый проверяющий — 1200
Второй проверяющий — 1200
Изменение во время проверки — отложено
Цена перед разрешённым изменением — 1200
Сохранённая новая цена — 1350
CLI-программа создаёт одну собственную таблицу InnoDB в подготовленной учебной схеме, выполняет запросы, сохраняет данные readback и удаляет только свой объект. Новые участники используют отдельные соединения; все транзакции завершаются до уборки. Это не HTTP-обработчик.
Таблица фактического запуска · Данные, типы и диагностика JSON.
Границы и типичные ошибки
Фраза PHP Manual «Only one lock can be active at a time» не означает, что на строке может быть лишь один совместный читатель. На одном builder выбирается один режим; совместимость разных транзакций задаётся MySQL. Если каждый читатель затем попытается повысить shared до exclusive, возможен deadlock: когда заранее собираетесь изменять строку, обычно сразу выбирайте lockExclusive. Нельзя держать транзакцию открытой, пока человек заполняет форму или внешний API отвечает минуту. Пример показывает короткие машинные проверки, а не бронь цены на весь просмотр страницы. При долгой работе уместнее версия строки и повторная проверка в момент сохранения.
Измените требование
Первый читатель завершился, второй ещё работает. Можно ли уже считать строку свободной для изменения?
Разбор следующего шага
Нет. Оставшийся shared-lock второго читателя продолжает конфликтовать с exclusive-lock. В приложении редактор либо ждёт в пределах выбранной политики, либо получает NOWAIT-отказ. Освобождение одного из нескольких держателей не равнозначно освобождению всех.
Выдать свободное задание, пока первое уже занято
Сервис делает превью товаров. Исполнитель A уже выбрал задание 1, но ещё удерживает его строку. Исполнитель B не должен ни ждать его, ни взять ту же работу: он получает свободное задание 2. После коротких транзакций обе строки отмечены processing с разными владельцами, а задание 3 ещё ожидает обработки.
- A удерживает id 1: preview_LED
- B ищет pending по id с LIMIT 1 и SKIP_LOCKED
- B получает id 2: preview_BOX → processing, worker-B → commit
- A закрепляет id 1 → commit; id 3 остаётся pending
Небольшая запись и её смысл
SKIP_LOCKED означает «пропусти строки, для которых сейчас нельзя получить требуемую блокировку». where выбирает ожидающие задания, orderby задаёт воспроизводимый порядок, limit(1) ограничивает размер выдачи. Пустой массив здесь означает, что подходящего доступного задания не найдено сейчас; часть подходящих строк может быть занята. Пока строка заблокирована, UPDATE сохраняет владельца и состояние processing, затем commit освобождает lock. Короткий фрагмент показывает выбор, а полный файл содержит обязательное закрепление: отдельно SELECT и поздний UPDATE без одной транзакции оставляют гонку.
$worker->startTransaction();
$jobs = $queue->select('id', 'task')->where('status = :state')->bind(['state' => 'pending'])->orderby('id')->limit(1)->lockExclusive(MYSQLX_LOCK_SKIP_LOCKED)->execute()->fetchAll();
// Если jobs не пуст: закрепить выбранный id за исполнителем в этой транзакции.
$worker->commit();Что получается на исходных данных
| Наблюдение | Результат |
|---|---|
| Задание worker-A | 1 |
| Задание worker-B | 2 |
| Владелец задания 1 | worker-A |
| Владелец задания 2 | worker-B |
| Задание 3 осталось | pending |
Самостоятельная программа и архив
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$config = x144Config();
$session = mysql_xdevapi\getSession($config['uri']);
$schema = null;
$owned = [];
$removed = [];
$rows = $checks = $observations = [];
try {
$schema = $session->getSchema($config['schema']);
if (!$schema->existsInDatabase()) {
throw new RuntimeException('Учебная схема не существует: подготовьте её отдельно.');
}
$runtime = ['php'=>PHP_VERSION, 'extension'=>phpversion('mysql_xdevapi'),
'server_version'=>$session->getServerVersion(), 'schema'=>$config['schema'],
'host'=>$config['host'], 'port'=>$config['port']];
$name=x144OwnTable($session,$schema,$owned,'worker_queue',
'(id INT PRIMARY KEY, task VARCHAR(40) NOT NULL, status VARCHAR(20) NOT NULL, claimed_by VARCHAR(20) NULL, INDEX queue_state (status,id)) ENGINE=InnoDB');
$queue=$schema->getTable($name);
$queue->insert('id','task','status','claimed_by')->values(
[1,'preview_LED','pending',null],[2,'preview_BOX','pending',null],[3,'preview_TAPE','pending',null])->execute();
$workerB=mysql_xdevapi\getSession($config['uri']);
$schemaB=$workerB->getSchema($config['schema']); $queueB=$schemaB->getTable($name);
$aOpen=$bOpen=false;
try {
$ids=[$session->sql('SELECT CONNECTION_ID() AS id')->execute()->fetchOne()['id'],
$workerB->sql('SELECT CONNECTION_ID() AS id')->execute()->fetchOne()['id']];
$session->startTransaction(); $aOpen=true;
// Worker A has been assigned candidate 1 and claims that exact existing row.
$first=$queue->select('id','task')->where('id = 1 AND status = :state')->bind(['state'=>'pending'])->lockExclusive()->execute()->fetchOne();
if (!$first) {throw new RuntimeException('Первое задание недоступно.');}
$workerB->startTransaction(); $bOpen=true;
$available=$queueB->select('id','task')->where('status = :state')->bind(['state'=>'pending'])
->orderby('id')->limit(1)->lockExclusive(MYSQLX_LOCK_SKIP_LOCKED)->execute()->fetchAll();
if (count($available)!==1) {throw new RuntimeException('Не найдено ожидаемое свободное задание.');}
$second=$available[0];
$bReceipt=$queueB->update()->set('status','processing')->set('claimed_by','worker-B')->where('id = :id')->bind(['id'=>$second['id']])->execute();
if ($bReceipt->getAffectedItemsCount()!==1) {throw new RuntimeException('Задание B не закреплено.');}
$workerB->commit(); $bOpen=false;
$aReceipt=$queue->update()->set('status','processing')->set('claimed_by','worker-A')->where('id = :id')->bind(['id'=>$first['id']])->execute();
if ($aReceipt->getAffectedItemsCount()!==1) {throw new RuntimeException('Задание A не закреплено.');}
$session->commit(); $aOpen=false;
$saved=$queue->select('id','status','claimed_by')->orderby('id')->execute()->fetchAll();
$rows=[['Задание worker-A',$first['id']],['Задание worker-B',$second['id']],
['Владелец задания 1',$saved[0]['claimed_by']],['Владелец задания 2',$saved[1]['claimed_by']],
['Задание 3 осталось',$saved[2]['status']]];
$checks=['independent_workers'=>$ids[0]!==$ids[1],
'locked_candidate_skipped'=>$first['id']===1 && $second['id']===2 && $first['id']!==$second['id'],
'claims_saved_exactly'=>$saved===[['id'=>1,'status'=>'processing','claimed_by'=>'worker-A'],['id'=>2,'status'=>'processing','claimed_by'=>'worker-B'],['id'=>3,'status'=>'pending','claimed_by'=>null]]];
$observations=['connection_ids'=>$ids,'held_candidate'=>$first,'available_candidates'=>$available,'saved_rows'=>$saved];
} finally {
try {if ($bOpen) {$workerB->rollback();}} finally {
try {if ($aOpen) {$session->rollback();}} finally {$workerB->close();}
}
}
} finally {
try {
if ($schema instanceof mysql_xdevapi\Schema) {
$removed = x144Cleanup($schema, $owned);
}
} finally {
$session->close();
}
}
x144Report('mysql144-x-table-skip-locked-queue', 'Выдать свободное задание, пока первое уже занято', $rows, $checks, $observations, $runtime, $owned, $removed);
Ожидаемый результат
Задание worker-A — 1
Задание worker-B — 2
Владелец задания 1 — worker-A
Владелец задания 2 — worker-B
Задание 3 осталось — pending
CLI-программа создаёт одну собственную таблицу InnoDB в подготовленной учебной схеме, выполняет запросы, сохраняет данные readback и удаляет только свой объект. Новые участники используют отдельные соединения; все транзакции завершаются до уборки. Это не HTTP-обработчик.
Таблица фактического запуска · Данные, типы и диагностика JSON.
Границы и типичные ошибки
В первом участнике кандидат 1 уже известен, поэтому он защищает точный PRIMARY KEY. Второй ищет свободный из очереди. Это минимальная демонстрация конкурентной выдачи, а не готовый планировщик. Индекс (status,id) соответствует отбору и порядку; без подходящего индекса скан может блокировать больше записей, чем возвращённый LIMIT. SKIP_LOCKED даёт неполное представление и не подходит для общего финансового отчёта. Он не обещает строгую справедливость и не исключает deadlock в других действиях транзакции. Генерацию изображения или внешнюю отправку выполняют после короткого закрепления; затем отдельно отмечают done. Для сбоя worker нужны срок аренды, повторное получение и идемпотентность. В этом уроке показана выдача, а не само построение превью.
Вопрос показывает неожиданную пустую выдачу второго участника при ORDER BY по полю без подходящего индекса, хотя LIMIT мал. Поэтому урок отделяет число возвращённых строк от плана доступа, использует индекс и не объявляет SKIP LOCKED гарантией ровно одной индексной блокировки. Обсуждение: Пустой SKIP LOCKED при сортировке по дате.
Разработчик получал одно задание двумя исполнителями: выбор строки и отметка начала обработки были разнесены. Наше решение связывает locking read и запись владельца в короткой транзакции, после которой может начинаться дорогая работа. Не переносим из обсуждения неточную идею, что блокируется всякое обычное чтение. Обсуждение: Два worker выбирают задание между SELECT и UPDATE.
Измените требование
Worker B получил пустой массив. Следует ли немедленно удалять всю очередь как завершённую?
Разбор следующего шага
Нет. Нужно закончить текущую транзакцию и выбрать политику следующей проверки: ограниченная пауза, событие или планировщик. Пустой SKIP_LOCKED-ответ не доказывает, что задач нет вообще. Для повторной обработки после аварии храните срок аренды и проверяйте владельца при завершении.
Отменить отклонённый резерв и освободить строку
Резерв начал изменять остаток, но локальное правило отклонило доставку. Нельзя оставить уменьшенный stock или зависшую блокировку. rollback восстанавливает оба поля и позволяет следующему заказу взять исходные две единицы; следующий резерв расходует одну и сохраняет владельца ORDER-B.
- Исходно stock 2, владелец null
- В транзакции: stock 1, владелец REJECTED
- Правило доставки отклоняет → finally rollback → stock 2, null
- Следующий NOWAIT успешен → stock 1, ORDER-B → commit
Небольшая запись и её смысл
Исключение передаёт управление из текущего блока. finally выполняется при выходе, в том числе после исключения; здесь он завершает отклонённую транзакцию. Это пример специально отказного пути, поэтому короткая запись всегда вызывает rollback и не содержит успешного commit. В полном файле флаг открытой транзакции не даёт откатывать уже завершённую; ожидаемый DomainException обрабатывается отдельно от ошибок MySQL. Внутреннее чтение показывает временные значения, последующее чтение — восстановленные, а новый NOWAIT другого соединения подтверждает освобождение lock.
$session->startTransaction();
try {
$row = $stock->select('stock')->where('id = 1')->lockExclusive()->execute()->fetchOne();
// Локальная проверка отклонила заказ.
throw new DomainException('Доставка недоступна');
} finally {
$session->rollback();
}Что получается на исходных данных
| Наблюдение | Результат |
|---|---|
| Остаток внутри отклонённого заказа | 1 |
| Остаток после rollback | 2 |
| Старый резерв отменён | да |
| Следующий получил строку через NOWAIT | да |
| Новый остаток | 1 |
| Новый владелец | ORDER-B |
Самостоятельная программа и архив
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$config = x144Config();
$session = mysql_xdevapi\getSession($config['uri']);
$schema = null;
$owned = [];
$removed = [];
$rows = $checks = $observations = [];
try {
$schema = $session->getSchema($config['schema']);
if (!$schema->existsInDatabase()) {
throw new RuntimeException('Учебная схема не существует: подготовьте её отдельно.');
}
$runtime = ['php'=>PHP_VERSION, 'extension'=>phpversion('mysql_xdevapi'),
'server_version'=>$session->getServerVersion(), 'schema'=>$config['schema'],
'host'=>$config['host'], 'port'=>$config['port']];
$name=x144OwnTable($session,$schema,$owned,'rollback_stock',
'(id INT PRIMARY KEY, stock INT NOT NULL, reserved_by VARCHAR(20) NULL) ENGINE=InnoDB');
$stock=$schema->getTable($name);
$stock->insert('id','stock','reserved_by')->values([1,2,null])->execute();
$next=mysql_xdevapi\getSession($config['uri']);
$nextSchema=$next->getSchema($config['schema']); $nextStock=$nextSchema->getTable($name);
$aOpen=$bOpen=false;
try {
$ids=[$session->sql('SELECT CONNECTION_ID() AS id')->execute()->fetchOne()['id'],
$next->sql('SELECT CONNECTION_ID() AS id')->execute()->fetchOne()['id']];
$rejection=null;
try {
$session->startTransaction(); $aOpen=true;
$before=$stock->select('stock')->where('id = 1')->lockExclusive()->execute()->fetchOne();
if ($before['stock']<1) {throw new RuntimeException('Нет остатка.');}
$change=$stock->update()->set('stock',$before['stock']-1)->set('reserved_by','REJECTED')->where('id = 1')->execute();
if ($change->getAffectedItemsCount()!==1) {throw new RuntimeException('Пробный резерв не записан.');}
$inside=$stock->select('stock','reserved_by')->where('id = 1')->execute()->fetchOne();
$deliveryAllowed=false; // Fixed local business condition; no external call.
if (!$deliveryAllowed) {throw new DomainException('Доставка в выбранную зону недоступна.');}
$session->commit(); $aOpen=false;
} catch (DomainException $error) {
$rejection=$error->getMessage();
} finally {
if ($aOpen) {$session->rollback(); $aOpen=false;}
}
$restored=$stock->select('stock','reserved_by')->where('id = 1')->execute()->fetchOne();
$next->startTransaction(); $bOpen=true;
$unlocked=$nextStock->select('stock','reserved_by')->where('id = 1')->lockExclusive(MYSQLX_LOCK_NOWAIT)->execute()->fetchOne();
if ($unlocked['stock']<1) {throw new RuntimeException('Нет остатка для следующего заказа.');}
$receipt=$nextStock->update()->set('stock',$unlocked['stock']-1)->set('reserved_by','ORDER-B')->where('id = 1')->execute();
if ($receipt->getAffectedItemsCount()!==1) {throw new RuntimeException('Следующий резерв не записан.');}
$next->commit(); $bOpen=false;
$saved=$stock->select('stock','reserved_by')->where('id = 1')->execute()->fetchOne();
$rows=[['Остаток внутри отклонённого заказа',$inside['stock']],['Остаток после rollback',$restored['stock']],
['Старый резерв отменён',$restored['reserved_by']===null ? 'да' : 'нет'],
['Следующий получил строку через NOWAIT','да'],['Новый остаток',$saved['stock']],['Новый владелец',$saved['reserved_by']]];
$checks=['independent_sessions'=>$ids[0]!==$ids[1],
'business_rejection_observed'=>$rejection==='Доставка в выбранную зону недоступна.' && $inside===['stock'=>1,'reserved_by'=>'REJECTED'],
'rollback_restores_entire_row'=>$restored===['stock'=>2,'reserved_by'=>null],
'rollback_releases_lock'=>$unlocked===$restored,
'next_reservation_saved'=>$saved===['stock'=>1,'reserved_by'=>'ORDER-B']];
$observations=['connection_ids'=>$ids,'before'=>$before,'inside'=>$inside,'rejection'=>$rejection,'after_rollback'=>$restored,'next_locked_read'=>$unlocked,'saved'=>$saved];
} finally {
try {if ($bOpen) {$next->rollback();}} finally {
try {if ($aOpen) {$session->rollback();}} finally {$next->close();}
}
}
} finally {
try {
if ($schema instanceof mysql_xdevapi\Schema) {
$removed = x144Cleanup($schema, $owned);
}
} finally {
$session->close();
}
}
x144Report('mysql144-x-table-rollback-release', 'Отменить отклонённый резерв и освободить строку', $rows, $checks, $observations, $runtime, $owned, $removed);
Ожидаемый результат
Остаток внутри отклонённого заказа — 1
Остаток после rollback — 2
Старый резерв отменён — да
Следующий получил строку через NOWAIT — да
Новый остаток — 1
Новый владелец — ORDER-B
CLI-программа создаёт одну собственную таблицу InnoDB в подготовленной учебной схеме, выполняет запросы, сохраняет данные readback и удаляет только свой объект. Новые участники используют отдельные соединения; все транзакции завершаются до уборки. Это не HTTP-обработчик.
Таблица фактического запуска · Данные, типы и диагностика JSON.
Границы и типичные ошибки
В реальном приложении дешёвые локальные проверки выполняют до изменения, когда это возможно; намеренное позднее отклонение здесь нужно, чтобы показать действие rollback. Отмена касается транзакционных изменений InnoDB. Отправленное письмо, HTTP-запрос или записанный внешний файл она не возвращает назад; DDL тоже не следует помещать внутрь такой операции. Нельзя ловить любой Throwable и всё равно сообщать «заказ сохранён». Если соединение потеряно при commit, нужен отдельный разбор неопределённого результата, а не слепой повтор. Финальная строка не хранит историю всех резервов: для неё нужна отдельная связанная таблица.
Измените требование
Уберите проверку доставки и разрешите первый заказ. Где должен находиться commit?
Разбор следующего шага
После всех проверок и связанных записей, до выхода из успешного пути. Затем флаг открытой транзакции сбрасывается, и finally уже не делает rollback. Второй участник должен прочитать новый остаток после commit; он не должен пользоваться значением, которое было получено до транзакции.
Фабрики и режимы чтения
| Инструмент | Практическая задача |
|---|---|
| Table::__construct | Открыть существующий каталог через Schema и получить объект Table. |
| TableInsert::__construct | Собрать вставку товара или задания и выполнить её одной командой. |
| TableSelect::__construct | Получить команду выборки, настроить её и затем прочитать результат. |
| TableUpdate::__construct | Получить команду изменения и связать поля с нужной строкой. |
| TableDelete::__construct | Создать удаление с точным условием вместо удаления всего каталога. |
| Result::__construct | Получить квитанцию записи и связать её с сохранёнными данными. |
| RowResult::__construct | Прочитать строки из выполненной табличной выборки. |
| SqlStatement::__construct | Подготовить SQL с параметрами через существующее соединение. |
| SqlStatementResult::__construct | Получить строки и метаданные именно выполненного SQL. |
| ColumnResult::__construct | Понять подпись и тип столбца, который пришёл в отчёт. |
| TableSelect::lockExclusive | Защитить прочитанную строку перед согласованным изменением остатка или владельца задания. |
| TableSelect::lockShared | Прочитать строку совместно с другими проверяющими и защитить её от изменения до завершения короткой транзакции. |
Развить модель данных и обработку отказов
Теперь команда связана с полезным результатом: изменённый товар, выбранные строки отчёта, единственный владелец резерва или свободное задание. Следующий шаг — отношения и ограничения реляционной базы, связанные изменения таблиц и ограниченное время жизни соединений.
Для коллекций документов похожие задачи разбираются в согласовании резервов. Для SQL-ответов используйте квитанции, предупреждения и несколько наборов. Успешное выполнение шести уроков не означает полного покрытия транзакционного сервера или всего PHP Manual.
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
mysql_xdevapi\Table::__constructОткрыть существующий каталог через Schema и получить объект Table.
Страницы выбранных методов не указывают начальную версию; учебный профиль PECL mysql_xdevapi 8.0.30, MySQL 8.4.13. · mysql_xdevapi
private mysql_xdevapi\Table::__construct()Открыть существующий каталог через Schema и получить объект Table.
Запись, параметры и результат
private mysql_xdevapi\Table::__construct()Параметров у указанного конструктора нет. private закрывает прямой вызов из пользовательского кода; аргументы передаются публичной фабрике: Schema::getTable($name).
Самостоятельного возвращаемого значения конструктора здесь нет. Настроенный объект Table возвращает фабрика; он связан с конкретной таблицей, запросом или серверным ответом.
Официальная страница показывает private-конструктор без параметров и путь getSession → getSchema → getTable. Дескриптор Table не означает, что выполнен CREATE TABLE: создание, проверка существования и чтение — отдельные действия.
Два применения
Открыть карточку товара
После фабрики можно запросить конкретный LED и показать его остаток; объект доступа не является массивом товаров.
$catalog = $schema->getTable($name);
$lamp = $catalog->select('sku', 'stock')->where('sku = :sku')->bind(['sku' => 'LED'])->execute()->fetchOne();Контекстный фрагмент: соединение и названные объекты уже подготовлены. Исходные данные, самостоятельный файл и результат.
Обратиться к той же таблице для отчёта
Отчёт получает только выбранные поля и подходящие строки своей таблицы продаж.
$sales = $schema->getTable($name);
$reportRows = $sales->select('sku', 'qty')->where('qty >= 2')->orderby('id')->execute()->fetchAll();Контекстный фрагмент: соединение и названные объекты уже подготовлены. Исходные данные, самостоятельный файл и результат.
Границы и следующий шаг
Прямое new mysql_xdevapi\Table() из пользовательского кода вызывает Error из-за закрытого конструктора. Обход через Reflection не создаёт корректное внутреннее состояние. Если таблица не существует, успешное получение дескриптора не заменяет проверку existsInDatabase или обработку ошибки запроса. Имя выбирают из доверенной конфигурации, а не из произвольного HTTP-параметра.
Команда и сохранённый каталог связывает конструкторы с CRUD; SQL-отчёт — с выбранными столбцами. Подробные метаданные и квитанции и предупреждения развивают эти результаты.
mysql_xdevapi\TableInsert::__constructСобрать вставку товара или задания и выполнить её одной командой.
Страницы выбранных методов не указывают начальную версию; учебный профиль PECL mysql_xdevapi 8.0.30, MySQL 8.4.13. · mysql_xdevapi
private mysql_xdevapi\TableInsert::__construct()Собрать вставку товара или задания и выполнить её одной командой.
Запись, параметры и результат
private mysql_xdevapi\TableInsert::__construct()Параметров у указанного конструктора нет. private закрывает прямой вызов из пользовательского кода; аргументы передаются публичной фабрике: Table::insert($columns).
Самостоятельного возвращаемого значения конструктора здесь нет. Настроенный объект TableInsert возвращает фабрика; он связан с конкретной таблицей, запросом или серверным ответом.
Официальный baseline: объект инициируется insert(), конструктор закрыт и не принимает параметров; values добавляет значения, execute выполняет вставку. Подготовка команды сама по себе не сохраняет строку.
Два применения
Добавить два товара
В исходных данных урока появятся LED и BOX, квитанция сообщает о двух вставленных строках.
$command = $catalog->insert('sku', 'stock')->values(['LED', 5], ['BOX', 3]);
$receipt = $command->execute();
$inserted = $receipt->getAffectedItemsCount();Контекстный фрагмент: соединение и названные объекты уже подготовлены. Исходные данные, самостоятельный файл и результат.
Поставить задание в очередь
Новое задание получает pending и ещё не имеет исполнителя; выбор worker происходит позже.
$command = $queue->insert('id', 'task', 'status', 'claimed_by');
$receipt = $command->values([4, 'preview_NEW', 'pending', null])->execute();Контекстный фрагмент: соединение и названные объекты уже подготовлены. Исходные данные, самостоятельный файл и результат.
Границы и следующий шаг
Прямое new mysql_xdevapi\TableInsert() из пользовательского кода вызывает Error из-за закрытого конструктора. Обход через Reflection не создаёт корректное внутреннее состояние. Порядок значений соответствует порядку столбцов. UNIQUE-конфликт не превращается в успешную вставку, а добавленное pending ещё не означает выполненную работу. Квитанцию и фактическую строку различают.
Команда и сохранённый каталог связывает конструкторы с CRUD; SQL-отчёт — с выбранными столбцами. Подробные метаданные и квитанции и предупреждения развивают эти результаты.
mysql_xdevapi\TableSelect::__constructПолучить команду выборки, настроить её и затем прочитать результат.
Страницы выбранных методов не указывают начальную версию; учебный профиль PECL mysql_xdevapi 8.0.30, MySQL 8.4.13. · mysql_xdevapi
private mysql_xdevapi\TableSelect::__construct()Получить команду выборки, настроить её и затем прочитать результат.
Запись, параметры и результат
private mysql_xdevapi\TableSelect::__construct()Параметров у указанного конструктора нет. private закрывает прямой вызов из пользовательского кода; аргументы передаются публичной фабрике: Table::select($columns).
Самостоятельного возвращаемого значения конструктора здесь нет. Настроенный объект TableSelect возвращает фабрика; он связан с конкретной таблицей, запросом или серверным ответом.
Объект возвращается методом select(), а запрос выполняется execute(). Официальный пример сочетает where, bind, orderBy, execute и fetchAll. Его иллюстрация с age=42 при условии age>42 противоречива: такую строку условие не пропускает.
Два применения
Остаток одного товара
Команда становится RowResult при выполнении; только fetchOne получает строку LED.
$query = $catalog->select('sku', 'stock')->where('sku = :sku')->bind(['sku' => 'LED']);
$result = $query->execute();
$lamp = $result->fetchOne();Контекстный фрагмент: соединение и названные объекты уже подготовлены. Исходные данные, самостоятельный файл и результат.
Доступное задание в транзакции
В открытой транзакции берётся подходящее незанятое задание; после этого его надо закрепить UPDATE до commit.
$query = $queue->select('id', 'task')->where('status = :state')->bind(['state' => 'pending']);
$jobs = $query->orderby('id')->limit(1)->lockExclusive(MYSQLX_LOCK_SKIP_LOCKED)->execute()->fetchAll();Контекстный фрагмент: соединение и названные объекты уже подготовлены. Исходные данные, самостоятельный файл и результат.
Границы и следующий шаг
Прямое new mysql_xdevapi\TableSelect() из пользовательского кода вызывает Error из-за закрытого конструктора. Обход через Reflection не создаёт корректное внутреннее состояние. Возвращаемые строки и builder — разные объекты. Запрос без where не ограничивается автоматически текущим товаром. LIMIT сокращает выдачу, но не является обещанием стоимости плана или числа затронутых индексных блокировок.
Команда и сохранённый каталог связывает конструкторы с CRUD; SQL-отчёт — с выбранными столбцами. Подробные метаданные и квитанции и предупреждения развивают эти результаты.
mysql_xdevapi\TableUpdate::__constructПолучить команду изменения и связать поля с нужной строкой.
Страницы выбранных методов не указывают начальную версию; учебный профиль PECL mysql_xdevapi 8.0.30, MySQL 8.4.13. · mysql_xdevapi
private mysql_xdevapi\TableUpdate::__construct()Получить команду изменения и связать поля с нужной строкой.
Запись, параметры и результат
private mysql_xdevapi\TableUpdate::__construct()Параметров у указанного конструктора нет. private закрывает прямой вызов из пользовательского кода; аргументы передаются публичной фабрике: Table::update().
Самостоятельного возвращаемого значения конструктора здесь нет. Настроенный объект TableUpdate возвращает фабрика; он связан с конкретной таблицей, запросом или серверным ответом.
Официальный конструктор закрыт, аргументов нет, объект инициируется update(). Пример страницы показывает set, where, limit и orderby перед execute. Условия и новые значения принадлежат команде, а не конструктору.
Два применения
Исправить остаток LED
Обновляется выбранный товар, затем отдельное чтение показывает stock=7.
$command = $catalog->update()->set('stock', 7)->where('sku = :sku')->bind(['sku' => 'LED']);
$receipt = $command->execute();Контекстный фрагмент: соединение и названные объекты уже подготовлены. Исходные данные, самостоятельный файл и результат.
Закрепить выбранное задание
В транзакции worker записывает владельца именно той строки, которую уже получил с блокировкой.
$command = $queue->update()->set('status', 'processing')->set('claimed_by', 'worker-B')->where('id = :id')->bind(['id' => $chosenId]);
$receipt = $command->execute();Контекстный фрагмент: соединение и названные объекты уже подготовлены. Исходные данные, самостоятельный файл и результат.
Границы и следующий шаг
Прямое new mysql_xdevapi\TableUpdate() из пользовательского кода вызывает Error из-за закрытого конструктора. Обход через Reflection не создаёт корректное внутреннее состояние. Отсутствие where может изменить весь каталог. В выбранном расширении выражения set с параметрами задают до where, затем выполняют bind; правила подробно разобраны в реляционных сценариях. affected=1 не обязательно означает, что значение отличалось от старого.
Команда и сохранённый каталог связывает конструкторы с CRUD; SQL-отчёт — с выбранными столбцами. Подробные метаданные и квитанции и предупреждения развивают эти результаты.
mysql_xdevapi\TableDelete::__constructСоздать удаление с точным условием вместо удаления всего каталога.
Страницы выбранных методов не указывают начальную версию; учебный профиль PECL mysql_xdevapi 8.0.30, MySQL 8.4.13. · mysql_xdevapi
private mysql_xdevapi\TableDelete::__construct()Создать удаление с точным условием вместо удаления всего каталога.
Запись, параметры и результат
private mysql_xdevapi\TableDelete::__construct()Параметров у указанного конструктора нет. private закрывает прямой вызов из пользовательского кода; аргументы передаются публичной фабрике: Table::delete().
Самостоятельного возвращаемого значения конструктора здесь нет. Настроенный объект TableDelete возвращает фабрика; он связан с конкретной таблицей, запросом или серверным ответом.
Конструктор private без параметров; команда инициируется delete(). Официальный пример использует where/bind, orderby, limit и execute. Создание объекта не удаляет данные до execute.
Два применения
Убрать собственную пробную строку
Удаляется DRAFT по ID, полученному при её вставке; LED и BOX сохраняются.
$command = $catalog->delete()->where('id = :id')->bind(['id' => $draftId]);
$receipt = $command->execute();Контекстный фрагмент: соединение и названные объекты уже подготовлены. Исходные данные, самостоятельный файл и результат.
Отменить ещё не взятое задание
Дополнительное условие не даёт удалить задание, которое уже стало processing; ноль удалений требует нового чтения состояния.
$command = $queue->delete()->where('id = :id AND status = :state')->bind(['id' => 3, 'state' => 'pending']);
$removed = $command->execute()->getAffectedItemsCount();Контекстный фрагмент: соединение и названные объекты уже подготовлены. Исходные данные, самостоятельный файл и результат.
Границы и следующий шаг
Прямое new mysql_xdevapi\TableDelete() из пользовательского кода вызывает Error из-за закрытого конструктора. Обход через Reflection не создаёт корректное внутреннее состояние. Не определяйте право удаления только по заранее прочитанному массиву: состояние могло измениться. Для истории заказов часто нужен статус отмены, а не физическое DELETE. Очистка учебной таблицы выполняется отдельно и только по имени, которым владеет программа.
Команда и сохранённый каталог связывает конструкторы с CRUD; SQL-отчёт — с выбранными столбцами. Подробные метаданные и квитанции и предупреждения развивают эти результаты.
mysql_xdevapi\Result::__constructПолучить квитанцию записи и связать её с сохранёнными данными.
Страницы выбранных методов не указывают начальную версию; учебный профиль PECL mysql_xdevapi 8.0.30, MySQL 8.4.13. · mysql_xdevapi
private mysql_xdevapi\Result::__construct()Получить квитанцию записи и связать её с сохранёнными данными.
Запись, параметры и результат
private mysql_xdevapi\Result::__construct()Параметров у указанного конструктора нет. private закрывает прямой вызов из пользовательского кода; аргументы передаются публичной фабрике: TableInsert/TableUpdate/TableDelete::execute().
Самостоятельного возвращаемого значения конструктора здесь нет. Настроенный объект Result возвращает фабрика; он связан с конкретной таблицей, запросом или серверным ответом.
Страница конструктора описывает объект результатов записи с generated IDs, AUTO_INCREMENT и предупреждениями. Её пример получает AUTO_INCREMENT от вставки. Конструктор закрыт; квитанцию создаёт выполненная операция. Возможности документа и таблицы не надо смешивать: generated document IDs не являются универсальными табличными ID.
Два применения
Проверить число исправленных строк
Квитанция сообщает о записи, а saved подтверждает фактический остаток.
$receipt = $catalog->update()->set('stock', 7)->where('sku = :sku')->bind(['sku' => 'LED'])->execute();
$matched = $receipt->getAffectedItemsCount();
$saved = $catalog->select('stock')->where('sku = :sku')->bind(['sku' => 'LED'])->execute()->fetchOne();Контекстный фрагмент: соединение и названные объекты уже подготовлены. Исходные данные, самостоятельный файл и результат.
Удалить именно созданный черновик
AUTO_INCREMENT связывает вставку и последующее действие без поиска MAX(id), который мог бы принадлежать чужой вставке.
$receipt = $catalog->insert('sku', 'stock')->values(['DRAFT', 0])->execute();
$draftId = $receipt->getAutoIncrementValue();
$catalog->delete()->where('id = :id')->bind(['id' => $draftId])->execute();Контекстный фрагмент: соединение и названные объекты уже подготовлены. Исходные данные, самостоятельный файл и результат.
Границы и следующий шаг
Прямое new mysql_xdevapi\Result() из пользовательского кода вызывает Error из-за закрытого конструктора. Обход через Reflection не создаёт корректное внутреннее состояние. Result не содержит строки SELECT. affected=0 и ошибка запроса — разные состояния. Предупреждения надо читать у конкретной квитанции; сам факт получения Result не проверяет бизнес-правила.
Команда и сохранённый каталог связывает конструкторы с CRUD; SQL-отчёт — с выбранными столбцами. Подробные метаданные и квитанции и предупреждения развивают эти результаты.
mysql_xdevapi\RowResult::__constructПрочитать строки из выполненной табличной выборки.
Страницы выбранных методов не указывают начальную версию; учебный профиль PECL mysql_xdevapi 8.0.30, MySQL 8.4.13. · mysql_xdevapi
private mysql_xdevapi\RowResult::__construct()Прочитать строки из выполненной табличной выборки.
Запись, параметры и результат
private mysql_xdevapi\RowResult::__construct()Параметров у указанного конструктора нет. private закрывает прямой вызов из пользовательского кода; аргументы передаются публичной фабрике: TableSelect::execute().
Самостоятельного возвращаемого значения конструктора здесь нет. Настроенный объект RowResult возвращает фабрика; он связан с конкретной таблицей, запросом или серверным ответом.
Официальная страница описывает набор результатов запроса и показывает select/where/execute/fetchAll. Конструктор закрыт без параметров. Строки являются данными внутри результата, а не объектами RowResult на каждый товар.
Два применения
Показать каталог после изменения
Массив содержит LED со stock=7 и BOX со stock=3; по нему уже можно строить строки HTML-таблицы.
$result = $catalog->select('sku', 'stock')->orderby('id')->execute();
$products = $result->fetchAll();Контекстный фрагмент: соединение и названные объекты уже подготовлены. Исходные данные, самостоятельный файл и результат.
Проверить остаток под блокировкой
В открытой транзакции строка даёт значение для проверки stock до изменения; объект результата не заменяет commit/rollback.
$result = $catalog->select('stock')->where('id = 1')->lockExclusive(MYSQLX_LOCK_NOWAIT)->execute();
$row = $result->fetchOne();Контекстный фрагмент: соединение и названные объекты уже подготовлены. Исходные данные, самостоятельный файл и результат.
Границы и следующий шаг
Прямое new mysql_xdevapi\RowResult() из пользовательского кода вызывает Error из-за закрытого конструктора. Обход через Reflection не создаёт корректное внутреннее состояние. Пустой результат не равен отказу соединения. Не вызывайте fetchAll после fetchOne с предположением «только остаток»: в выбранном профиле буферный fetchAll возвращает весь набор; выбирайте один способ чтения либо отдельный запрос. Для большого списка нужны ограничение и пагинация.
Команда и сохранённый каталог связывает конструкторы с CRUD; SQL-отчёт — с выбранными столбцами. Подробные метаданные и квитанции и предупреждения развивают эти результаты.
mysql_xdevapi\SqlStatement::__constructПодготовить SQL с параметрами через существующее соединение.
Страницы выбранных методов не указывают начальную версию; учебный профиль PECL mysql_xdevapi 8.0.30, MySQL 8.4.13. · mysql_xdevapi
private mysql_xdevapi\SqlStatement::__construct()Подготовить SQL с параметрами через существующее соединение.
Запись, параметры и результат
private mysql_xdevapi\SqlStatement::__construct()Параметров у указанного конструктора нет. private закрывает прямой вызов из пользовательского кода; аргументы передаются публичной фабрике: Session::sql($query).
Самостоятельного возвращаемого значения конструктора здесь нет. Настроенный объект SqlStatement возвращает фабрика; он связан с конкретной таблицей, запросом или серверным ответом.
В PHP Manual для конструктора оставлены только private-сигнатура без параметров, предупреждение о недокументированности и пустой пример. Путь создания подтверждён matching исходником: Session::sql создаёт SqlStatement; сам конструктор не является интерфейсом подключения.
Два применения
Отобрать продажи по количеству
Порог 2 передаётся как значение, сервер возвращает подходящие строки отчёта.
$statement = $session->sql('SELECT sku AS product, qty FROM ' . $qualifiedTable . ' WHERE qty >= ? ORDER BY id');
$result = $statement->bind(2)->execute();Контекстный фрагмент: соединение и названные объекты уже подготовлены. Исходные данные, самостоятельный файл и результат.
Отобрать конкретный товар
Поиск LED не требует склеивать кавычки вокруг пользовательского значения.
$statement = $session->sql('SELECT sku, qty FROM ' . $qualifiedTable . ' WHERE sku = ?');
$product = $statement->bind('LED')->execute()->fetchOne();Контекстный фрагмент: соединение и названные объекты уже подготовлены. Исходные данные, самостоятельный файл и результат.
Границы и следующий шаг
Прямое new mysql_xdevapi\SqlStatement() из пользовательского кода вызывает Error из-за закрытого конструктора. Обход через Reflection не создаёт корректное внутреннее состояние. Значения передают bind; имя таблицы placeholder не заменяет. Пустая строка SQL в выбранном расширении возвращает false с предупреждением. Имя/URI соединения и пароль не должны попадать в отчёт об ошибке. Подробности async относятся к отдельной главе результатов.
Команда и сохранённый каталог связывает конструкторы с CRUD; SQL-отчёт — с выбранными столбцами. Подробные метаданные и квитанции и предупреждения развивают эти результаты.
mysql_xdevapi\SqlStatementResult::__constructПолучить строки и метаданные именно выполненного SQL.
Страницы выбранных методов не указывают начальную версию; учебный профиль PECL mysql_xdevapi 8.0.30, MySQL 8.4.13. · mysql_xdevapi
private mysql_xdevapi\SqlStatementResult::__construct()Получить строки и метаданные именно выполненного SQL.
Запись, параметры и результат
private mysql_xdevapi\SqlStatementResult::__construct()Параметров у указанного конструктора нет. private закрывает прямой вызов из пользовательского кода; аргументы передаются публичной фабрике: SqlStatement::execute() без ASYNC.
Самостоятельного возвращаемого значения конструктора здесь нет. Настроенный объект SqlStatementResult возвращает фабрика; он связан с конкретной таблицей, запросом или серверным ответом.
Страница конструктора содержит закрытую сигнатуру без аргументов и предупреждение о недокументированности. Рабочий объект создаёт синхронное выполнение SqlStatement. ASYNC — отдельный протокол получения ответа; нельзя безусловно приписывать каждому execute объект результата.
Два применения
Вывести строки отчёта
Для подготовленных продаж видны LED и BOX. Выборка фильтруется в MySQL до передачи массива PHP.
$result = $session->sql('SELECT sku AS product, qty FROM ' . $qualifiedTable . ' WHERE qty >= ? ORDER BY id')->bind(2)->execute();
$rows = $result->fetchAll();Контекстный фрагмент: соединение и названные объекты уже подготовлены. Исходные данные, самостоятельный файл и результат.
Связать заголовки с выборкой
Имена product и total_kopecks относятся к результату запроса, а не к произвольному набору полей всей таблицы.
$result = $session->sql('SELECT sku AS product, qty * price_kopecks AS total_kopecks FROM ' . $qualifiedTable . ' ORDER BY id')->execute();
$columnNames = $result->getColumnNames();
$rows = $result->fetchAll();Контекстный фрагмент: соединение и названные объекты уже подготовлены. Исходные данные, самостоятельный файл и результат.
Границы и следующий шаг
Прямое new mysql_xdevapi\SqlStatementResult() из пользовательского кода вызывает Error из-за закрытого конструктора. Обход через Reflection не создаёт корректное внутреннее состояние. Не путайте SqlStatement, который отправляет запрос, и SqlStatementResult, который описывает ответ. Для CALL с несколькими наборами нужно прочитать последовательность до конца; здесь один обычный SELECT. Квитанция SQL-записи тоже не заменяет отдельное чтение сохранённой строки.
Команда и сохранённый каталог связывает конструкторы с CRUD; SQL-отчёт — с выбранными столбцами. Подробные метаданные и квитанции и предупреждения развивают эти результаты.
mysql_xdevapi\ColumnResult::__constructПонять подпись и тип столбца, который пришёл в отчёт.
Страницы выбранных методов не указывают начальную версию; учебный профиль PECL mysql_xdevapi 8.0.30, MySQL 8.4.13. · mysql_xdevapi
private mysql_xdevapi\ColumnResult::__construct()Понять подпись и тип столбца, который пришёл в отчёт.
Запись, параметры и результат
private mysql_xdevapi\ColumnResult::__construct()Параметров у указанного конструктора нет. private закрывает прямой вызов из пользовательского кода; аргументы передаются публичной фабрике: RowResult::getColumns() или SqlStatementResult::getColumns().
Самостоятельного возвращаемого значения конструктора здесь нет. Настроенный объект ColumnResult возвращает фабрика; он связан с конкретной таблицей, запросом или серверным ответом.
Официальная страница указывает метаданные столбца, включая кодировку, и фабрику RowResult::getColumns. В SQL-ветке её старого примера показан FieldMetadata; matching 8.0.30 и новый прототип возвращают ColumnResult и для SqlStatementResult. Конструктор закрыт, параметров нет.
Два применения
Подписать поле с псевдонимом
Для sku AS product подпись product отличается от исходного sku; это полезно для заголовка и диагностики отчёта.
$result = $statement->execute();
$columns = $result->getColumns();
$label = $columns[0]->getColumnLabel();
$sourceName = $columns[0]->getColumnName();Контекстный фрагмент: соединение и названные объекты уже подготовлены. Исходные данные, самостоятельный файл и результат.
Получить описание полей обычной Table-выборки
Описание stock приходит от реально выполненной выборки; строковые значения и информация о поле разделены.
$result = $catalog->select('sku', 'stock')->execute();
$columns = $result->getColumns();
$stockType = $columns[1]->getType();Контекстный фрагмент: соединение и названные объекты уже подготовлены. Исходные данные, самостоятельный файл и результат.
Границы и следующий шаг
Прямое new mysql_xdevapi\ColumnResult() из пользовательского кода вызывает Error из-за закрытого конструктора. Обход через Reflection не создаёт корректное внутреннее состояние. Метаданные не сообщают, что пользователь имеет право менять столбец. Псевдоним и исходное имя могут отличаться, а вычисляемому выражению не обязательно соответствует исходный столбец. Значения type — константы API, их нельзя принимать за номер SQL-столбца. Подробные типы и точность находятся в главе метаданных.
Команда и сохранённый каталог связывает конструкторы с CRUD; SQL-отчёт — с выбранными столбцами. Подробные метаданные и квитанции и предупреждения развивают эти результаты.
mysql_xdevapi\TableSelect::lockExclusiveЗащитить прочитанную строку перед согласованным изменением остатка или владельца задания.
Страницы выбранных методов не указывают начальную версию; учебный профиль PECL mysql_xdevapi 8.0.30, MySQL 8.4.13. · mysql_xdevapi
public mysql_xdevapi\TableSelect::lockExclusive(int $lock_waiting_option = MYSQLX_LOCK_DEFAULT): mysql_xdevapi\TableSelectЗащитить прочитанную строку перед согласованным изменением остатка или владельца задания.
Запись, параметры и результат
public mysql_xdevapi\TableSelect::lockExclusive(int $lock_waiting_option = MYSQLX_LOCK_DEFAULT): mysql_xdevapi\TableSelectНеобязательный целочисленный режим: MYSQLX_LOCK_DEFAULT — обычное ожидание конфликтующего row-lock; MYSQLX_LOCK_NOWAIT — отказ вместо ожидания; MYSQLX_LOCK_SKIP_LOCKED — исключение занятых строк из выдачи. Эти константы глобальные.
Возвращает тот же TableSelect для продолжения цепочки. Блокирующее чтение запускается execute, результат чтения — RowResult. Серверный конфликт приходит при выполнении, а не при простом создании builder.
PHP Manual показывает необязательный режим, три допустимые константы и пример startTransaction → lockExclusive → execute → commit. InnoDB удерживает блокировки до commit/rollback. Строку следует искать по подходящему индексу: range/скан может блокировать больше, чем один возвращённый товар. Для FOR UPDATE нужны SELECT и как минимум одно из UPDATE, DELETE или LOCK TABLES; пример использует уже настроенный учебный профиль.
Два применения
Проверить последний остаток
Короткий успешный путь защищает прочитанный остаток. В полном файле уменьшение, владелец и обработка отказа показаны вместе.
$session->startTransaction();
$row = $catalog->select('stock')->where('id = 1')->lockExclusive()->execute()->fetchOne();
// Проверить stock и изменить его в той же транзакции.
$session->commit();Контекстный фрагмент: соединение и названные объекты уже подготовлены. Исходные данные, самостоятельный файл и результат.
Получить свободное задание
Занятое первым worker задание пропускается. Само чтение ещё не записывает владельца; полный сценарий сохраняет его до commit.
$session->startTransaction();
$jobs = $queue->select('id', 'task')->where('status = :state')->bind(['state' => 'pending'])->orderby('id')->limit(1)->lockExclusive(MYSQLX_LOCK_SKIP_LOCKED)->execute()->fetchAll();
// Закрепить jobs[0] через UPDATE, если строка получена.
$session->commit();Контекстный фрагмент: соединение и названные объекты уже подготовлены. Исходные данные, самостоятельный файл и результат.
Границы и следующий шаг
Обычное consistent read может читать снимок несмотря на exclusive-lock; не обещайте запрет любых SELECT. NOWAIT и SKIP LOCKED касаются row-lock, не любого ожидания сервера. SKIP LOCKED даёт неполное представление и небезопасен для statement-based replication. DEFAULT не означает таймаут 0. Не запускайте два конфликтующих DEFAULT-запроса последовательно в одном потоке, если первый будет освобождён только после второго. В выбранном коннекторе 3572 находится в previous-цепочке внешнего 10000. Не всякая ошибка 10000 означает конфликт: рассматривайте полную цепочку и отдельную политику повторов.
Последний остаток, совместные читатели и освобождение при откате показывают разные ситуации. Отдельные Session и время их жизни объясняют участников.
mysql_xdevapi\TableSelect::lockSharedПрочитать строку совместно с другими проверяющими и защитить её от изменения до завершения короткой транзакции.
Страницы выбранных методов не указывают начальную версию; учебный профиль PECL mysql_xdevapi 8.0.30, MySQL 8.4.13. · mysql_xdevapi
public mysql_xdevapi\TableSelect::lockShared(int $lock_waiting_option = MYSQLX_LOCK_DEFAULT): mysql_xdevapi\TableSelectПрочитать строку совместно с другими проверяющими и защитить её от изменения до завершения короткой транзакции.
Запись, параметры и результат
public mysql_xdevapi\TableSelect::lockShared(int $lock_waiting_option = MYSQLX_LOCK_DEFAULT): mysql_xdevapi\TableSelectНеобязательный режим ожидания принимает глобальные MYSQLX_LOCK_DEFAULT, MYSQLX_LOCK_NOWAIT или MYSQLX_LOCK_SKIP_LOCKED. NOWAIT не запрещает совместимый shared-lock: второй shared-читатель может получить ту же строку сразу.
Настроенный TableSelect; execute выполняет запрос и возвращает RowResult. Ошибка конфликта возникает при выполнении. Транзакцию всё равно надо завершить, даже если она ничего не изменила.
Закрытых параметров или отдельного конструктора блокировки нет: это метод уже созданной TableSelect. Официальный baseline содержит три режима и пример транзакции. Утверждение про одну активную блокировку относится к выбору режима запроса, а не к запрету нескольких совместных читателей; InnoDB допускает S+S и конфликтует S+X. FOR SHARE требует SELECT. Commit и rollback освобождают полученные блокировки.
Два применения
Сверить цену при подготовке счёта
В выбранной строке читается 1200, а изменение другим участником требует завершения этой проверки.
$session->startTransaction();
$price = $prices->select('price_kopecks')->where('id = 1')->lockShared()->execute()->fetchOne();
// Короткая локальная проверка price_kopecks.
$session->commit();Контекстный фрагмент: соединение и названные объекты уже подготовлены. Исходные данные, самостоятельный файл и результат.
Присоединить второго проверяющего
Если первый держит shared-lock, второй shared NOWAIT совместим с ним; exclusive NOWAIT третьего участника даёт конфликт.
$reader->startTransaction();
$second = $readerPrices->select('price_kopecks')->where('id = 1')->lockShared(MYSQLX_LOCK_NOWAIT)->execute()->fetchOne();
$reader->commit();Контекстный фрагмент: соединение и названные объекты уже подготовлены. Исходные данные, самостоятельный файл и результат.
Границы и следующий шаг
Shared-lock не подходит для схемы «оба прочитали счётчик и оба затем повышают блокировку»: такое повышение может привести к deadlock. Если цель заранее состоит в изменении, рассмотрите exclusive-lock сразу. Один builder имеет один режим; вызовы lockShared/lockExclusive до execute заменяют поле режима, а не складывают независимые блокировки. Не держите транзакцию на время ожидания пользователя. Обычное чтение с MVCC, явная shared-блокировка и табличный LOCK TABLES — разные инструменты.
Последний остаток, совместные читатели и освобождение при откате показывают разные ситуации. Отдельные Session и время их жизни объясняют участников.
Источники и обсуждения
- PHP Manual — Table::__construct
- PHP Manual — TableInsert::__construct
- PHP Manual — TableSelect::__construct
- PHP Manual — TableUpdate::__construct
- PHP Manual — TableDelete::__construct
- PHP Manual — Result::__construct
- PHP Manual — RowResult::__construct
- PHP Manual — SqlStatement::__construct
- PHP Manual — SqlStatementResult::__construct
- PHP Manual — ColumnResult::__construct
- PHP Manual — TableSelect::lockExclusive
- PHP Manual — TableSelect::lockShared
- MySQL 8.4 — locking reads, NOWAIT и SKIP LOCKED
- MySQL 8.4 — совместимость shared и exclusive
- MySQL 8.4 — блокировки и выбранный индекс
- Исходник PECL mysql_xdevapi 8.0.30
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


