Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Кудравцев Создание баз данных 2010

.pdf
Скачиваний:
83
Добавлен:
16.08.2013
Размер:
2.65 Mб
Скачать

Далее приводится пример более сложной программы, в которой, помимо соединения с сервером базы данных, выполняется набор SQL-запросов.

#include <stdlib.h>

#include <libpq-fe.h>

void doSQL(PGconn *conn, char *command)

{

PGresult *result ;

printf ("%s\n", command) ;

//Выполнение операторов SQL

//Результат возвращается через специальную структуру PGresult.

//Если возвращается NULL, то не хватает памяти для новой

//структуры.

result=PQexec(conn, command) ;

printf("Status is %s\n", PQresStatus(PQresultStatus(result))); printf("Result message %s\n", PQresultErrorMessage(result)) ; printf("#rows affected %s\n", PQcmdTuples(result)) ;

//Возвращает результат выполнения запроса switch (PQresultStatus(result))

{

//Успешное завершение. Команда не возвращает данные

//например CREATE TABLE

case PGRES_COMMAND_OK:

//Доступ к БД не требуется. Пустой запрос. case PGRES_EMPTY_QUERY:

//Успешное завершение. Запрос вернул 0 и более строк case PGRES_TUPLES_OK:

{

int r, n;

int nrows=PQntuples(result);

//Поля результата пронумерованы с нуля!

int nfields=PQnfields(result);

111

printf("Number of rows returned = %d\n", nrows) ; printf("Number of fields returned = %d\n", nfields) ; for (r=0; r<nrows; r++)

{

for (n=0; n<nfields; n++)

{

if (PQgetisnull(result, r, n)) printf(" %s is NULL, ",

PQfname(result, n));

else

printf(" %s = %s(%d),", PQfname(result, n), PQgetvalue(result, r, n), PQgetlength(result, r, n)) ;

}

printf("\n") ;

}

}

}

PQclear(result);

}

int main(int argc, char** argv)

