Базы данных не терпят суеты и дилетантов

      Мой коллега-пользователь данного сайта написал в блоге о том, как легко и просто работать с реляционными базами данных (БД). Удивляет однако, что сколь бы ни вещали о том, что это просто, форумы по БД живут, здравствуют и, кажется, по численности задаваемых там элементарных вопросов, занимают первое место. Если всё так просто, то в чём же дело? Кажется, составил запрос к БД и – вуаля! – получил ответ.

       Всё дело в том, что система управления базой данных (СУБД), как сервер, существует сама по себе; БД, как особо организованная файловая система, тоже существует сама по себе, а приложение, которое разрабатывает многострадальный программист, также существует само по себе. Мало того – эти части могут быть расположены в разных местах на диске и даже на разных компьютерах. Нередко удалённых. Поэтому прежде чем разбираться, что такое язык запросов SQL, надо бы уяснить некоторые сущности, без которых  эти самые SQL не работают. Т.е. абсолютно.

 

 

      Для программиста БД существует в двух ипостасях – реальная  БД, как структурированные  данные где-то на каком-то носителе, и их отражение в его программе, которое надо связать с реальной БД. Если такой связи нет, то вся дальнейшая работа бессмысленна. Чтобы установить такую связь, программист должен знать имя компьютера или его IP-адрес (в простейшем случае – «localhost»), путь к реальной БД, её имя, логин и пароль доступа. И ещё т.н. "роль" (ну это пока опустим). Это всё – параметры команды Connect(). Я далее буду рассматривать пример, а чтобы не заморачиваться конкретной IDE, воспользуюсь древней, но всё ещё живой библиотекой классов IBPP, работающей везде, где употребляется отец языков программирования C++ и промышленная СУБД "Firebird". Кроме того, IBPP работает и в линуксных операционных системах (ОС). Библитотеку с исходными файлами можно скачать тут. Файлы библиотеки надо подключить в свой проект с помощью препроцессорной директивы #include. После подключения к проекту файлов, составляющих библиотеку IBPP, дополним класс IBPP новым членом, объявив отражение БД в начале программы или в её заголовке:

IBPP::Database db;

      Прежде всего, создаём отражение БД в своей программе:

try

{

db = IBPP::DatabaseFactory("localhost", DBName.c_str(), "SYSDBA", "masterkey");

}

catch(IBPP::Exception& e)

         {

         // сюда помещаете сообщение о том, что что-то не так

         }

      Предполагается, что БД с именем DBName уже существует. Имя должно состоять из IP-адреса сервера БД, двоеточия, пути и собственно имени БД. Исключения надо отслеживать обязательно -- ЗАКОН ДЛЯ ПРОГРАММИСТА, РАБОТАЮЩЕГО С БД. Далее для краткости отслеживание исключений опускаю, но будем подразумевать, что они всегда есть. Пытаемся соединить отражение БД, т.е. объект db, с физической БД:

db->Connect();

      Если коннект удачен, (исключений нет) – вам повезло. Не повезти может по массе причин. Особенно в Линуксе: файл БД должен принадлежать операционному пользователю firebird (назначается командой chown) и этому пользователю должны быть предоставлены неограниченные права. Кроме того, если БД создавалась в Windows c помощью какой-нибудь экспертной программы для пользователя SYSDBA (это пользователь БД, а не ОС, не путайте, пожалуйста), то по умолчанию пароль доступа к БД будет не masterkey, а masterke, т.е. 8 символов.

      Пока не спешите с SQL. Запрос не будет работать, если он находится за пределами транзакции. Что такое транзакция? О, в Интернете на этот счёт много вранья. То ли неопытные программисты давали определения, то ли любители лишь бы писать. Транзакция – это сеанс связи между отражением БД в вашей программе и физической БД, а вовсе не «операция, которая либо выполняется до конца, либо совсем не выполняется». Транзакция может обрамлять один или несколько запросов. Во время транзакции в специальном буфере сервера СУБД при каждом запросе создаются копии записей в физической БД, к которым обращается ваше отражение БД. Запросы могут выполняться, а могут и не выполняться, например, по причине их некорректности. Сеанс завершается и транзакция становится неактивной либо после подтверждения (так называемого "коммита"), либо после отката изменений -- "роллбэка". Так вот, если проигнорировать «неправильные» запросы и ненароком закоммитить транзакцию – в реальной БД останутся изменения только для «правильных» запросов. В результате деньги на счетах могут размножится или вообще исчезнуть. Смешно, да? Но так бывает, если по небрежности программист проигнорирует исключение и не выполнит откат транзакции. Давайте объявим транзакцию:

IBPP::Transaction tr;

И затем создадим её:

tr = IBPP::TransactionFactory(db);

      Стоп-стоп. Что мы создали? На самом деле мы создали не транзакцию, а пункт управления ею. Это такой объект -- класс или подкласс. Он имеет свои методы. А реальная транзакция проживает на сервере СУБД. Но, как только мы создали транзакцию в программе при подключённой реальной БД, на сервере СУБД создалась реальная транзакция и получила свой номер. Для простоты объект tr будем называть тоже транзакцией. Она пока неактивная. Активизировать её будем тогда, когда это потребуется. Теперь объявим SQL-запрос, как член класса IBPP:

IBPP::Statement st;

      В строку S поместим текст запроса. Подразумеваем, что запрос – выборка из одного столбца с некоторыми условиями. Например,  имеем статистическую таблицу переписи размеров бюста девушек и надо построить гистограмму размеров бюста 1,2,3 для выборки из 1000 человек, тогда текст SQL-запроса будет выглядеть так:

String S = "select first 1000 count(boops_size) from table group by boops_size";

      Обнулим счётчик строк в ответе, создадим тело запроса, стартанём транзакцию, загрузим текст S запроса в его тело, выполним запрос и, если СУБД приняла и выполнила запрос, то начнём опрашивать буфер ответа c целью поместить полученные из БД данные в своё приложение:

int rc=0;

int array[10];

        try

                {

                st = IBPP::StatementFactory(db, tr);

                try

                        {

                        tr->Start();

                        st->Execute( S.c_str() );

                        }

               catch(…)

                        {

                        tr->Rollback();

                         return rc;

                        }

                while( st->Fetch() )

                        {

                        st->Get(1, array[rc]);

                        rc++;

                        }

                tr->Commit();

                }

        catch(IBPP::Exception& e)

                {

                cout << e.ErrorMessage();

                }

        return rc;

      О том, что запрос выполнился, мы можем судить по отсутствию исключений и по ненулёвости количества записей, если, конечно, знаем, что какие-то записи должны быть. Вообщем, в буфере array должны получиться три числа в сумме составляющие 1000.

      В любом случае при неудачах мы должны откатывать транзакцию, иначе пострадает целостность БД. Мы также обязаны думать и безопасности запроса: в данном случае запрос вроде бы безопасен, так как размер ответа вроде бы не может превысить размер буфера array. Не может, если размеры в таблице были проставлены правильно, т.е. 1,2 или 3. Но если злоумышленник, зная логин и пароль, запишет в таблицу количество разных размеров, превышающее 10, то произойдёт переполнение буфера, что крайне нежелательно. Поэтому до нашего запроса следует проверить значения столбца boops_size на валидность. Это делается предварительным запросом. Видите, как всё не просто.

      Транзакции имеют время жизни, уровень изоляции, способы разрешения коллизий. Об этом много написано, ссылок не даю, пользуйтесь поиском, кому интересно. Итак, минимальный джентльменский набор для работы с реляционными БД: отражение БД, транзакция, запрос. А примеры текстов запросов – смотрите в любом справочнике по используемому диалекту SQL.

      Да, совсем забыл. Завершая работу с БД, не забывайте прибирать за собой, иначе наделаете проблем пользователям из-за незакрытых операций. Ну типа так:

bool res;

       try
                {
                db->Disconnect();
               res = true;
                }
        catch (IBPP::Exception& e)
                {

                res = false;
                }

 

 

Версия для печатиВерсия для печати
  • 1
  • 2
  • 3
  • 4
  • 5
Всего голосов: 3
Заметили ошибку? Выделите ее мышкой и нажмите Ctrl+Enter!

Читайте также

 

Комментарии

Исключения надо отслеживать обязательно!

В языке PHP вместо исключений - сообщения об ошибках, константы, которые однажды настраиваются в ini-файле. И ничего там этого try catch не надо. А так, конечно, спасибо за пример с базами, которыми особо не пользуются, да и новичкам тут ничего не понять. 

-1
Аватар пользователя mike

Во-первых, пиэйчпи написан на сиплюсплюс, и сообщения об ошибках берутся из его исключений, а во-вторых, в пиэйчпи тоже есть исключения -- загугли "PHP исключения". laugh

+1
Аватар пользователя mike

Новичкам тут ничего не понятно.

Новички разные бывают.laugh В-третьих, я этот блог собираюсь дополнить. Как только появится время. 

+1

mike пишет:

Во-первых, пиэйчпи написан на сиплюсплюс, и сообщения об ошибках берутся из его исключений, а во-вторых, в пиэйчпи тоже есть исключения -- загугли "PHP исключения". laugh

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

-1
Аватар пользователя mike

Есть и исключения и замыкания...

Видишь, как полезно гуглить.laugh

-1
Аватар пользователя mike

Если посмотреть на исходники пиэйчпи, то практически все сообщения об ошибках при работе с базами данных берутся из отловленных исключений.

+1
Аватар пользователя mike

Нет, нет, всё в порядке, критика полезна.

+1
Аватар пользователя mike

Так как автор блога по ссылке удалил свой пост, я в приведенном примере добавил чуть-чуть конкретики -- текст SQL .