Транзакции. Избежание повторных запросов

Транзакции

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

В реляционных базах данных защита целостности данных реализована в виде блокировки таблиц или транзакций. Блокировка производится способом, напоминающим блокировку файлов при работе с ними с помощью функций РНР.

Транзакция — это группа операций, выполняющаяся как единая операция, которая не прерывается другими клиентами. Другими словами, пока происходит работа с записями таблицы (их обновлением или удалением), никто другой не может получить доступ к этим записям, поскольку MySQL автоматически блокирует кним доступ.

Примечание. Таблицы ISAM, MyISAM, HEAP не поддерживают транзакции. Внастоящий момент их подержка осуществляется только в таблицах BDB и InnoDB.

Транзакции позволяют объединить операции в группу и гарантировать, что все операции внутри группы будут выполнены успешно. Если часть транзакции выполняется со сбоем, результат работы всех операторов транзакции до места сбоя отменяется, приводя базу данных к виду, в котором она была до выполнения транзакции.

Системы, поддерживающие возможности транзакций, часто еще характеризуют как обеспечивающие свойства ACID(Atomic, Consistent, Isolated, Durable — атомарный, целостный, изолированный, длительный).

Понять механизм транзакции можно на примере интернет-магазина. Предположим, что на складе имеется 100 книг. При этом одновременно оформление покупки начинают два покупателя.

Когда начинается оформление покупки книги первым покупателем, происходят два события: регистрируется факт покупки и обновляется количество книг, существующих на складе. Регистрация покупки разрешена, если количество книг, возвращаемое оператором SELECT, больше нуля. Далее из этого значения вычитается количество заказываемых покупателем книг и происходит обновление базы данных склада. Первый раз программное обеспечение по запросу SELECT получает количество книг на складе, равное 100.

Одновременно второй покупатель начинает оформление покупки. Запрос на выборку количества книг для него так же возвращает значение 100.

После регистрации покупки книги первым покупателем число книг, оставшихся на складе, обновляется, и 100 заменяется значением 99.

После регистрации покупки книги вторым покупателем, так же происходит обновление базы данных склада при помощи оператора UPDATE, но поскольку поток, в котором выполняется программа обработки покупки, ничего не знает о параллельно выполняющемся другом запросе, то в результате двух покупок остаток на складе составит 99 книг, вместо 98.

Для преддотвращения такой ситуации выполнение запроса для подсчета остатка на складе при помощи оператора SELECT и обновление данного значения в базе данных при помощи оператора UPDATE должно выполняться как одна транзакция.

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

Если при оплате покупки происходит перевод денежной суммы со счета клиента на счет интернет-магазина, то счет клиента должен уменьшится на сумму sum, а счет интернет-магазина увеличится на ту же сумму:

UPDATE account SET balance = balance - sum WHERE name = 'client';
UPDATE account SET balance = balance + sum WHERE name = 'bookshop';

Если в момент завершения первого запроса и перед самым выполнением второго произойдет аварийный сбой, транзакция окажется незавершенной.

Примечание. Ряд ситуаций можно обработать без привлечения механизма транзакций. Например, блокируя таблицу, с последующей разблокировкой при помощи операторов LOCK TABLES и UNLOCK TABLES. Кроме того, в ситуации с интернет-магазином можно воспользоваться так называемым относительным изменением, используя запрос UPDATE store SET count = count - 1;

По умолчанию СУБД MySQL работает в режиме auto-commit (автовыполнения), т.е. каждый отдельный оператор рассматривается как отдельная трасзакция и выполняется немедленно. Для того чтобы выполнить транзакции явным образом, необходимо отключение режима автовыполнения, которое осуществляется оператором BEGIN. Завершение транзакции отмечается выполнением оператора COMMIT. Если во время операции происходит сбой, ее результаты можно отменить с помощью оператора ROLLBACK.

Примечание. После выполнения одного из двух операторов COMMIT или ROLLBACK режим восстанавливается таким, каким он был до выполнения оператора BEGIN.

Примечание. Существует еще один способ выполнения транзакций — это установка и сброс режима автозаполнения при помощи операторов SET AUTOCOMMIT = 1 и SET AUTOCOMMIT = 0.