{

char *conninfo="dbname=test" ; PGresult *result;

PGconn *conn=PQconnectdb(conninfo);

if ( PQstatus(conn) == CONNECTION_OK )

{

printf ("Connection made: DB=%s User=%s Pass=%s Host=%s Port=%s Options=%s \n", Qdb(conn), PQuser(conn), PQpass(conn), PQhost(conn),

PQport(conn), PQoptions(conn) ) ;

doSQL (conn, "CREATE TABLE number(value INTEGER,

112

name VARCHAR)") ;

doSQL(conn, "INSERT INTO number values(48, 'Petr')") ; doSQL(conn, "INSERT INTO number values(48, 'Marta')") ; doSQL(conn, "INSERT INTO number values(24, 'Demos')") ; doSQL(conn, "INSERT INTO number values(20, 'Iren')") ; doSQL(conn, "UPDATE number SET name='Sara' WHERE

value=20") ;

doSQL(conn, "DELETE FROM number WHERE value=20");

doSQL(conn, "SELECT * FROM number WHERE value=48");

}

else

{

printf ("Connection failed: %s", PQerrorMessage(conn)) ;

}

PQfinish(conn);

return EXIT_SUCCESS;

}

В данном примере используется функция doSQL(…), основной задачей которой является выполнение SQL-запросов для установленного соединения путем вызова функции PQexec(…). В основной функции main происходит вызов функции для установления соединения с базой данных, а затем (если соединение успешно установлено) вызов функций вида:

doSQL(conn, "SELECT * FROM number WHERE value=48");

в которых в виде текстовых строк передаются SQL-запросы.

Транзакции

Транзакция – это набор операций, который выполняется (или не выполняется) как один логический блок [6, 8]. Операции по изменению содержимого базы данных (вставка, удаление, обновление), входящие в логический блок, должны или произойти все вместе, или ни одна из них. Применение транзакций позволяет обеспечивать определенный уровень целостности и восстанавливаемости данных. Для восстановления данных при ошибках или отказах системы используется журнал транзакций.

113

Типичным примером, иллюстрирующим работу транзакций, является перевод денег с одного счета на другой в какой-либо базе данных финансовой организации (банка). В упрощенном виде данная транзакция состоит из двух операций изменения двух кортежей одной таблицы. Один кортеж относится к счету, с которого списывается заданная сумма, а на другой эта сумма начисляется. Если между операциями списания и начисления произойдет сбой системы, то целостность базы данных будет нарушена (баланс первого счета уменьшится, а второго останется прежним). Следовательно, эти две операции должны выполняться как один логический блок, т.е. должны быть выполнены обе или не выполнены вообще.

Каждая транзакция в базе данных изолирована от всех остальных транзакций, происходящих в базе данных в то же самое время. В идеале каждая транзакция должна вести себя так, как будто ей предоставлен полный доступ к базе данных. Однако для высокой производительности часто приходится идти на компромисс. Допустимая для использования транзакция должна обладать ACIDсвойствами. ACID — аббревиатура от Atomicity (атомарность), Consistency (согласованность), Isolation (изолированность), Durability (устойчивость). Рассмотрим каждое из этих требований подробнее.

Термин Atomicity (атомарность) означает, что транзакция должна выполняться как элементарная (атомарная) операция. Чтобы транзакция считалась успешно выполненной, должен быть выполнен каждый шаг этой транзакции. При неудачном выполнении хотя бы одного из шагов вся транзакция считается неуспешной, и происходит отмена всех операций, произведенных с начала транзакции.

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

Термин Durability (устойчивость) означает, что после того как транзакция фиксирована, влияние этой транзакции в базе данных

114

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

Термин Isolation (изолированность) означает, что каждая транзакция выполняется так, как если бы она была единственной в системе, т.е. ее выполнение изолируется от другой параллельно выполняемой транзакции. Модификации данных, вносимые одной транзакцией, не зависят от модификаций данных вносимых другой параллельной транзакцией до тех пор, пока эти модификации не будут зафиксированы. Добиться полной изоляции без снижения производительности работы системы практически невозможно. Поэтому стандарт ANSI/ISO SQL определяет четыре уровня изолированности. Уровень изолированности определяет степень изолированности одной транзакции от другой, т.е. определяет уровень, при котором в транзакции допускаются несогласованные данные. Чем выше уровень, тем выше согласованность (непротиворечивость) данных, но при этом может снижаться количество параллельно выполняемых транзакций и тем самым снижаться общая производительность сервера базы данных. Рассмотрим четыре уровня изолированности:

Read uncommitted (чтение незафиксированных данных). Самый низкий уровень. Транзакции практически не изолированы. Запрещается читать только физически поврежденные данные.

Read committed (чтение фиксированных данных). Разрешается читать только фиксированные данные. Наиболее используемый уровень.

Repeatable read (повторяемость чтения). Повторное чтение одной и той же строки в транзакции дает одинаковый результат.

Serializable (упорядочиваемость, сериализация). Самый высокий уровень. Транзакции полностью изолируются друг от друга. Результат параллельного выполнения транзакций совпадает с результатом последовательного выполнения тех же транзакций.

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

1. Чтение нефиксированных данных (Dirty read — неаккуратное чтение) происходит тогда, когда одна транзакция модифициру-

115

ет данные, а другая читает модифицированные, но еще не зафиксированные данные.

2.Неповторяемое чтение (Nonrepeatable read) происходит тогда, когда одна транзакция производит чтение одной строки данных более одного раза, а между чтениями другая транзакция вносит изменения в эту строку и фиксирует эти изменения.

3.Фантомное чтение (Phantom read) происходит тогда, когда одна транзакция пытается прочитать строку, которой не существует в начале транзакции, но вставляется второй транзакцией, прежде чем закончится первая.

В зависимости от уровня изолированности допускаются те или иные типы поведения. Результаты представлены в табл. 4.6.

Таблица 4.6 Поведение транзакций при разных уровнях изолированности

Уровни

Поведение параллельных транзакций

изолированности

 

 

 

Dirty read

Nonrepeatable read

Phantom read

 

Read uncommitted

Да

Да

Да

 

 

 

 

Read committed

Нет

Да

Да

 

 

 

 

Repeatable read

Нет

Нет

Да

Serializable

Нет

Нет

Нет

 

 

 

 

Обычно, по умолчанию, в качестве уровня изолированности используется Read committed (чтение фиксированных данных), при котором возможно неповторяемое и фантомное чтение. Переход на более высокие уровни приводит к тому, что сервер базы данных налагает блокировки на более длительные периоды времени, и это приводит к уменьшению производительности работы сервера.

Для задания уровня изолированности достаточно выполнить команду (пример для PostgreSQL)

SET TRANSACTION ISOLATION LEVEL READ COMMITED

или

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE

СУБД PostgreSQL может работать в режиме явных и неявных транзакций (по умолчанию). В режиме неявных транзакций тран-

116

закция запускается перед выполнением каждого SQL запроса и фиксируется по его завершении.

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

BEGIN WORK.

Режим транзакции включается для всех последующих SQL запросов. Для фиксации транзакции выполняется команда

COMMIT WORK

а для отказа от фиксации (отката) — команда

ROLLBACK WORK.

В PostgreSQL не допускаются вложенные транзакции, хотя в других СУБД они возможны. Не рекомендуется делать транзакции содержащими большое количество SQL операторов из-за взаимоблокировок таблиц. Кроме того, в различных транзакциях рекомендуется придерживаться одного и того же порядка обработки таблиц и строк в таблицах.

Хранимые процедуры

Во многих случаях целесообразно перенести базовые механизмы первичной обработки данных на сторону сервера. Для этих задач современные клиент-серверные СУБД представляют средства в виде так называемых хранимых (на сервере) процедур, которые создаются с помощью специализированных языков типа в PostgreSQL [6] или PL/SQL в Oracle [7] и т.п. Эти языки содержат много общего, хотя имеются и отличия.

Рассмотрим пример создания простейшей хранимой процедуры написанной на языке PL/pgSQL:

CREATE FUNCTION add_one(int4) RETURNS int4 AS ' DECLARE

n1 integer; BEGIN

n1:=1;

117

RETURN $1 + n1 ; END ;

'

LANGUAGE 'plpgsql'

Для создания функции используется оператор CREATE FUNCTION, далее следуют имя функции и типы ее аргументов. После ключевого слова RETURNS указывается тип возвращаемого значения. В PL/pgSQL функция должна возвращать значение, в противном случае возникнет ошибка времени выполнения. Тело функции помещается после ключевого слова AS и ограничивается апострофами «'». В теле функции существуют два блока:

блок объявления переменных (DECLARE);

блок операторов между ключевыми словами BEGIN — END. Все переменные, адресуемые в функции, должны быть объявле-

ны до их использования. Исключением являются только переменные управления циклом. Тип переменной может быть встроенным типом PostgreSQL (integer, float, timestamp и т.д.) или совпадать с типом некоторого столбца из некоторой таблицы. Это задается следующим образом:

v1 table1.field3%TYPE;

v2 table3.field1%TYPE;

В данном примере переменная v1 будет иметь тот же тип, что и столбец field3 из таблицы table1, а переменная v2 будет иметь тот же тип, что и столбец field1 из таблицы table3.

Кроме того, можно объявлять и использовать составные переменные (ROWTYPE и RECORD), которые соответствуют целым строкам некоторой таблицы. Например,

r1 table2% ROWTYPE;

объявляет переменную r1 имеющую поля из таблицы table2. Для доступа к этим полям применяется точечная нотация r1.field1, r1.field2, где field1, field2 столбцы таблицы table2.

Составной тип данных RECORD похож на ROWTYPE, но при его определении не требуется ссылаться на конкретную таблицу. В записи будут содержаться поля, присвоенные переменной во время

118

выполнения хранимой процедуры. Объявление будет выглядеть следующим образом:

r2 RECORD;

Операторы заканчиваются символом «;». Доступ к аргументам функции осуществляется как $1, $2 и т.д. В конце указывается язык, на котором написана хранимая процедура LANGUAGE 'plpgsql'. Для установления языка хранимых процедур для базы данных следует выполнить команду

createlang –U postgres plpgsql mydatabase –L/usr/local/pgsql/lib

Вызов хранимой процедуры выполняется аналогично посылке SQL запроса на сервер. После соединения с базой данных (например, с помощью клиента psql) следует отправить запрос

SELECT add_one(3);

Сервер обработает запрос, отыщет и выполнит процедуру add_one(…) и вернет результат ее выполнения – 4.

Для построения более сложной хранимой процедуры рассмотрим простую базу данных учета товаров на складе, состоящую из двух таблиц item (товар) и stock (склад). Опишем данные таблицы на языке DDL, который подробно будет рассмотрен в гл. 5.

create table item

 

 

(

 

 

item_id

serial,

 

description

varchar(64)

not null,

cost_price

numeric(7,2),

sell_price

numeric(7,2),

CONSTRAINT

item_pk

PRIMARY KEY(item_id)

);

 

 

create table stock

 

 

(

 

 

item_id

integer

not null,

quantity

integer

not null,

 

119

 

CONSTRAINT

stock_pk PRIMARY KEY(item_id)

);

Рассмотрим применение хранимой процедуры для автоматического формирования таблицы заказов (reorders) для тех товаров, количество которых на складе осталось меньше некоторого минимума. Ниже приводится содержимое файла reorders.sql, основное место в котором занимает хранимая процедура reorders(int4):

-- Удалить и создать таблицу reorders DROP TABLE reorders ;

CREATE TABLE reorders

(

item_id integer, message text

);

-- Просмотреть stock, для повторного заказа товаров

CREATE FUNCTION reorders(int4) RETURNS integer AS ' DECLARE

min_stock alias for $1 ; reorder_item integer ; reorder_count integer ; stock_row stock%rowtype ; msg text ;

BEGIN

SELECT COUNT(*) INTO reorder_count

FROM stock

WHERE quantity <= min_stock ;

for stock_row in SELECT * FROM stock WHERE quantity <= min_stock

loop

declare

item_row item%rowtype;

begin

120