Этот материал был создан при поддержке и спонсорстве компании iBase.ru, которая разрабатывает инструменты Firebird SQL для предприятий и предоставляет сервис технической поддержки для Firebird SQL.
Материал выпущен под лицензией Public Documentation License https://www.firebirdsql.org/file/documentation/html/en/licenses/pdl/public-documentation-license.html
Предисловие
Предстоящий релиз PHP 8.4, который должен выйти в конце 2024 года, содержит множество новых функций и улучшений, главные из которых обработчики свойств (property hooks) и поддержка HTML5 а расширении DOM.
В настоящее для тестирования доступна версия PHP 8.4 Beta 4. Скачать готовые сборки для тестирования можно по ссылке 8.4.0beta5
Разработчикам использующим СУБД Firebird будут интересны новые возможности работы с этой СУБД.
Историческая справка
Как известно, начиная с PHP 7.4 основным и единственным драйвером для работы с Firebird являет расширение pdo_firebird. Расширение Firebird/InterBase было перемещено в PECL и не поставляется в составе PHP.
В 2016 году я занимался написанием серии статей по работе с СУБД Firebird из различных языков программирования, которые затем легли в основу Firebird 3.0 Developer’s Guide (Russian) и Firebird 3.0 Developer’s Guide (English). В то время актуальными были PHP 7.0 и Firebird 3.0. В одной из этих статей рассказывалось о том как работать с Firebird из PHP, в ней же проводилось сравнение расширений pdo_firebird и Firebird/InterBase. Там же я отметил, что почти все фреймворки и ORM использовали PDO, поскольку он предоставляет унифицированный интерфейс для работы с различными СУБД. Сам же я практически не использовал pdo_firebird в своих разработках, поскольку в то время оно содержало множество ошибок. Расширение Firebird/InterBase было намного стабильней. Но уже тогда я понимал, что будущее за PDO и надо что-то менять.
Однажды на одном популярном форуме была создана тема, в которой обсуждались ошибке в драйвере pdo_firebird, и один из участников форума набрался смелости и исправил несколько ошибок, сделав соответствующие Pull Requests. В эту тему продолжали публиковать сообщения об ошибках с надеждой, что он их исправит. Тогда я решил присоединиться и тоже попробовать, что-то исправить и мне это удалось. Вот краткий перечень того что было исправлено мной:
-
утечка памяти при работе с типом данных BLOB;
-
поддержка типа данных
BOOLEAN
для входных параметров запросов; -
поддержка 1 sql диалекта;
-
улучшенный разбор именованных параметров в операторе
EXECUTE BLOCK
.
Поэтому, когда команда разработчиков PHP приняла решение об исключении расширения Firebird/InterBase из состава PHP, я не сильно расстроился. К тому времени расширение pdo_firebird уже стало стабильным, и я с легкостью заменил им расширение Firebird/InterBase.
Работа с Firebird в настоящее время
Как известно в Firebird 4.0 был добавлены новые типы данных:
-
DECFLOAT(16)
иDECFLOAT(34)
; -
TIME WITH TIME ZONE
иTIMESTAMP WITH TIME ZONE
; -
INT128
, который лежит в основеNUMERIC(38, x)
иDECIMAL(38, x)
.
Первые два типа данных не вызывают проблем, они используются редко и предназначены в основном для точных научных вычислений.
С TIME WITH TIME ZONE
и TIMESTAMP WITH TIME ZONE
дела обстоят сложнее. Дело в том, что до введения этих типов данных контекстные переменные CURRENT_TIME
и CURRENT_TIMESTAMP
возвращали значения типов TIME WITHOUT TIME ZONE
и TIMESTAMP WITHOUT TIME ZONE
, хотя это противоречило стандарту SQL, теперь же они возвращают TIME WITH TIME ZONE
и TIMESTAMP WITH TIME ZONE
. Для возврата значений без информации о часовых поясах предусмотрены контекстные переменные LOCALTIME
и LOCALTIMESTAMP
. Поэтому те запросы, которые раньше работали корректно возвращают новые типы данных, которые неизвестны драйверу pdo_firebird.
Попробуйте запустить следующий пример:
<?php
namespace MyApp;
use PDO;
use PDOException;
use PDOStatement;
const TS_SQL = <<<'SQL'
SELECT
CURRENT_TIMESTAMP AS TS_TZ,
LOCALTIMESTAMP AS TS,
CURRENT_TIME AS T_TZ,
LOCALTIME AS T
FROM RDB$DATABASE
SQL;
try {
echo 'PHP version: ' . phpversion() . "\n";
$dbh = new PDO('firebird:dbname=inet://localhost/employee;charset=utf8', 'SYSDBA', 'masterkey');
$sth = $dbh->prepare(TS_SQL);
$sth->execute();
$row = $sth->fetch(PDO::FETCH_ASSOC);
echo "Result: \n";
var_dump($row);
echo "\n";
$sth->closeCursor();
}
catch(PDOException $e) {
echo 'Error: ' . $e->getMessage() . "\n";
}
В результате будет выведено следующее:
PHP version: 8.3.11 Result: array(4) { ["TS_TZ"]=> NULL ["TS"]=> string(19) "2024-09-11 15:50:21" ["T_TZ"]=> NULL ["T"]=> string(8) "15:50:21" }
То есть для типов с часовыми поясами всегда возвращается значение null
, поскольку драйвер ничего не знает о новых типах данных.
Ещё хуже обстоят дела при использовании NUMERIC
и DECIMAL
. Дело в том, что при вычислении агрегатных функций SUM
и AVG
, а также при умножении, возвращается результат с наиболее широким типом данных. До Firebird 4.0 для типов NUMERIC
и DECIMAL
самым вместительным был тип BIGINT
, что позволяло считать такие числа с точностью до 18 знаков. Начиная с Firebird 4.0 для NUMERIC
и DECIMAL
наиболее вместительным стал тип INT128
. Это позволяет считать числа с точностью до 38 знаков. Таким образом, если ваши исходные типы данных были NUMERIC(18, x)
, то после этих операций они расширяются до NUMERIC(38, x)
. Но pdo_firebird ничего не знает о новых типах данных.
Попробуйте запустить следующий пример:
<?php
namespace MyApp;
use PDO;
use PDOException;
use PDOStatement;
const TS_SQL = <<<'SQL'
SELECT
SUM(SALARY) AS SUM_SALARY,
CAST(SUM(SALARY) AS NUMERIC(18, 2)) AS SUM_SALARY_2
FROM EMPLOYEE
SQL;
try {
echo 'PHP version: ' . phpversion() . "\n";
$dbh = new PDO('firebird:dbname=inet://localhost/employee;charset=utf8', 'SYSDBA', 'masterkey');
$sth = $dbh->prepare(TS_SQL);
$sth->execute();
$row = $sth->fetch(PDO::FETCH_ASSOC);
echo "Result: \n";
var_dump($row);
echo "\n";
$sth->closeCursor();
}
catch(PDOException $e) {
echo 'Error: ' . $e->getMessage() . "\n";
}
В результате будет выведено следующее:
PHP version: 8.3.11 Result: array(2) { ["SUM_SALARY"]=> string(4) "0.01" ["SUM_SALARY_2"]=> string(11) "16203468.02" }
В данном случае вместо null
вообще выведено непонятное число, что ещё хуже. Следует отметить, что поля SUM_SALARY_2
, которое было искусственно преобразовано к типу NUMERIC(18, 2)
, результат верный.
Как уже говорилось ранее тип DECFLOAT
сам по себе не возникнет в вашей базе данных, но если он всё же потребуется, то будет та же печальная картина.
<?php
namespace MyApp;
use PDO;
use PDOException;
use PDOStatement;
const TS_SQL = <<<'SQL'
SELECT
QUANTIZE(12354.678, 123.54) AS DF,
CAST(QUANTIZE(12354.678, 123.54) AS DOUBLE PRECISION) AS D
FROM RDB$DATABASE
SQL;
try {
echo 'PHP version: ' . phpversion() . "\n";
$dbh = new PDO('firebird:dbname=inet://localhost/employee;charset=utf8', 'SYSDBA', 'masterkey');
$sth = $dbh->prepare(TS_SQL);
$sth->execute();
$row = $sth->fetch(PDO::FETCH_ASSOC);
echo "Result: \n";
var_dump($row);
echo "\n";
$sth->closeCursor();
}
catch(PDOException $e) {
echo 'Error: ' . $e->getMessage() . "\n";
}
В результате будет выведено следующее:
PHP version: 8.3.11 Result: array(2) { ["DF"]=> NULL ["D"]=> string(8) "12354.68" }
Что же делать в данном случае? На самом деле разработчики Firebird 4.0 прекрасно понимали, что новые типы в драйверах появятся не сразу, а потому позаботились о том чтобы эту проблему можно было решить одним из следующих способов:
-
установить параметр
DataTypeCompatibility
в значение '3.0' вfirebird.conf
илиdatabase.conf
; -
установить привязку новых типов данных к одному из тех, что поддерживается драйвером с помощью оператора
SET BIND OF
; -
установить привязку новых типов данных к одному из тех, что поддерживается драйвером с помощью тега
isc_dpb_set_bind
.
Драйвер pdo_firebird не позволяет самостоятельно конструировать буфер параметров соединения, поэтому третий вариант не подходит. Рассмотрим оставшиеся два.
Параметр DataTypeCompatibility
Суть этого параметра проста, он позволяет установить привязку новых типов данных к типам данных, которые существовали в указанной версии Firebird и наиболее близки по свойствам.
На сегодняшний день он может принимать два значения "2.5" и "3.0". Все запросы на сервере будут работать с родными типами данных и только при передачи данных на клиента будут происходить следующие преобразования
Значение параметра | Native type | Legacy type |
---|---|---|
2.5 |
BOOLEAN |
CHAR(5) |
2.5 или 3.0 |
DECFLOAT |
DOUBLE PRECISION |
2.5 или 3.0 |
INT128 |
BIGINT |
2.5 или 3.0 |
TIME WITH TIME ZONE |
TIME WITHOUT TIME ZONE |
2.5 или 3.0 |
TIMESTAMP WITH TIME ZONE |
TIMESTAMP WITHOUT TIME ZONE |
Давайте попробуем установить DataTypeCompatibility = 3.0
и посмотреть на результаты выполнения наших скриптов.
Результаты выполнения первого скрипта index_ts.php
:
PHP version: 8.3.11 Result: array(4) { ["TS_TZ"]=> string(19) "2024-09-11 16:51:24" ["TS"]=> string(19) "2024-09-11 16:51:24" ["T_TZ"]=> string(8) "16:51:24" ["T"]=> string(8) "16:51:24" }
Как видите результат не отличается для типов с часовыми поясами и без.
Результаты выполнения второго скрипта index_n.php
:
PHP version: 8.3.11 Result: array(2) { ["SUM_SALARY"]=> string(11) "16203468.02" ["SUM_SALARY_2"]=> string(11) "16203468.02" }
Тут сумма выводится верно.
Результаты выполнения третьего скрипта index_df.php
:
PHP version: 8.3.11 Result: array(2) { ["DF"]=> string(8) "12354.68" ["D"]=> string(8) "12354.68" }
Этот способ решения проблемы хорош тем что наиболее прост для того чтобы заставить ваши старые проекты работать правильно без каких-либо изменений кода, но он имеет существенные недостатки:
-
не всегда имеется возможность редактировать конфигурационные файлы
firebird.conf
илиdatabases.conf
; -
теряется информация об истинных значениях полей.
Что если завтра вам всё таки потребуется информация о часовом поясе? Что если сумма превышает вместимость NUMERIC(18, x)
? Эту проблему можно решить с помощью SQL оператора SET BIND OF
.
Давайте уберём DataTypeCompatibility = 3.0
из конфигурационного файла и посмотрим на второй способ решения проблемы.
Использование оператора SET BIND OF
Синтаксис оператора SET BIND OF
выглядит следующим образом:
SET BIND OF {<type-from> | TIME ZONE} TO { <type-to> | LEGACY | EXTENDED | NATIVE }
Параметр | Описание |
---|---|
type-from |
Тип данных для которого задаётся правило преобразования. |
type-to |
Тип данных в который следует преобразовать. |
Данный оператор позволяет задать правила описания типов возвращаемых клиенту нестандартным способом — тип type-from автоматически преобразуется к типу type-to.
Если используется неполное определение типа (например CHAR
вместо CHAR(n)
) в левой части SET BIND OF
приведения, то преобразование будет осуществляться для всех CHAR
столбцов, а не только для CHAR(1)
.
Специальный неполный тип TIME ZONE
обозначает все типы, а именно {TIME | TIMESTAMP} WITH TIME ZONE
. Когда неполное определение типа используется в правой части оператора (часть TO
), сервер автоматически определит недостающие детали этого типа на основе исходного столбца.
Изменение связывания любого NUMERIC
и DECIMAL
типа не влияет на соответствующий базовый целочисленный тип. Напротив, изменение привязки целочисленного типа данных также влияет на соответствующие NUMERIC
и DECIMAL
.
Ключевое слово LEGACY
в части TO
используется, когда тип данных, отсутствующий в предыдущей версии Firebird, должен быть представлен способом понятным для старого клиентского программного обеспечения (возможна некоторая потеря данных). Существуют следующие преобразования в LEGACY
типы:
Native тип | Legacy тип |
---|---|
BOOLEAN |
CHAR(5) |
DECFLOAT |
DOUBLE PRECISION |
INT128 |
BIGINT |
TIME WITH TIME ZONE |
TIME WITHOUT TIME ZONE |
TIMESTAMP WITH TIME ZONE |
TIMESTAMP WITHOUT TIME ZONE |
Использование EXTENDED
в части TO
заставляет Firebird использовать расширенную форму типа в части FROM. В настоящее время он работает только для {TIME | TIMESTAMP} WITH TIME ZONE
— они принудительно приводятся к EXTENDED {TIME | TIMESTAMP} WITH TIME ZONE
.
Установка NATIVE
означает, что тип будет использоваться так, как если бы для него не было предыдущих правил преобразования.
Давайте посмотрим применение оператора SET BIND OF
на одном из наших примеров. Для начала приведём все новые типы данных к соответствующим LEGACY типам.
<?php
namespace MyApp;
use PDO;
use PDOException;
use PDOStatement;
const COERCE_SQL = <<<'SQL'
EXECUTE BLOCK
AS
BEGIN
SET BIND OF TIME ZONE TO LEGACY;
SET BIND OF INT128 TO LEGACY;
SET BIND OF DECFLOAT TO LEGACY;
END
SQL;
const TS_SQL = <<<'SQL'
SELECT
CURRENT_TIMESTAMP AS TS_TZ,
LOCALTIMESTAMP AS TS,
CURRENT_TIME AS T_TZ,
LOCALTIME AS T
FROM RDB$DATABASE
SQL;
try {
echo 'PHP version: ' . phpversion() . "\n";
$dbh = new PDO('firebird:dbname=inet://localhost/employee;charset=utf8', 'SYSDBA', 'masterkey');
$dbh->exec(COERCE_SQL);
$sth = $dbh->prepare(TS_SQL);
$sth->execute();
$row = $sth->fetch(PDO::FETCH_ASSOC);
echo "Result: \n";
var_dump($row);
echo "\n";
$sth->closeCursor();
}
catch(PDOException $e) {
echo 'Error: ' . $e->getMessage() . "\n";
}
В результате будет выведено следующее:
PHP version: 8.3.11 Result: array(4) { ["TS_TZ"]=> string(19) "2024-09-11 17:26:33" ["TS"]=> string(19) "2024-09-11 17:26:33" ["T_TZ"]=> string(8) "17:26:33" ["T"]=> string(8) "17:26:33" }
Как видим результат тот же самый, что и при установке DataTypeCompatibility = 3.0
. Для остальных примеров будет тоже самое.
Но оператор SET BIND OF
гораздо более мощный. Мы можем любой тип данных преобразовать в любой другой совместимый тип. Поскольку в языке php не существует родных типов данных для представления типов данных Firebird 4.0, то наиболее логично вывести их строковое представление. Давайте попробуем сделать это.
<?php
namespace MyApp;
use PDO;
use PDOException;
use PDOStatement;
const COERCE_SQL = <<<'SQL'
EXECUTE BLOCK
AS
BEGIN
SET BIND OF TIME ZONE TO VARCHAR;
SET BIND OF INT128 TO VARCHAR;
SET BIND OF DECFLOAT TO VARCHAR;
END
SQL;
const TS_SQL = <<<'SQL'
SELECT
CURRENT_TIMESTAMP AS TS_TZ,
LOCALTIMESTAMP AS TS,
CURRENT_TIME AS T_TZ,
LOCALTIME AS T
FROM RDB$DATABASE
SQL;
try {
echo 'PHP version: ' . phpversion() . "\n";
$dbh = new PDO('firebird:dbname=inet://localhost/employee;charset=utf8', 'SYSDBA', 'masterkey');
$dbh->exec(COERCE_SQL);
$sth = $dbh->prepare(TS_SQL);
$sth->execute();
$row = $sth->fetch(PDO::FETCH_ASSOC);
echo "Result: \n";
var_dump($row);
echo "\n";
$sth->closeCursor();
}
catch(PDOException $e) {
echo 'Error: ' . $e->getMessage() . "\n";
}
В результате будет выведено следующее:
PHP version: 8.3.11 Result: array(4) { ["TS_TZ"]=> string(38) "2024-09-11 17:33:23.9400 Europe/Moscow" ["TS"]=> string(19) "2024-09-11 17:33:23" ["T_TZ"]=> string(27) "17:33:23.0000 Europe/Moscow" ["T"]=> string(8) "17:33:23" }
Отличный результат! Для двух других примеров тоже всё хорошо. Достаточно сразу после соединения выполнить дополнительный запрос для привязки типов данных и вы можете выводить новые типы данных без потерь. Но у этого способа тоже есть недостатки:
-
установку привязки типов данных надо делать при каждом соединении, а это дополнительный запрос к Firebird. Кроме того, если создание вашего соединения с базой данных не централизовано, то придётся менять код вашего приложения в каждом из этих мест.
-
формат вывода даты и времени зависит от текущей локали, поэтому при переносе в другую среду формат вывода может изменится.
Работа с Firebird 4.0 в PHP 8.4
Ну а теперь посмотрим, как работают наши скрипты в PHP 8.4.
<?php
namespace MyApp;
use PDO;
use PDOException;
use PDOStatement;
const TS_SQL = <<<'SQL'
SELECT
CURRENT_TIMESTAMP AS TS_TZ,
LOCALTIMESTAMP AS TS,
CURRENT_TIME AS T_TZ,
LOCALTIME AS T
FROM RDB$DATABASE
SQL;
try {
echo 'PHP version: ' . phpversion() . "\n";
$dbh = new PDO('firebird:dbname=inet://localhost/employee;charset=utf8', 'SYSDBA', 'masterkey');
$sth = $dbh->prepare(TS_SQL);
$sth->execute();
$row = $sth->fetch(PDO::FETCH_ASSOC);
echo "Result: \n";
var_dump($row);
echo "\n";
$sth->closeCursor();
}
catch(PDOException $e) {
echo 'Error: ' . $e->getMessage() . "\n";
}
В результате будет выведено следующее:
PHP version: 8.4.0beta5 Result: array(4) { ["TS_TZ"]=> string(33) "2024-09-11 17:44:52 Europe/Moscow" ["TS"]=> string(19) "2024-09-11 17:44:52" ["T_TZ"]=> string(22) "17:44:52 Europe/Moscow" ["T"]=> string(8) "17:44:52" }
Отлично. Мы ничего не меняли и всё заработало "из коробки".
Теперь посмотрим на пример с суммами.
<?php
namespace MyApp;
use PDO;
use PDOException;
use PDOStatement;
const TS_SQL = <<<'SQL'
SELECT
SUM(SALARY) AS SUM_SALARY,
CAST(SUM(SALARY) AS NUMERIC(18, 2)) AS SUM_SALARY_2
FROM EMPLOYEE
SQL;
try {
echo 'PHP version: ' . phpversion() . "\n";
$dbh = new PDO('firebird:dbname=inet://localhost/employee;charset=utf8', 'SYSDBA', 'masterkey');
$sth = $dbh->prepare(TS_SQL);
$sth->execute();
$row = $sth->fetch(PDO::FETCH_ASSOC);
echo "Result: \n";
var_dump($row);
echo "\n";
$sth->closeCursor();
}
catch(PDOException $e) {
echo 'Error: ' . $e->getMessage() . "\n";
}
В результате будет выведено следующее:
PHP version: 8.4.0beta5 Result: array(2) { ["SUM_SALARY"]=> string(11) "16203468.02" ["SUM_SALARY_2"]=> string(11) "16203468.02" }
Тоже хорошо.
И наконец пример с DECFLOAT
.
<?php
namespace MyApp;
use PDO;
use PDOException;
use PDOStatement;
const TS_SQL = <<<'SQL'
SELECT
QUANTIZE(12354.678, 123.54) AS DF,
CAST(QUANTIZE(12354.678, 123.54) AS DOUBLE PRECISION) AS D
FROM RDB$DATABASE
SQL;
try {
echo 'PHP version: ' . phpversion() . "\n";
$dbh = new PDO('firebird:dbname=inet://localhost/employee;charset=utf8', 'SYSDBA', 'masterkey');
$sth = $dbh->prepare(TS_SQL);
$sth->execute();
$row = $sth->fetch(PDO::FETCH_ASSOC);
echo "Result: \n";
var_dump($row);
echo "\n";
$sth->closeCursor();
}
catch(PDOException $e) {
echo 'Error: ' . $e->getMessage() . "\n";
}
В результате будет выведено следующее:
PHP version: 8.4.0beta5 Result: array(2) { ["DF"]=> string(8) "12354.68" ["D"]=> string(8) "12354.68" }
И здесь всё хорошо.
Таким образом в предстоящий версии PHP 8.4 вы сможете работать со всеми типами данных Firebird 4.0 и Firebird 5.0 без дополнительных "костылей". Рад сообщить вам, что ваш покорный слуга лично приложил свою руку для обеспечения этой возможности. Надеюсь данная статья и описанное нововведение ускорит миграцию на современные версии Firebird, в том числе на самую последнюю версию Firebird 5.0.
Nullable параметры
Работая над поддержкой новых типов данных я вспомнил ещё об одной очень неприятной особенности драйвера pdo_firebird. Сейчас я её продемонстрирую.
Допустим у вас есть таблица, описанная следующим образом:
create sequence gen_employee;
create table employee (
employee_id bigint not null,
name varchar(50) not null,
lastname varchar(50)
);
set term ^;
create trigger tr_employee_bi
for employee before insert
as
begin
if (new.employee_id is null) then
new.employee_id = next value for gen_employee;
end^
set term ;^
Теперь попробуем выполнить следующий скрипт.
<?php
namespace MyApp;
use PDO;
use PDOException;
use PDOStatement;
const TS_SQL = <<<'SQL'
INSERT INTO employee (employee_id, name, lastname)
VALUES (?, ?, ?)
SQL;
try {
echo 'PHP version: ' . phpversion() . "\n";
$dbh = new PDO('firebird:dbname=inet://localhost/test;charset=utf8', 'SYSDBA', 'masterkey');
$sth = $dbh->prepare(TS_SQL);
$sth->execute([null, 'John', 'Smith']);
echo "OK\n";
$cur_stmt = $dbh->prepare('select * from employee');
$cur_stmt->execute();
$rows = $cur_stmt->fetchAll(PDO::FETCH_ASSOC);
var_dump($rows);
$cur_stmt->closeCursor();
}
catch(PDOException $e) {
echo 'Error: ' . $e->getMessage() . "\n";
}
В результате получаем:
PHP version: 8.3.11 Error: SQLSTATE[HY105]: Invalid parameter type: -999 Parameter requires non-null value
Всё дело в том, что драйвер опирается на информацию о параметрах, которую он получает в структуре SQLDA, где первый параметр описан как not nullable, поскольку поле EMPLOYEE_ID
описано как NOT NULL
. Но на самом деле в этот параметр возможно передать значение NULL
, потому что существует триггер tr_employee_bi
, который изменяет значение столбцов таблицы перед вставкой. Вообще nullable флаг полезен для выходных параметров, поскольку позволяет сэкономить на выделении памяти под индикатор значения NULL. но дл входных параметров такое поведение скорее вредит.
Если мы попробуем выполнить следующий запрос, то он будет успешен
INSERT INTO employee (employee_id, name, lastname)
VALUES (null, 'John', 'Smith')
Поскольку я всё равно начал заниматься драйвером pdo_firebird, то решил исправить и эту проблему. Теперь попробуем выполнить тоже самое на PHP 8.4. Результат:
PHP version: 8.4.0beta5 OK array(1) { [0]=> array(3) { ["EMPLOYEE_ID"]=> int(2) ["NAME"]=> string(4) "John" ["LASTNAME"]=> string(5) "Smith" } }
Теперь всё работает как ожидалось.
Режим изолированности транзакций
Как известно, по умолчанию PDO работает в режиме автоматического старта и подтверждения транзакций. В этом случае сразу после соединения с базой данных стартует транзакция по умолчанию. После выполнения любого запроса, транзакция автоматически подтверждается и стартует новая транзакция.
Для ручного управления транзакциями необходимо отключить режим автоматического подтверждения. Это можно сделать при помощи установки аттрибута PDO::ATTR_AUTOCOMMIT
в значение false
, после чего транзакциями можно управлять при помощи методов beginTransaction
, commit
и rollback
. Но в метод beginTransaction
невозможно передать параметры транзакции и изменить режим её изолированности.
Давайте посмотрим с какими параметрами стартует транзакция по умолчанию:
<?php
namespace MyApp;
use PDO;
use PDOException;
use PDOStatement;
const TNX_PROP_SQL = <<<'SQL'
SELECT
TRIM(
CASE
WHEN T.MON$ISOLATION_MODE = 0 THEN 'CONSISTENCY'
WHEN T.MON$ISOLATION_MODE = 1 THEN 'CONCURRENCY'
WHEN T.MON$ISOLATION_MODE = 2 THEN 'READ COMMITTED RECORD VERSION'
WHEN T.MON$ISOLATION_MODE = 3 THEN 'READ COMMITTED NO RECORD VERSION'
WHEN T.MON$ISOLATION_MODE = 4 THEN 'READ COMMITTED READ CONSISTENCY'
END
) AS ISOLATION_MODE,
TRIM(
CASE
WHEN T.MON$LOCK_TIMEOUT = 0 THEN 'NO WAIT'
ELSE 'WAIT'
END
) AS WAIT_MODE,
CASE
WHEN T.MON$LOCK_TIMEOUT > 0 THEN MON$LOCK_TIMEOUT
END AS LOCK_TIMEOUT,
TRIM(
CASE
WHEN T.MON$READ_ONLY = 1 THEN 'READ ONLY'
WHEN T.MON$READ_ONLY = 0 THEN 'READ WRITE'
END
) AS RW_MODE,
(T.MON$AUTO_COMMIT = 1) AS AUTO_COMMIT,
(T.MON$AUTO_UNDO = 1) AS AUTO_UNDO
FROM
MON$TRANSACTIONS T
WHERE T.MON$TRANSACTION_ID = CURRENT_TRANSACTION
SQL;
try {
echo 'PHP version: ' . phpversion() . "\n";
$dbh = new PDO('firebird:dbname=inet://localhost/employee;charset=utf8', 'SYSDBA', 'masterkey');
$sth = $dbh->query(TNX_PROP_SQL);
$row = $sth->fetch(PDO::FETCH_ASSOC);
$sth->closeCursor();
echo "Transaction property: \n";
var_dump($row);
echo "\n";
}
catch(PDOException $e) {
echo 'Error: ' . $e->getMessage() . "\n";
}
В результате будет выведено следующее:
PHP version: 8.4.0beta5 Transaction property: array(6) { ["ISOLATION_MODE"]=> string(31) "READ COMMITTED READ CONSISTENCY" ["WAIT_MODE"]=> string(4) "WAIT" ["LOCK_TIMEOUT"]=> NULL ["RW_MODE"]=> string(9) "READ WRITE" ["AUTO_COMMIT"]=> bool(false) ["AUTO_UNDO"]=> bool(true) }
Для того, чтобы обойти эту проблему стартовать транзакции явно можно с помощью SQL оператора SET TRANSACTION
. Давайте посмотрим как это сделать.
<?php
namespace MyApp;
use PDO;
use PDOException;
use PDOStatement;
const TNX_PROP_SQL = <<<'SQL'
SELECT
TRIM(
CASE
WHEN T.MON$ISOLATION_MODE = 0 THEN 'CONSISTENCY'
WHEN T.MON$ISOLATION_MODE = 1 THEN 'CONCURRENCY'
WHEN T.MON$ISOLATION_MODE = 2 THEN 'READ COMMITTED RECORD VERSION'
WHEN T.MON$ISOLATION_MODE = 3 THEN 'READ COMMITTED NO RECORD VERSION'
WHEN T.MON$ISOLATION_MODE = 4 THEN 'READ COMMITTED READ CONSISTENCY'
END
) AS ISOLATION_MODE,
TRIM(
CASE
WHEN T.MON$LOCK_TIMEOUT = 0 THEN 'NO WAIT'
ELSE 'WAIT'
END
) AS WAIT_MODE,
CASE
WHEN T.MON$LOCK_TIMEOUT > 0 THEN MON$LOCK_TIMEOUT
END AS LOCK_TIMEOUT,
TRIM(
CASE
WHEN T.MON$READ_ONLY = 1 THEN 'READ ONLY'
WHEN T.MON$READ_ONLY = 0 THEN 'READ WRITE'
END
) AS RW_MODE,
(T.MON$AUTO_COMMIT = 1) AS AUTO_COMMIT,
(T.MON$AUTO_UNDO = 1) AS AUTO_UNDO
FROM
MON$TRANSACTIONS T
WHERE T.MON$TRANSACTION_ID = CURRENT_TRANSACTION
SQL;
try {
echo 'PHP version: ' . phpversion() . "\n";
$dbh = new PDO('firebird:dbname=inet://localhost/employee;charset=utf8', 'SYSDBA', 'masterkey');
// start transaction
$dbh->setAttribute(PDO::ATTR_AUTOCOMMIT, false);
$dbh->exec('SET TRANSACTION READ WRITE NO WAIT ISOLATION LEVEL SNAPSHOT');
// execute query
$sth = $dbh->query(TNX_PROP_SQL);
$row = $sth->fetch(PDO::FETCH_ASSOC);
$sth->closeCursor();
echo "Transaction property: \n";
var_dump($row);
echo "\n";
// commit transaction
//$dbh->exec('COMMIT');
$dbh->commit();
$dbh->setAttribute(PDO::ATTR_AUTOCOMMIT, true);
}
catch(PDOException $e) {
echo 'Error: ' . $e->getMessage() . "\n";
}
В результате будет выведено следующее:
PHP version: 8.3.11 Transaction property: array(6) { ["ISOLATION_MODE"]=> string(11) "CONCURRENCY" ["WAIT_MODE"]=> string(7) "NO WAIT" ["LOCK_TIMEOUT"]=> NULL ["RW_MODE"]=> string(10) "READ WRITE" ["AUTO_COMMIT"]=> bool(false) ["AUTO_UNDO"]=> bool(true) } Error: There is no active transaction
Нам удалось изменить уровень изолированности транзакции, но с подтверждением такой транзакции возникли проблемы, причём любым из методов.
Мне не нравится такое поведение, и возможно следующее чем я займусь — исправлю это до выхода финальной версии PHP 8.4. |
Теперь посмотрим какие возможности нам предоставили разработчики PHP 8.4 для изменения уровня изолированности транзакции через аттрибуты соединения.
В PHP 8.4 для драйверов PDO были добавлены дополнительные классы пространстве имён PDO, которые предоставляют дополнительные аттрибуты и методы для специфичного драйвера. Для драйвера Firebird такой класс называется PDO\Firebird
. Он описан следующим образом:
namespace Pdo;
class Firebird extends \PDO
{
// Attributes for date and time formats
public const int ATTR_DATE_FORMAT;
public const int ATTR_TIME_FORMAT;
public const int ATTR_TIMESTAMP_FORMAT;
public const int TRANSACTION_ISOLATION_LEVEL;
// Transaction isolation level
public const int READ_COMMITTED;
public const int REPEATABLE_READ;
public const int SERIALIZABLE;
public const int WRITABLE_TRANSACTION;
public static function getApiVersion(): int;
}
Аттрибут PDO\Firebird::WRITABLE_TRANSACTION
предназначен для установки режима доступа транзакции READ ONLY
или READ WRITE
, а аттрибут PDO\Firebird::TRANSACTION_ISOLATION_LEVEL
для переключения режима изолированности. Константы режимом изолированности соответствуют следующим параметрам транзакции:
-
PDO\Firebird::READ_COMMITTED
-READ COMMITTED RECORD_VERSION
. В Firebird 4.0 и выше если параметр конфигурацииReadConsistency = 1
, режим изолированности будетREAD COMMITTED READ CONSISTENCY
; -
PDO\Firebird::REPEATABLE_READ
-SNAPSHOT
; -
PDO\Firebird::SERIALIZABLE
-SNAPSHOT TABLE STABILITY
.
Давайте посмотрим как их можно использовать.
<?php
namespace MyApp;
use PDO;
use PDOException;
use PDOStatement;
const TNX_PROP_SQL = <<<'SQL'
SELECT
TRIM(
CASE
WHEN T.MON$ISOLATION_MODE = 0 THEN 'CONSISTENCY'
WHEN T.MON$ISOLATION_MODE = 1 THEN 'CONCURRENCY'
WHEN T.MON$ISOLATION_MODE = 2 THEN 'READ COMMITTED RECORD VERSION'
WHEN T.MON$ISOLATION_MODE = 3 THEN 'READ COMMITTED NO RECORD VERSION'
WHEN T.MON$ISOLATION_MODE = 4 THEN 'READ COMMITTED READ CONSISTENCY'
END
) AS ISOLATION_MODE,
TRIM(
CASE
WHEN T.MON$LOCK_TIMEOUT = 0 THEN 'NO WAIT'
ELSE 'WAIT'
END
) AS WAIT_MODE,
CASE
WHEN T.MON$LOCK_TIMEOUT > 0 THEN MON$LOCK_TIMEOUT
END AS LOCK_TIMEOUT,
TRIM(
CASE
WHEN T.MON$READ_ONLY = 1 THEN 'READ ONLY'
WHEN T.MON$READ_ONLY = 0 THEN 'READ WRITE'
END
) AS RW_MODE,
(T.MON$AUTO_COMMIT = 1) AS AUTO_COMMIT,
(T.MON$AUTO_UNDO = 1) AS AUTO_UNDO
FROM
MON$TRANSACTIONS T
WHERE T.MON$TRANSACTION_ID = CURRENT_TRANSACTION
SQL;
try {
echo 'PHP version: ' . phpversion() . "\n";
$dbh = new PDO('firebird:dbname=inet://localhost/employee;charset=utf8', 'SYSDBA', 'masterkey');
$dbh->setAttribute(PDO::ATTR_AUTOCOMMIT, false);
$dbh->setAttribute(PDO\Firebird::TRANSACTION_ISOLATION_LEVEL, PDO\Firebird::REPEATABLE_READ);
$dbh->setAttribute(PDO\Firebird::WRITABLE_TRANSACTION, false);
// start transaction
$dbh->beginTransaction();
// execute query
$sth = $dbh->query(TNX_PROP_SQL);
$row = $sth->fetch(PDO::FETCH_ASSOC);
$sth->closeCursor();
echo "Transaction property: \n";
var_dump($row);
echo "\n";
// commit transaction
$dbh->commit();
$dbh->setAttribute(PDO::ATTR_AUTOCOMMIT, true);
}
catch(PDOException $e) {
echo 'Error: ' . $e->getMessage() . "\n";
}
В результате будет выведено следующее:
PHP version: 8.4.0beta5 Transaction property: array(6) { ["ISOLATION_MODE"]=> string(11) "CONCURRENCY" ["WAIT_MODE"]=> string(4) "WAIT" ["LOCK_TIMEOUT"]=> NULL ["RW_MODE"]=> string(9) "READ ONLY" ["AUTO_COMMIT"]=> bool(false) ["AUTO_UNDO"]=> bool(true) }
Кроме того, эти аттрибуты можно применять прямо при установке соединения и тогда даже транзакция транзакция по умолчанию, которая стартует вместе с соединением изменит свои параметры.
<?php
namespace MyApp;
use PDO;
use PDOException;
use PDOStatement;
const TNX_PROP_SQL = <<<'SQL'
SELECT
TRIM(
CASE
WHEN T.MON$ISOLATION_MODE = 0 THEN 'CONSISTENCY'
WHEN T.MON$ISOLATION_MODE = 1 THEN 'CONCURRENCY'
WHEN T.MON$ISOLATION_MODE = 2 THEN 'READ COMMITTED RECORD VERSION'
WHEN T.MON$ISOLATION_MODE = 3 THEN 'READ COMMITTED NO RECORD VERSION'
WHEN T.MON$ISOLATION_MODE = 4 THEN 'READ COMMITTED READ CONSISTENCY'
END
) AS ISOLATION_MODE,
TRIM(
CASE
WHEN T.MON$LOCK_TIMEOUT = 0 THEN 'NO WAIT'
ELSE 'WAIT'
END
) AS WAIT_MODE,
CASE
WHEN T.MON$LOCK_TIMEOUT > 0 THEN MON$LOCK_TIMEOUT
END AS LOCK_TIMEOUT,
TRIM(
CASE
WHEN T.MON$READ_ONLY = 1 THEN 'READ ONLY'
WHEN T.MON$READ_ONLY = 0 THEN 'READ WRITE'
END
) AS RW_MODE,
(T.MON$AUTO_COMMIT = 1) AS AUTO_COMMIT,
(T.MON$AUTO_UNDO = 1) AS AUTO_UNDO
FROM
MON$TRANSACTIONS T
WHERE T.MON$TRANSACTION_ID = CURRENT_TRANSACTION
SQL;
try {
echo 'PHP version: ' . phpversion() . "\n";
$dbh = new PDO(
'firebird:dbname=inet://localhost/employee;charset=utf8',
'SYSDBA',
'masterkey',
[
PDO\Firebird::WRITABLE_TRANSACTION => false
]
);
$sth = $dbh->query(TNX_PROP_SQL);
$row = $sth->fetch(PDO::FETCH_ASSOC);
$sth->closeCursor();
echo "Transaction property: \n";
var_dump($row);
echo "\n";
}
catch(PDOException $e) {
echo 'Error: ' . $e->getMessage() . "\n";
}
В результате будет выведено следующее:
PHP version: 8.4.0beta5 Transaction property: array(6) { ["ISOLATION_MODE"]=> string(31) "READ COMMITTED READ CONSISTENCY" ["WAIT_MODE"]=> string(4) "WAIT" ["LOCK_TIMEOUT"]=> NULL ["RW_MODE"]=> string(9) "READ ONLY" ["AUTO_COMMIT"]=> bool(false) ["AUTO_UNDO"]=> bool(true) }
Теперь транзакция по умолчанию стартует в READ ONLY
режиме.
Это всё о чем я хотел рассказал про то как работать с современными версиями Firebird в PHP, и какие улучшения для этого произошли в PHP 8.4.