Для демонстрации работы транзакций создадим таблицу forums, назначив ей тип InnoDB.

В таблице forums присутствуют следующие поля:

Создание таблицы forums

Создание таблицы forums

Обозначим начало транзакции оператором BEGIN, добавим в таблицу две записи и подтвердим выполнение транзакции оператором COMMIT. После чего произведем выборку значений из таблицы.

Выполнение транзакции

Выполнение транзакции

При необходимости работу транзакции можно отменить посредством оператора ROLLBACK.

Отмена транзакции

Отмена транзакции

Как видно из примера размещенные записи "Форум3" и "Форум4" были удалены из таблицы forums после выполнения оператора ROLLBACK, производящего откат таблицы в исходное положение.

Проверка результатов запроса на значение NULL

Определить, возвращает ли оператор SELECT значение NULL, можно при помощи функции isset().

Примечание. РНР интерпретирует значение NULL как установленное значение.

Рассмотрим пример, проверки насзначение NULL для выборки из таблицы test, содержащей три столбца (first, second, third).

Проверка на равенство значению NULL

<?php
//Устанавливаем соединение с базой данных
require_once("config.php");
//Выполняем SQL-запрос
$ath=mysql_query("SELECT first, second, third FROM test;");
//Проверяем выполнение SQL-запроса
if(!$ath)
{
echo "<p><b>Error: ".mysql_error()."</b></p>";
exit();
}
//Выводим заголовок таблицы
echo "<table>";
while(list($first, $second, $third)=mysql_fetch_row($ath))
{
echo "<tr>";
if(!isset($first)) echo "<td>-</td>";
else echo "<td> $first</td>";
if(!isset($second)) echo "<td>-</td>";
else echo "<td> $second</td>";
if(!isset($third)) echo "<td>-</td>";
else echo "<td> $third</td>";
echo "</tr>";
}
echo "</table>";
?>

В окне веб-браузера это будет выглядеть ТАК.

Примечание. Использование функции empty() в данном случае будет не точным, т.к. она возвращает true и для значения NULL, и для пустой строки ("").

Аналогично функции isset(), на равенство пустому значению можно проверять при помощи оператора идентичности = = = и константы NULL.

Использование оператора = = = и константы NULL

<?php
//Устанавливаем соединение с базой данных
require_once("config.php");
//Выполняем SQL-запрос
$ath=mysql_query("SELECT first, second, third FROM test;");
//Проверяем выполнение SQL-запроса
if(!$ath)
{
echo "<p><b>Error: ".mysql_error()."</b></p>";
exit();
}
//Выводим заголовок таблицы
echo "<table>";
while(list($first, $second, $third)=mysql_fetch_row($ath))
{
echo "<tr>";
if($first===NULL) echo "<td>-</td>";
else echo "<td> $first</td>";
if($second===NULL) echo "<td>-</td>";
else echo "<td> $second</td>";
if($third===NULL) echo "<td>-</td>";
else echo "<td> $third</td>";
echo "</tr>";
}
echo "</table>";
?>

В окне веб-браузера это будет выглядеть ТАК.

Близко к рассмотренной лежит задача определения пустых выборок из таблиц. Рассмотренный ниже скрипт выведет таблицу с единственной строкой. Решить эту проблему можно с помощью функции mysql_num_rows(), возвращающую число строк в результате запроса.

Определение количества строк в результате выполнения запроса

<?php
//Устанавливаем соединение с базой данных
require_once("config.php");
//Выполняем SQL-запрос
$ath=mysql_query("SELECT first, second, third FROM test;");
//Проверяем выполнение SQL-запроса
if(!$ath)
{
echo "<p><b>Error: ".mysql_error()."</b></p>";
exit();
}
//Проверяем, содержит ли результат хоть одну строку
if(mysql_num_rows($ath)>0)
{
//Выводим заголовок таблицы
echo "<table border=1>";
while(list($first, $second, $third)=mysql_fetch_row($ath))
{
echo "<tr>";
if($first===NULL) echo "<td>-</td>";
else echo "<td> $first</td>";
if($second===NULL) echo "<td>-</td>";
else echo "<td> $second</td>";
if($third===NULL) echo "<td>-</td>";
else echo "<td> $third</td>";
echo "</tr>";
}
echo "</table>";
}
?>

В окне веб-браузера это будет выглядеть ТАК.

Избежание повторных запросов

Пусть имеется таблица tb3, содержащая:

При этом поле name является обязательным, а поля email и url — опциональными и в случае отсутствия у посетителя email или url, принимающие значение NULL.

Таблица tb3

Таблица tb3

Одна из задач, стоящая перед администратором клуба, в выводе двух списков: членов клуба имеющих email, и членов клуба, имеющих свои виртуальные странички.

Для примера заполним таблицу.

Пример заполнения таблицы

Вывод списков различных членов клуба (вариант 1)

<?php
//таблица 1
//Устанавливаем соединение с базой данных

require_once("config.php");
//Выполняем SQL-запрос
$ath=mysql_query("SELECT name, email FROM tb3");
//Проверяем выполнение SQL-запроса
if(!$ath)
{
echo "<p><b>Error: ".mysql_error()."</b></p>";
exit();
}
//Проверяем, содержит ли результат хоть одну строку
if(mysql_num_rows($ath)>0)
{
//Выводим заголовок таблицы
echo "<table border=1>";
while(list($name, $email)=mysql_fetch_row($ath))
if($email !==NULL) echo "<tr><td> $name </td><td> $email</td></tr>";
//Выводим завершение таблицы
echo "</table>";
}
//таблица 2
$ath=mysql_query("SELECT name, url FROM tb3");
//Проверяем выполнение SQL-запроса
if(!$ath)
{
echo "<p><b>Error: ".mysql_error()."</b></p>";
exit();
}
//Проверяем, содержит ли результат хоть одну строку
if(mysql_num_rows($ath)>0)
{
//Выводим заголовок таблицы
echo "<table border=1>";
while(list($name, $url)=mysql_fetch_row($ath))
if($email ==NULL) echo "<tr><td> $name </td><td> $url</td></tr>";
//Выводим завершение таблицы
echo "</table>";
}
?>

В окне веб-браузера это будет выглядеть ТАК.

Из примера видно, что два раза подряд выполняется практически одинаковый код. А что если поместить после оператора SELECT первого SQL-запроса, поле url, и использовать дескриптор запроса повторно, без выполнения второго запроса? Однако повторно использовать дескриптор $ath без дополнительного кода не удастся, т.к. внутренний указатель находится на последнем элементе. Решить проблему может функция mysql_data_seek(), способная перемещать внутренний указатель и имеющая следующий синтаксис:

bool mysql_data_seek(resource result-identifier, int row_number)

Функция принимает два параметра, первый из которых result-identifier — дескриптор запроса, возвращаемый функцией mysql_query(), второй — смещение внутреннего указателя относительно первой позиции. Функция возвращает true в случае успеха и false в случае неудачи.

Вывод списков членов клуба (вариант 2)

<?php
//Устанавливаем соединение с базой данных
require_once("config.php");
//Выполняем SQL-запрос
$ath=mysql_query("SELECT name, email FROM tb3");
//Проверяем выполнение SQL-запроса
if(!$ath)
{
echo "<p><b>Error: ".mysql_error()."</b></p>";
exit();
}
//Проверяем, содержит ли результат хоть одну строку
if(mysql_num_rows($ath)>0)
{
//таблица 1
//Выводим заголовок таблицы

echo "<table border=1>";
while(list($name, $email)=mysql_fetch_row($ath))
if($email !==NULL) echo "<tr><td> $name </td><td> $email</td></tr>";
//Выводим завершение таблицы
echo "</table>";
//Устанавливаем внутренний указатель в начало
mysql_data_seek($ath,0);
//таблица 2
//Выводим заголовок таблицы

echo "<table border=1>";
while(list($name, $url)=mysql_fetch_row($ath))
if($email ==NULL) echo "<tr><td> $name </td><td> $url</td></tr>";
//Выводим завершение таблицы
echo "</table>";
}
?>

В окне веб-браузера это будет выглядеть ТАК.






  • Другие |
назадвверхвперед
Rambler's Top100