This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
Программирование баз данных в Delphi Бесплатный курс на http://www.intuit.ru/
Лекция 1. Теория проектирования баз данных. Введение Программирование баз данных – очень большой и серьезный раздел самого что ни на есть практического программирования. На предыдущем курсе «Введение в программирование на Delphi» мы лишь коснулись этой темы, затронули даже не верхушку айсберга под названием Базы Данных, а только его макушку. Между тем, многие программисты большую часть своего времени тратят именно на проектирование баз данных и разработку приложений, работающих с ними. Это неудивительно – в настоящее время каждая государственная организация, каждая фирма или крупная корпорация имеют рабочие места с компьютерами. Имеется масса данных, которые нужно не только сохранить, но и обработать, получить комплексные отчеты. Без баз данных сегодня не обойтись. А завтра они будут еще нужней. Недостаточно просто написать программу, взаимодействующую с БД. Нужно уметь правильно спроектировать эту базу данных. Проектирование баз данных, в общем, является первым шагом разработки приложения. Только когда база данных спроектирована, программист приступает непосредственно к проекту приложения. На этой лекции мы коротко определимся с терминологией БД (для тех, кто пропустил курс «Введение в программирование на Delphi»), затем изучим вопросы проектирования баз данных. Этот курс лекций целиком и полностью посвящен базам данных и разработке приложений, обслуживающих их. На предыдущем курсе мы упоминали, что существуют такие типы баз данных: локальные, файлсерверные, клиент-серверные и распределенные БД. Нам с вами доводилось работать с локальными БД, однако многое осталось «за кадром» - в рамках одного курса просто невозможно дать материал по разнообразным темам, для каждой из которых написано немало книг. Здесь мы продолжим знакомство с локальными БД. Мы познакомимся с различными механизмами доступа к базам данных. Подробно изучим архитектуру клиент-сервер, которая является наиболее востребованной сегодня архитектурой программирования БД. Также рассмотрим механизмы создания распределенных (или многоуровневых) баз данных. Файл-серверные БД имеют очень ограниченные возможности, и в настоящий момент практически не используются, поэтому мы не будем касаться этой темы. Вместо этого гораздо удобней использовать распределенную архитектуру совместно с применением локальных технологий. Обо всем этом и о многом другом мы поговорим на этом курсе. На курсе «Введение в программирование на Delphi» мы пользовались BDE – встроенным механизмом доступа к базам данных. Больше к этим темам мы возвращаться не будем, поэтому если вы пропустили этот курс, то хотя бы бегло просмотрите работу с BDE в лекциях 29-32. Тем не менее, в рамках изучения новых возможностей при работе с базами данных, мы кратко коснемся и BDE, наряду с другими технологиями доступа.
Терминология Базой данных (БД) называется электронное хранилище информации, доступ к которому имеет один или несколько компьютеров. В былые времена под базой данных понимали файл, где данные хранились в табличном виде. Сейчас под базой данных обычно подразумевают папку, в которой хранится один или несколько файлов с таблицами. Эти таблицы, вместе или по отдельности, взаимодействуют с пользовательским приложением. Существуют базы данных, в которых таблицы, индексы и другие служебные данные хранятся в одном файле. К таким БД можно отнести, например, MS Access и InterBase. В этом случае базой данных будет созданный файл. Таблицы, имеющие связи между собой, называют реляционными, и базы данных, в которых имеются взаимосвязанные таблицы, также называются реляционными. Реляционные базы данных в настоящее время наиболее распространены. Часто пользовательские приложения не работают с базами данных напрямую. Имеются специальные программы, называемые Системы Управления Базами Данных (СУБД), которые служат посредниками между базой данных и пользовательским приложением. Такой подход называют архитектурой клиент-сервер, а такие СУБД часто называют серверами баз данных. Иногда еще добавляют букву Р (РСУБД – Реляционная СУБД). 2
Однако не все СУБД предназначены для архитектуры клиент-сервер. Например, программа Access из пакета MS Office – это СУБД, предназначенная для локального или файл-серверного использования. Основой любой БД является таблица. Таблица – это файл определенного формата с данными, представленными в табличном виде, например:
Рис. 1.1. Представление данных в табличном виде. Такая таблица состоит из полей и записей. Поле – столбец таблицы, имеющий название, тип данных и размер. Поле предназначено для описания отдельного атрибута записи. Например, поле «№» имеет целочисленный тип данных, а поле «Фамилия» - строковый. Запись – строка таблицы, описывающая какой-то объект, или иначе, набор атрибутов какого-то объекта. Например, строка под номером 1 описывает человека – Иванова Ивана Ивановича. Первичный ключ – это поле или набор полей, однозначно идентифицирующих запись. В ключевом поле не может быть двух записей с одинаковым значением. Например, поле «Фамилия» нельзя делать ключевым, ведь в таблице могут оказаться однофамильцы. Поле «№» больше подходит для того, чтобы сделать его ключевым. Первичные ключи помогают упорядочить записи и облегчают установку связей между таблицами. Каждая таблица может содержать только один первичный ключ. Индекс – это поле или набор полей, которые часто используются для сортировки или поиска данных. Индексные поля еще называют вторичными ключами. В отличие от первичных ключей, поля для индексов могут содержать как уникальные, так и повторяющие значения. Например, поле «Фамилия» можно сделать индексным – ведь поиск и сортировка записей часто может производиться по этому полю. Индексы могут быть уникальными, то есть, не допускающими совпадений в записях, как первичные ключи, и не уникальными, допускающими такие совпадения. Индексы могут быть как в восходящем порядке (А, Б, …, Я), так и в нисходящем (Я, Ю, …, А). Таблица может иметь множество индексов. Можно все поля сделать индексными, причем даже на каждое поле по два индекса – в восходящем и нисходящем порядке. Однако при этом следует иметь в виду, что база данных в этом случае будет непомерно раздута, и работа с ней значительно замедлится. Другими словами, нужно соблюдать меру, и делать индексными только те поля, по которым действительно часто придется сортировать или фильтровать данные.
Связи (отношения) Реляционные связи (отношения) между таблицами предназначены для разбивки таблиц на самодостаточные части. Рассмотрим пример. Допустим, люди из предыдущей таблицы – студенты. Таблица предназначена для того, чтобы указать, какие экзамены были сданы конкретным студентом. Следовательно, в таблицу требуется добавить поле «Экзамен»:
3
Рис. 1.2. Исправленная таблица Сразу бросается в глаза недостаток такого проектирования: данные из полей «Фамилия», «Имя» и «Отчество» многократно повторяются. Пользователю придется вводить большое количество дублирующих данных, а таблица получается «распухшей», переполненной этими данными. Исправить положение несложно, нужно лишь разбить эту таблицу на две разных таблицы, имеющие релятивную связь:
Рис. 1.3. Реляционная связь между таблицами Как вы можете заметить, избыточность данных исчезла – в одной таблице представлены только данные по студентам, в другой – данные по экзаменам. Связь между таблицами организована по ключевому полю «№» таблицы со студентами. В таблице с экзаменами, вместо полных данных о студенте вписывается только его номер. Студент может сдать сколько угодно много экзаменов, пользователь же просто выберет его из списка, и в таблицу попадет его номер. Такую таблицу легче заполнять, и размер ее будет тоже меньше. При создании связей, как правило, одна таблица называется главной (master), другая – подчиненной (details). В нашем случае главной является таблица со студентами. Таблица со списком сданных экзаменов – подчиненная. Связь, представленная в рисунке 1.3 называется отношением один-ко-многим. То есть, одна запись из одной таблицы может иметь связь с множеством записей из другой таблицы. Однако имеется возможность и того, что запись из первой таблицы не будет иметь никаких связей с другой таблицей – студент может еще не сдать ни одного экзамена. Отношение один-ко-многим встречается наиболее часто. Отношение один-к-одному подразумевает, что одной записи в главной таблице соответствует одна запись в подчиненной таблице. Взгляните на рисунок:
Рис. 1.4. Отношение один-к-одному Данные о студентах, такие как фамилия, группа, могут часто использоваться для самых разных отчетов. Однако домашний адрес и телефон студентов нужны далеко не всегда, поэтому они вынесены 4
в другую таблицу. Если бы мы объединили эти таблицы в одну, то получили бы таблицу с переизбытком данных. Связь один-к-одному используют для того, чтобы отделить главную информацию от второстепенных данных. Отношение многие-ко-многим встречается реже. Такое отношение подразумевает, что одна запись из главной таблицы может иметь связь со многими записями из подчиненной таблицы. А одна запись из подчиненной таблицы может иметь связь со многими записями главной таблицы. Рассмотрим следующий рисунок:
Рис. 1.5. Отношение многие-ко-многим Как видно из рисунка, один покупатель может купить несколько товаров, в то же время как один товар может быть куплен несколькими покупателями. Считается, что базу данных можно спроектировать так, чтобы любая связь многие-ко-многим была бы заменена одной или более связями один-ко-многим. В самом деле, подобные отношения сложно отлаживать. Не все СУБД поддерживают индексацию и контроль над ссылочной целостностью в таких связях, поэтому старайтесь избегать отношений многие-ко-многим.
Ссылочная целостность Ссылочной целостностью называют особый механизм, осуществляемый средствами СУБД или программистом, ответственный за поддержание непротиворечивых данных в связанных релятивными отношениями таблицах. Ссылочная целостность подразумевает, что в таблицах, имеющих релятивные связи, нет ссылок на несуществующие записи. Взгляните на рис. 1.3. Если мы удалим из списка студента Иванова И.И., и при этом не изменим таблицу со сданными экзаменами, ссылочная целостность будет нарушена, в таблице с экзаменами появится «мусор» - данные, на которые не ссылается ни одна запись из таблицы студентов. Ссылочная целостность будет нарушена. Таким образом, если мы удаляем из списка студента Иванова И.И., следует позаботиться о том, чтобы из таблицы со сданными экзаменами также были удалены все записи, на которые ранее ссылалась удаленная запись главной таблицы. Существует несколько видов изменений данных, которые могут привести к нарушению ссылочной целостности: 1. Удаляется запись в родительской таблице, но не удаляются соответствующие связанные записи в дочерней таблице. 2. Изменяется запись в родительской таблице, но не изменяются соответствующие ключи в дочерней таблице. 3. Изменяется ключ в дочерней таблице, но не изменяется значение связанного поля родительской таблицы. Многие СУБД блокируют действия пользователя, которые могут привести к нарушению связей. Нарушение хотя бы одной такой связи делает информацию в БД недостоверной. Если мы, например, удалили Иванова И.И., то теперь номер 1 принадлежит Петрову П.П.. Имеющиеся связи указывают, что он сдал экзамены по математике и физике, но не сдавал экзаменов по русскому языку и литературе. Достоверность данных нарушена. Конечно, в таких случаях в качестве ключа обычно используют счетчик – поле автоинкрементного типа. Если удалить запись со значением 1, то другие записи не изменят своего значения, значение 1 просто невозможно будет присвоить какой-то другой записи, оно будет отсутствовать в таблице. Путаницы в связях не случится, однако все равно подчиненная таблица будет иметь «потерянные» записи, не связанные ни с какой записью главной таблицы. Механизм ссылочной целостности должен запрещать удаление записи в главной таблице до того, как будут удалены все связанные с ней записи в дочерней таблице. 5
Нормализация базы данных Каждый программист обычно по-своему проектирует базу данных для программы, над которой работает. У одних это получается лучше, у других – хуже. Качество спроектированной БД в немалой степени зависит от опыта и интуиции программиста, однако существуют некоторые правила, помогающие улучшить проектируемую БД. Такие правила носят рекомендательный характер, и называются нормализацией базы данных. Процесс нормализации данных заключается в устранении избыточности данных в таблицах. Существует несколько нормальных форм, но для практических целей интерес представляют только первые три нормальные формы. Первая нормальная форма (1НФ) требует, чтобы каждое поле таблицы БД было неделимым (атомарным) и не содержало повторяющихся групп. Неделимость означает, что в таблице не должно быть полей, которые можно разбить на более мелкие поля. Например, если в одном поле мы объединим фамилию студента и группу, в которой он учится, требование неделимости соблюдаться не будет. Первая нормальная форма требует, чтобы мы разбили эти данные по двум полям. Под понятием повторяющиеся группы подразумевают поля, содержащие одинаковые по смыслу значения. Взгляните на рисунок:
Рис. 1.6. Повторяющиеся группы Верно, такую таблицу можно сделать, однако она нарушает правило первой нормальной формы. Поля «Студент 1», «Студент 2» и «Студент 3» содержат одинаковые по смыслу объекты, их требуется поместить в одно поле «Студент», как в рисунке 1.4. Ведь в группе не бывает по три студента, правда? Представляете, как будет выглядеть таблица, содержащая данные на тридцать студентов? Это тридцать одинаковых полей! В приведенном выше рисунке поля описывают студентов в формате «Фамилия И.О.». Однако если оператор будет вводить эти описания в формате «Фамилия Имя Отчество», то нарушается также правило неделимости. В этом случае каждое такое поле следует разбить на три отдельных поля, так как поиск может вестись не только по фамилии, но и по имени или по отчеству. Вторая нормальная форма (2НФ) требует, чтобы таблица удовлетворяла всем требованиям первой нормальной формы, и чтобы любое не ключевое поле однозначно идентифицировалось полным набором ключевых полей. Рассмотрим пример: некоторые студенты посещают спортивные платные секции, и ВУЗ взял на себя оплату этих секций. Взгляните на рисунок:
Рис. 1.7. Нарушение второй нормальной формы В чем здесь нарушение? Ключом этой таблицы служат поля «№ студента» - «Секция». Однако данная таблица также содержит отношение «Секция» - «Плата». Если мы удалим запись студента № 110, то потеряем данные о стоимости секции по скейтборду. А после этого мы не сможем ввести информацию об этой секции, пока в нее не запишется хотя бы один студент. Говорят, что такое отношение подвержено как аномалии удаления, так и аномалии вставки. 6
В соответствие с требованиями второй нормальной формы, каждое не ключевое поле должно однозначно зависеть от ключа. Поле «Плата» в приведенном примере содержит сведения от стоимости данной секции, и ни коим образом не зависит от ключа – номера студента. Таким образом, чтобы удовлетворить требованию второй нормальной формы, данную таблицу следует разбить на две таблицы, каждая из которых зависит от своего ключа:
Рис. 1.8. Правильная вторая нормальная форма Мы получили две таблицы, в каждой из которых не ключевые данные однозначно зависят от своего ключа. Третья нормальная форма (3НФ) требует, чтобы в таблице не имелось транзитивных зависимостей между не ключевыми полями, то есть, чтобы значение любого поля, не входящего в первичный ключ, не зависело от другого поля, также не входящего в первичный ключ. Допустим, в нашей студенческой базе данных есть таблица с расходами на спортивные секции:
Рис. 1.9. Нарушение третьей нормальной формы Как нетрудно заметить, ключевым полем здесь является поле «Секция». Поля «Плата» и «Кол-во студентов» зависят от ключевого поля и не зависят друг от друга. Однако поле «Общая стоимость» зависит от полей «Плата» и «Кол-во студентов», которые не являются ключевыми, следовательно, нарушается правило третьей нормальной формы. Поле «Общая стоимость» в данном примере можно спокойно убрать из таблицы, ведь если потребуется вывести такие данные, нетрудно будет перемножить значения полей «Плата» и «Кол-во студентов», и создать для вывода вычисляемое поле. Таким образом, нормализация данных подразумевает, что вы вначале проектируете свою базу данных: планируете, какие таблицы у вас будут, какие в них будут поля, какого типа и размера. Затем вы приводите каждую таблицу к первой нормальной форме. После этого приводите полученные таблицы ко второй, затем к третьей нормальной форме, после чего можете утверждать, что ваша база данных нормализована. Однако такой подход имеет и недостатки: если вам требуется разработать программный комплекс для крупного предприятия, база данных будет довольно большой. При нормализации данных, вы можете получить сотни взаимосвязанных между собой таблиц. С увеличением числа нормализованных таблиц уменьшается восприятие программистом базы данных в целом, то есть вы можете потерять общее представление вашей базы данных, запутаетесь в связях. Кроме того, поиск в чересчур нормализованных данных может быть замедлен. Отсюда вывод: при работе с данными большого объема ищите компромисс между требованиями нормализации и собственным общим восприятием базы данных. 7
Лекция 2. ADO. Связь с таблицей MS Access. С самого появления технологии баз данных программисты испытывали потребность в механизмах доступа к этим самым данным. Различные компании по-своему пытались предоставить им такую возможность. Например, для работы с таблицами типа dBase была создана Система Управления Базами Данных (СУБД) Clipper. Для времен операционной системы MS-DOS – превосходное решение. Однако Clipper не мог работать ни с какими другими типами таблиц. И чем больше типов данных появлялось, тем острее вставала необходимость разработать универсальный инструмент доступа, который мог бы работать с любым типом данных. Механизм доступа к данным – это программный инструмент, позволяющий получить доступ к базе данных и ее таблицам. Как правило, это драйвер в виде *.dll файлов, который устанавливается на ПК разработчика (и клиента), и который используется программой для связи с БД.
Сравнение BDE и ADO Borland Database Engine (BDE) – первая такая разработка фирмы Borland. Этот механизм доступа к данным позволяет обращаться к локальным и файл-серверным форматам баз данных dBase, FoxPro и Paradox, к различным серверам SQL и ко многим другим источникам данных, доступ которых поддерживался при помощи драйверов ODBC. Например, с помощью BDE можно напрямую работать с табличными файлами MS Excel. Увы, механизм доступа BDE признается устаревшим даже самой компанией Borland. В данный момент многие инструменты Delphi являются кросс - платформенными, то есть, программы с небольшими доработками можно переносить на другие операционные системы. Корпорация Borland выпустила новую среду быстрой разработки программ – Kylix, на которой создаются приложения для операционных систем семейства Linux. Часто говорят, что Kylix – это Delphi для Linux. Так и есть – если вы умеете программировать на Delphi, сумеете и на Kylix. Большинство инструментов Delphi были унаследованы Kylix, но, увы, не BDE. Дальнейшее развитие этого механизма доступа к данным корпорацией Borland прекращено. Тем не менее, хоронить его рано. Многие программисты до сих пор используют данный инструмент в разработке приложений для небольших компаний. Да что там говорить, китайская компания Huawei, разрабатывающая современнейшие электронные АТС как для городских, так и для мобильных телефонов, до сих пор использует BDE для доступа к настройкам и статистическим данным этих АТС! Кроме того, BDE имеет множество простых и удобных возможностей для программиста, таких например, как создание таблиц программно. Удобство работы с BDE трудно переоценить, однако нельзя не сказать и о минусах. Основной минус – распространение приложений. Если ваше приложение использует для доступа к данным компоненты BDE, то и у клиента, который будет пользоваться вашей программой, должен быть установлен BDE. Причем если вы использовали алиасы (псевдонимы базы данных), то настройка на эти же алиасы должна быть и у клиента. Впрочем, создание инсталляционного пакета при помощи стандартной утилиты Install Shield Express снимает эту проблему. Эта утилита позволяет включать настроенный механизм BDE в состав инсталляционного пакета вашей программы. Конечно, за это приходится расплачиваться большими размерами инсталляционного файла. Другой минус касается не только BDE, но и любого другого универсального механизма доступа к данным. Универсальность такого механизма подразумевает сложность его реализации. Программисту предоставляется уже готовый инструмент, с которым удобно работать, однако этот инструмент достаточно «тяжелый» - используя его, вы довольно существенно увеличиваете размеры своего приложения. На предыдущем курсе «Введение в программирование на Delphi» мы затрагивали работу с базами данных посредством BDE. Больше к этим темам мы возвращаться не будем, хотя изредка будем обращаться к BDE, чтобы продемонстрировать те или иные возможности, отсутствующие в других механизмах доступа к данным, или отличающиеся от них. Поэтому если вы пропустили этот курс, то хотя бы бегло просмотрите работу с BDE в лекциях 29-32. ActiveX Data Object (ADO) – это механизм доступа к данным, разработанный корпорацией Microsoft. Если точнее, то ADO – это надстройка над технологией OLE DB, посредством которой 8
можно связываться с различными данными приложений Microsoft. В середине 1990-х годов большое развитие получила технология COM, и корпорация Microsoft в связи с этим объявила о постепенном переходе от старой технологии ODBS к новой OLE DB. Однако технология OLE DB достаточно сложная, использование этой технологии происходит на системном уровне и требует от программиста немало знаний и труда. Кроме того, технология OLE DB очень чувствительна к ошибкам, и «вылетает» при первом удобном случае. Чтобы облегчить программистам жизнь, корпорация Microsoft разработала дополнительный прикладной уровень ADO, который мы будем изучать на этом курсе. По своим возможностям ADO напоминает BDE, хотя конечно, является более мощным инструментом. Компания Borland разработала набор компонентов для доступа к ADO и первоначально назвала его ADOExpress. Однако корпорация Microsoft упорно противится использованию своих обозначений в продуктах сторонних разработчиков, поэтому, начиная с Delphi 6, этот набор компонентов стал именоваться dbGo. Эти компоненты вы можете увидеть на вкладке ADO палитры компонентов. Технология ADO, как и BDE, независима от конкретного сервера БД, имеет поддержку как локальных баз данных различных типов, так и некоторых клиент-серверных БД. Плюсов у этой технологии много. Драйверы, разработанные корпорацией Microsoft для собственных нужд, более надежные, чем драйверы сторонних производителей. Поэтому если вам требуется работать с базами данных MS Access или для архитектуры клиент-сервер использовать MS SQL Server, то использование ADO будет наиболее предпочтительным. Кроме того, имеется плюс и в вопросе распространения программ – во всех современных Windows встроены драйверы ADO. Другими словами, ваша программа будет работать на любом ПК, где установлен Windows. Как ни странно, но основной минус так же заключается в вопросе распространения программ. Корпорация Microsoft поступает довольно хитро. Каждые пару-тройку лет появляются новые версии Windows. Рядовому пользователю обычно нет нужды переходить на свежую ОС, тем более что каждая новая система становится все требовательней к ресурсам ПК. Для того чтобы заставить пользователя перейти на новую версию, корпорация Microsoft обязательно вводит несколько новых стандартов или технологий, несовместимых со старыми. А для старых версий доработок не предусматривается. Вот и приходится бедному пользователю скрепя зубы тратиться на новые версии операционной системы и пакета MS Office. Поэтому при использовании технологии ADO приходится думать о том, какая версия Windows стоит у конечного пользователя, будет ли ваша программа работать у него на ПК. Технология ADO на самом деле является частью Microsoft Data Access Components (MDAC). Компания Microsoft распространяет MDAC как отдельный продукт, к счастью, бесплатный. При этом поддерживается только самая последняя версия MDAC. Например, в состав Delphi 7 входит MDAC 2.6. При распространении собственных программ следует учитывать, что у клиента с большей долей вероятности уже установлена эта самая MDAC, причем самой последней версии. Однако если он пользуется старыми версиями Windows (Win95, 98, ME, NT), то вам потребуется позаботиться об установке MDAC на его компьютер. Если же у него установлена ОС Win2000, WinXP или более новая, то MDAC у него уже есть, и вам беспокоиться не о чем. Еще один серьезный минус ADO в том, что он для подключения к БД использует довольно медлительную технологию COM. Если ваша база данных будет содержать несколько тысяч записей, то скорость работы с таблицами может стать в сотни раз более медленной, чем если бы вы использовали BDE! На современных ПК, имеющих частоту процессора до 2 ГГц и выше, эти замедления могут быть и незаметны, но работа с огромной базой данных на более медленных ПК превратится в сплошное ожидание. Основными компонентами, с которыми нам предстоит работать, являются TADOConnection (для подключения к БД), TADOTable (аналог TTable из BDE), TADOQuery (аналог TQuery из BDE, предназначенный для набора данных, полученных через SQL-запрос) и TADODataSet (для выполнения запросов и получения набора данных).
Создание базы данных MS Access Базы данных MS Access имеют много плюсов, часто программисты предпочитают использовать именно их. Во-первых, база данных MS Access – это один файл. Сколько бы таблиц и индексов она не содержала, все это хранится в одном единственном файле. А значит, такую базу данных легче 9
обслуживать – переносить на новое место, делать резервные копии и так далее. Еще один плюс – имена полей в такой БД можно давать русскими буквами. Лучше всего изучать новый материал на практике. Для примера создадим базу данных для отдела кадров какого-нибудь предприятия. Какие данные на сотрудника нам понадобятся? Прежде всего, фамилия, имя и отчество. Затем укажем пол (мужской или женский), семейное положение (холост или женат/замужем), количество детей. Также понадобятся дата рождения и дата поступления на работу. Стаж работы в годах. Образование. Военнообязанный сотрудник, или нет. Телефоны, по которым можно связаться с сотрудником в любое время. Должность сотрудника и отдел (если есть), в котором он числится. А также его домашний адрес. При этом учитываем, что сотрудник не обязательно является жителем города, где он работает. А вдруг он приехал на заработки из другого города? Или даже из другой страны? Следовательно, придется вводить и страну, и город – вдруг потребуется делать отчет по сотрудникам, прописанным в Украине, например? Вот сколько данных нужно будет вводить для отдела кадров! А ведь мы еще немного упростили их. Стаж работы подразделяется на общий и непрерывный. Эти данные учитываются при расчете больничных листов. Но для учебной базы данных такими деталями можно пренебречь. Итак, первым делом оптимизируем данные, исходя из правил трех нормальных форм. В результате получаем целых четыре таблицы:
Рис. 2.1. Оптимизированные таблицы Главной здесь будет таблица LichData, которая содержит основные данные о сотруднике. Она имеет релятивные связи с другими таблицами. Поле «Ключ» будет автоинкрементным, то есть автоматически будет прибавляться на единицу, гарантируя нам уникальность ключа. В подчиненных таблицах имеется поле «Сотрудник» целого типа, по которому будет обеспечиваться связь. Причем ключевых полей в дочерних таблицах не будет. Главная таблица поддерживает связь один-к-одному с таблицами Doljnost и Adres, и связь один-комногим с таблицей Telephones, ведь у сотрудника наверняка есть и домашний, и рабочий телефоны, а в карманах, возможно, лежит пару мобильников. То есть, один сотрудник может иметь много телефонов. С полями и связями определились, пора создавать базу данных. Для этого загрузите программу MS Access. Если в правой части окна у вас нет панели «Создание файла», то выберите команду «Файл Создать». Затем выберите команду «Новая база данных». Сразу же выйдет запрос с именем этой базы данных. Создайте папку, которая все равно нам понадобится для нового проекта, укажите эту папку, а базу данных назовите ok (отдел кадров). Как только вы нажмете кнопку «Создать», появится окно этой базы данных: 10
Рис. 2.2. Создание БД Сейчас нам потребуется сделать четыре таблицы. Поэтому дважды щелкаем по команде «Создание таблицы в режиме конструктора», и переходим к конструктору. В левой части мы вводим имя поля, причем русскими буквами. В поле «Тип данных выбираем тип», а на вкладке «Общие» делаем настройки поля. Описание поля заполнять необязательно. Итак, создаем поля: 1. «Ключ». Разумеется, имя поля пишем без кавычек. Выбираем тип данных «Счетчик», это автоинкрементный тип данных. В настройках убедитесь, что поле индексированно – Да (Совпадения не допускаются). Правой кнопкой щелкните по этому полю и выберите команду «Ключевое поле». Слева от поля появится значок ключа. 2. «Фамилия». Тип поля текстовый, размер 25 символов. Индексированное поле – Да (Совпадения допускаются). Ведь могут же попасться родственники или однофамильцы! 3. «Имя». Тип поля текстовый, размер 25 символов. Индексированное поле – Да (Совпадения допускаются). 4. «Отчество». Тип поля текстовый, размер 25 символов. Индексы не нужны. 5. «Пол». Текстовый, размер 3 символа. В формате поля укажите «муж/жен», конечно, без кавычек. 6. «Сем_Полож». Логический тип, формат поля «Да/Нет». Здесь мы будем указывать, состоит ли сотрудник (сотрудница) в браке. 7. «Детей». Числовой тип, размер поля Байт (трудно представить, что у кого-то будет более 255 детей!). 8. «Дата_Рожд». Тип поля – Дата/Время. Выберите формат «Краткий форма даты». Затем выберите тот же формат для поля «Маска». При попытке выбора маски выйдет запрос на подтверждение сохранения таблицы. Ответьте утвердительно, а вместо имени таблицы по умолчанию «Таблица 1» впишите «LichData», так будет называться наша первая таблица. После этого появится окно создания маски ввода. Выберите «Краткий формат даты», нажмите «Далее», после чего в окне «Маска ввода» наберите «00.00.0000». В результате мы будем иметь маску в виде «дд.мм.гггг». 9. «Дата_Пост». Все то же самое, что и в №8. 10. «Стаж». Тип поля числовой, размер – байт. 11. «Образование». Текстовый, размер поля 30 символов. Ведь здесь может быть и длинный текст, например «неоконченное высшее техническое». 12. «Военнообязанный». Логический тип, формат «Да/Нет». В результате получим такую картину:
11
Рис. 2.3. Поля таблицы LichData При попытке закрыть это окно, выйдет запрос о сохранении таблицы «LichData». Ответьте утвердительно. Главная таблица сделана, осталось еще три. Снова щелкаем «Создание таблицы в режиме конструктора». Вводим такие поля: 1. «Сотрудник». Тип поля – числовой, размер поля – длинное целое. Делать это поле ключевым не нужно, даже после того, как при попытке закрыть таблицу Access предложит вам сделать поле ключевым. 2. «Отдел», Текстовое, 15 символов. 3. «Должность», Текстовое, 20 символов. Закрываем таблицу, даем ей имя «Doljnost», отказываемся от создания ключа. Делаем следующую таблицу. Поля: 1. «Сотрудник». Тип поля – числовой, размер поля – длинное целое. Не ключевое. 2. «Страна». Тип текстовый, размер 15. 3. «Город». Тип текстовый, размер 20. 4. «Дом_Адрес». Тип текстовый, размер 100. Закрываем таблицу, даем имя «Adres», отказываемся от создания ключа. Делаем следующую таблицу. Поля: 1. «Сотрудник». Тип поля – числовой, размер поля – длинное целое. Не ключевое. 2. «Телефон». Тип текстовый, размер 17. Желательно задать маску. Сразу же выйдет запрос о сохранении таблицы, сохраните ее под именем «Telephones». Для этого выбираем маску (дважды щелкаем по ней), в окне нажимаем кнопку «Список». Настраиваем маску, как на рисунке:
12
Рис. 2.4. Маска для телефона 3. «Примечание». Тип текстовый, размер 10. Формат «Рабочий/Домашний/Мобильный». Закрываем таблицу «Telephones», отказываясь от создания ключевого поля. Все, база данных готова. Программу MS Access можно закрыть, больше она не понадобится. Пока база данных еще пустая, желательно сделать резервную копию файла ok.mdb, который и является полученной базой данных. Как видите, никаких связей между таблицами мы не делали – проще будет сделать их в проекте программы.
Практика работы с БД MS Access из Delphi Базу данных мы спроектировали, таблицы сделали. Осталась еще половина работы – проект Delphi, работающий с этой базой данных. Загружаем Delphi, делаем новый проект. Главная форма нашей программы будет выглядеть так:
Рис. 2.5. Главная форма 13
Здесь я поместил три обычных панели. Свойству Align верхней панели присвоил значение alTop (весь верх). Затем свойству Align нижней панели присвоил значение alBottom. Затем поместил компонент Splitter с вкладки Additional панели инструментов, и его свойству Align также присвоил alBottom, после чего он прижался к нижней панели. Splitter – это разделитель между панелями. С его помощью пользователь мышью сможет передвигать нижнюю панель, меняя ее размеры. И, наконец, свойству Align средней панели присвоил значение alClient, чтобы она заняла все оставшееся место на форме. Не забудьте очистить свойство Caption всех трех панелей. Далее на верхнюю панель я поместил три компонента RadioButton с вкладки Standard палитры компонентов. В их свойствах Caption я написал, соответственно, «Адрес», «Телефоны» и «Должность». Переключаясь между ними, пользователь сможет выводить в нижнюю, подчиненную сетку DBGrid нужные данные. Свойству Checked первой радиокнопки присвоил значение True, чтобы включить ее. Раздел с переключателями я разделил компонентом Bevel с вкладки Additional палитры компонентов. Его ширину (свойство Width) сделал равным 2 пикселям, превратив его в вертикальную разделительную полосу. Далее я сделал раздел поиска, поместив в него обычные Label, Edit и кнопку BitBtn. Этот раздел понадобится на следующей лекции. В последнем разделе верхней панели находятся еще две кнопки BitBtn. Одна из них предназначена для редактирования текущей записи, другая – для добавления новой. Вторая и третья панели содержат только по одному компоненту DBGrid из вкладки DataControls палитры компонентов, свойствам Align которых присвоено значение alClient. Свойству Name формы присвоено значение fMain, свойство Caption формы имеет текст «Отдел кадров», модуль сохранен под именем Main.pas, а проект в целом называется ok (отдел кадров). Далее в проект добавлен модуль данных (File -> New -> Data Module). Модуль данных – это не визуальный контейнер для размещения на нем не визуальных компонентов. В основном, он предназначен для размещения в нем компонентов подключения к данным (TDataBase, ADOConnection и т.п.), компонентов – наборов данных (TTable/ADOTable, TQuery/ADOQuery, TStoredProc/ADOStoredProc) и компонентов DataSource, которые обеспечивают связь наборов данных и компонентов отображения/редактирования данных. Также модуль данных часто используют и для хранения глобальных переменных, общих функций и процедур, которые должны быть видны по всей программе. Модуль данных не имеет формы, но сохраняется как модуль в файле *.pas. Свойству Name модуля данных мы присвоим имя fDM, а модуль сохраним как DM.pas. Теперь самое интересное. Добавляем в модуль компонент ADOConnection с вкладки ADO палитры компонентов. Этот компонент обеспечит связь других компонентов с базой данных при помощи механизма ADO. Связь обеспечивается свойством компонента ConnectionString. В общем-то, у таких компонентов, как ADOTable тоже есть это свойство, однако, имея четыре таблицы, придется четыре раза устанавливать связь. Проще единожды соединиться компонентом ADOConnection и использовать его для связи других компонентов. Приступим к делу. Щелкните дважды по свойству ConnectionString компонента ADOConnection. Откроется окно подключения компонента к ADO:
Рис. 2.6. Окно подключения к ADO. 14
Здесь мы можем подключиться тремя способами: 1. Использовать для связи созданный ранее link-файл. 2. Вписать в поле «Use Connection String» строку для связи с ADO. 3. Сгенерировать эту строку, нажав кнопку Build. Воспользуемся третьим способом – нажмем кнопку Build. Открывается новое окно, содержащее настройки подключения:
Рис. 2.7. Настройки подключения Вначале нам предлагается выбрать поставщика OLE DB, или иначе, указать нужный для подключения драйвер. Для связи с базой данных MS Access больше всего подходит «Microsoft Jet 4.0 OLE DB Provider». Jet – это название механизма работы с СУБД, встроенного в MS Access. Этот механизм поддерживает как собственные БД MS Access, имеющие расширение *.mdb, так и ODBC. Его и выделяем в списке. Нажимаем на кнопку «Далее», либо переходим к вкладке «Подключение». Здесь нам нужно выбрать или ввести базу данных. Тут есть одно замечание. Если мы выберем базу данных, то есть, нажмем на кнопку с тремя точками, откроем диалог выбора и найдем там наш файл, то база данных будет привязана к указанному адресу. Если вы желаете поместить базу данных в какой-то определенной папке, то так и поступите. Однако если вы поместили файл с базой данных (в нашем случае ok.mdb) там же, где находится программа, и не желаете зависеть от определенной папки (ведь пользователь может переместить вашу программу), то нужно вручную вписать только имя файла с БД, без всякого адреса. В этом случае вы не сможете проверить подключение, нажав на кнопку «Проверить подключение». Ну и не надо, обойдемся без проверки. Укажите только имя файла – ok.mdb (вы ведь уже поместили этот файл в папку с проектом?). Нажмите на кнопку «ОК». Закрываем окно редактора связей, и нам остается открыть подключение. Однако перед этим переведите свойство LoginPrompt компонента ADOConnection в False. Если этого не сделать, то при каждой попытке соединиться с базой данных будет выходить запрос на пользовательское имя и пароль, нам это не нужно, наша база данных без пароля. Теперь свойство Connected переведите в True. Если вам удалось это сделать, и не вышло никаких сообщений об ошибке, то подключение состоялось. 15
Пойдем дальше. Установите в модуль данных четыре компонента ADOTable, по одному на каждую таблицу из нашей базы данных. Компонент ADOTable (также как и TTable из вкладки BDE) предназначен для создания набора данных. Набором данных (НД) называется группа записей, полученных такими компонентами, как TTable/ADOTable, TQuery/ADOQuery, TStoredProc/ADOStoredProc из одной или нескольких таблиц базы данных. Все компоненты наборов данных являются потомками класса TDBDataSet, и имеют много общих свойств, методов и событий. Эти компоненты также называют наборами данных. Табличные компоненты (TTable/ADOTable) являются наборами данных, которые получают из базы данных полную копию одной из таблиц, и предоставляют полученный набор данных визуальным компонентам отображения данных (DBGrid, DBEdit, DBMemo и проч.). Компоненты запросов (TQuery/ADOQuery) для получения набора данных из базы данных используют SQL-запрос. Компоненты позволяют получить из одной или нескольких таблиц только те данные, которые удовлетворяют запросу. Выделите все четыре ADOTable (удерживая клавишу <Shift>), и в их свойстве Connection выберите нашу связь ADOConnection1. Таким образом, все четыре ADOTable мы подключили к базе данных. Выделите первый компонент ADOTable. Переименуйте его свойство Name в TLichData, а в свойстве TableName выберите главную таблицу базы – LichData. Буква «Т» в начале названия компонента укажет нам в дальнейшем, что это таблица. Рядом с компонентом установите компонент DataSource из вкладки Data Access палитры компонентов. Компонент DataSource предназначен для организации связи с наборами данных, и служит посредником между такими компонентами НД, как ADOTable, ADOQuery и между компонентами отображения данных, например, DBGrid, DBEdit и т.п. Свойство Name компонента DataSource переименуйте в DSLichData (DS - DataSource). В свойстве DataSet выберите таблицу TLichData. То же самое нужно проделать еще три раза, подключая аналогичным образом компоненты DataSource к другим таблицам:
Рис. 2.8. Модуль данных с установленными компонентами. Затем свойство Active таблиц переведите в True, открыв их. Для тех, кто пропустил предыдущий курс, напомню, что таблицы можно открывать и закрывать не только в Инспекторе Объектов, но и программно. Как открыть, так и закрыть таблицы можно двумя абсолютно равноценными способами:
Пойдем далее. Перейдите на главную форму. Выберите команду File -> Use Unit и подключите к ней модуль DM. Теперь мы сможем видеть таблицы из главной формы. На вкладке DataControls сосредоточены визуальные (видимые пользователю) компоненты отображения данных, такие как DBGrid (сетка, отображающая все данные НД в виде таблицы, и позволяющая редактировать их), DBEdit (поле редактирования данных, предназначенная для ввода или редактирования одного поля записи, то есть, ячейки таблицы), DBMemo (для редактирования MEMOполей) и т.д. Единственным исключением является компонент DBNavigator. Этот компонент предназначен не для отображения данных, а для перемещения по записям набора данных, для вставки новой записи или удаления старой, для перевода НД в режим редактирования или для подтверждения сделанных изменений в наборе данных. Выделите верхнюю сетку DBGrid, в ее свойстве DataSource выберите fDM.DSLichData. В таком же свойстве нижней сетки выберите fDM.DSAdres. Сетки среагировали, и вы можете видеть названия полей. Разумеется, таблица еще пуста, данных пока нет. Кстати, выделите обе сетки, и установите в True их свойства ReadOnly – только чтение. Таблицы ведь будут связаны, и нам не нужно, чтобы пользователь вводил данные фрагментарно. Мы для этого сделаем отдельную форму, а эти сетки нужны только для просмотра. Теперь нужно между таблицами установить связь. Это требуется не только для того, чтобы в нижней сетке выходили данные только на сотрудника, выделенного в верхней сетке, но и для того, чтобы мы смогли в дальнейшем вводить связанные данные в окне редактора. Снова выделите модуль данных. Щелкните дважды по первой таблице, чтобы открыть редактор полей. Правой кнопкой щелкните по этому редактору и выберите команду Add all fields (добавить все поля). В окне редактора полей появились все поля таблицы:
Рис. 2.9. Редактор полей Редактор полей предназначен для настройки параметров каждого поля, для добавления новых полей или удаления имеющихся. Если в редакторе полей нет ни одного поля, то в компоненте DBGrid будут отображены все поля таблицы, имеющие параметры по умолчанию. Если же мы добавили в редактор полей хотя бы одно поле, то сетка DBGrid его и отобразит. В редакторе мы можем для каждого поля изменить различные параметры, например, ширину колонки, название колонки, видимое это поле или нет, и т.п. Кроме того, редактор полей предоставляет возможность добавлять в набор данных новые поля, например вычисляемые или просматриваемые (lookup). Но эту возможность мы будем рассматривать в других лекциях. Поле «Ключ» у нас автоинкрементное, предназначено для связи с другими таблицами. Пользователю его видеть не обязательно. Выделите его, и в свойстве Visible установите False. Теперь 17
для пользователя оно будет невидимым. Здесь у нас есть два логических поля – «Сем_Полож» и «Военнообязанный». Чтобы True и False выходили на экране так, как нам нужно, свойству DisplayValues первого из этих полей присвойте значение «Женат;Холост» (разумеется, без кавычек), а второго – «Да;Нет». Первым здесь идет значение, которое будет обозначать True, вторым – False. Эти значения разделяются точкой с запятой, пробелы не нужны. Таким же образом добавьте все поля в остальные три таблицы. У них невидимым следует сделать поле «Сотрудник» – этому полю автоматически будет присвоено такое же число, как у поля Ключ соответствующей записи. Логических полей у них нет. Однако для поля «Телефон» таблицы Telephones следует изменить свойство EditMask. Щелкните по нему дважды, открыв редактор маски, и в поле Input Mask введите маску «#(###)-###-##-##». Сохраните ее, нажав кнопку ОК. Для полей типа Дата в этом свойстве (в таблице LichData два таких поля) введите маску «##.##.####». Далее кнопкой перейдите в редактор кода. В нижней части окна вы можете увидеть вкладку Diagram, перейдите на нее. Нам с вами потребуется сделать такие связи:
Рис. 2.10. Связи базы данных Для начала в окно диаграмм нужно добавить наши таблицы. Найдите их в окне дерева объектов Object TreeView. Если у вас это окно закрыто, откройте его клавишами <Shift+Alt+F11> либо командой меню View -> Object TreeView. Ухватитесь в этом окне мышью за название главной таблицы LichData {TLichData} и перетащите ее в окно диаграмм. Таблица вместе с полями отобразится в окне. Если бы ранее мы не добавили все поля в окне редактора полей компонента ADOTable, то здесь мы не увидели бы полей. Точно также перетащите остальные таблицы, как на рисунке. Связи главная – подчиненная делают кнопкой Master / Detail Connector, которую вы можете увидеть в верхней части окна диаграмм (предпоследняя). Нажмите на кнопку, затем подведите указатель мыши к боковой границе главной таблицы, нажмите левую кнопку и, удерживая ее, проведите линию к боковой границе таблицы Adres. Как только вы отпустите кнопку, появится окно связей: 18
Рис. 2.11. Окно связей Здесь в поле Detail Fields нужно выбрать поле, по которому будет осуществляться связь, в нашем случае это поле «Сотрудник». В поле Master Fields выбираем ключевое поле «Ключ». Затем нажимаем кнопку Add и кнопку OK. Связь установлена. При установке связей главный / подчиненный важно начинать вести линию с главной таблицы к подчиненной. Если бы мы сделали иначе, то главной таблицей стала бы TAdres. Такую же связь установите и с остальными таблицами. Просто, не правда ли? Сохраните проект, скомпилируйте его и запустите на выполнение. Если в сетках главного окна вы видите открытые таблицы, то все хорошо. Если нет, возможно, при изменении настроек ваши таблицы закрылись. В таком случае закройте программу (но не проект!), выделите таблицы, и их свойству Active снова присвойте значение True. Таблицы должны появиться в сетках главного окна, даже на этапе проектирования. Пойдем дальше. Теперь нам нужно сделать окно редактора данных. Создайте новую форму (File -> New -> Form). Ее свойство Name переименуйте в fEditor, а при сохранении формы дайте модулю имя Editor. Командой File -> Use Unit подключите к форме модуль данных DM. Теперь нам нужно установить на форму такие компоненты:
19
Рис. 2.12. Окно редактора данных Здесь я поступил следующим образом: установил на форму четыре панели GroupBox с вкладки Standard, на каждую таблицу свой GroupBox. Почему я так поступил, станет понятно позже. Займемся первой таблицей. В свойстве Caption компонента GroupBox впишите «Личные данные», это название отразится в заголовке панели. Далее на эту панель следует установить восемь компонентов DBEdit с вкладки DataControls палитры компонентов, два DBCheckBox для редактирования логических данных, и один компонент DBComboBox для списка. Поясняющие компоненты Label установите и настройте самостоятельно. Немного доработаем компонент DBComboBox. Щелкните дважды по его свойству Items, открыв редактор. В нем введите две строки: муж жен Сохраните текст, нажав кнопку ОК. Теперь пользователь сможет указать пол сотрудника, выбрав нужную строку из списка. Для таблицы Doljnost все еще проще: на панели GroupBox всего два компонента DBEdit и два поясняющих Label. Для таблицы Adres используйте три DBEdit. А вот для таблицы Telephones понадобится один DBEdit, один DBComboBox, сетка DBGrid и кнопка BitBtn. Сетка нужна для контроля введенных телефонов, ведь здесь связь один-ко-многим, и телефонов может быть несколько. В редакторе Items компонента DBComboBox введите три строки: Рабочий Домашний Мобильный 20
Теперь займемся подключением компонентов контроля. Удерживая <Shift>, выделите все компоненты контроля на первой панели (все компоненты, кроме Label). В их свойстве DataSource выберите fDM.DSLichData, подключив компоненты к нужному набору данных (таблице). Снимите общее выделение, и выделите первый DBEdit. В его свойстве DataField выберите поле «Фамилия». Это свойство подключает выбранный компонент к определенному полю таблицы. Таким же образом подключите к соответствующим полям остальные компоненты. Затем подключайте компоненты других таблиц, каждое к своей таблице и к соответствующему полю. Сетка DBGrid подключается к fDM.DSTelephones, и не имеет поля, разумеется. Она отображает все видимые поля таблицы. В правой нижней части для удобства пользователя я установил навигационный компонент DBNavigator с вкладки Data Controls. Этот компонент предназначен для перемещения по записям, включения режима редактирования записи, сохранения или отмены сделанных изменений, добавления новой записи или удаления существующей. В его свойстве DataSource я выбрал fDM.DSLichData, чтобы подключить компонент к главной таблице. Нам нужна от этого компонента только возможность перехода на начало или конец таблицы, на следующую или предыдущую запись. Поэтому раскройте его свойство VisibleButtons (видимость кнопок компонента) и переведите в False все кнопки, кроме nbFirst, nbPrior, nbNext и nbLast. Нажатие на эти кнопки приведет к вызову соответствующих методов компонента ADOTable. Эти методы делают следующее: First – переход на первую запись таблицы. Prior – переход на предыдущую запись. Next – переход на следующую запись. Last – переход на последнюю запись. Когда у DBNavigator останется всего четыре кнопки, эти кнопки окажутся вытянутыми. Уменьшите ширину компонента, чтобы кнопки приняли более привычный вид. Теперь пришло время объяснить, почему я поместил компоненты на панели GroupBox, и почему для каждой таблицы сделал отдельную панель. Если вы прошли предыдущий курс «Введение в программирование на Delphi», то знаете, что измененная запись в таблице сохраняется в трех случаях: 1. Применением метода Post. 2. При переходе на другую запись. 3. При добавлении новой записи. Когда мы, заполнив одну таблицу, перейдем к другой, то в первой таблице запись еще не будет сохранена. Поле «Ключ» у нас автоинкрементное, на него завязаны остальные таблицы. До тех пор, пока мы не сохраним запись, в этом поле не будет никакого значения. Следовательно, данные в других таблицах не смогут привязаться к какой-то записи главной таблицы. Поэтому выделите первый GroupBox, и дважды щелкните по событию onExit на вкладке Events инспектора объектов. Это событие происходит всякий раз, когда пользователь перейдет к другой панели GroupBox, либо к кнопкам, расположенным в нижней части окна. В сгенерированной процедуре впишите код: {Вышли из редактирования LichData} procedure TfEditor.GroupBox1Exit(Sender: TObject); begin if fDM.TLichData.Modified then fDM.TLichData.Post; end;
Свойство Modified компонента ADOTable имеет логический тип – в нем содержится True, если данные были изменены, и False в противном случае. Метод Post этого компонента, как уже упоминалось, сохраняет измененную запись таблицы. При этом в поле «Ключ» попадет присвоенное автоматически значение. Таким образом, введенный код означает, что если запись была изменена, то следует ее сохранить. Сгенерируйте событие onExit для оставшихся панелей GroupBox и таким же образом сохраните изменения записей в соответствующих таблицах. 21
Далее сгенерируйте событие нажатия на кнопку «Добавить» в GroupBox с телефонными данными. Этой кнопкой мы будем добавлять новые записи в таблицу, ведь один сотрудник может иметь более одного телефона. Код в процедуре будет такой: if fDM.TTelephones.Modified then fDM.TTelephones.Post; fDM.TTelephones.Append; DBEdit14.SetFocus;
Вначале мы сохраняем измененные значения, если они были. Затем методом Append мы добавляем в таблицу новую запись. Добавить новую запись можно двумя методами: 1. Append – добавляет новую запись в конец таблицы. 2. Insert – добавляет новую запись в текущее положение курсора. После добавления новой записи таблица уже будет в режиме редактирования, поэтому можно не вызывать метод Edit, который переводит таблицу в этот режим. Далее мы переводит фокус ввода на DBEdit с телефонными номерами, чтобы пользователю не пришлось делать это самому. В процедуре нажатия на кнопку «Сохранить и выйти» код простой: if fDM.TLichData.Modified then fDM.TLichData.Post; if fDM.TDoljnost.Modified then fDM.TDoljnost.Post; if fDM.TAdres.Modified then fDM.TAdres.Post; if fDM.TTelephones.Modified then fDM.TTelephones.Post; Close;
Здесь мы лишь сохраняем изменения во всех таблицах, если они были, и закрываем окно. Напоследок у нас осталась кнопка «Добавить сотрудника». Что мы должны сделать, если пользователь нажмет на эту кнопку? Добавить новую запись в каждую таблицу и перевести курсор в первый DBEdit, в котором редактируется фамилия. Это и делаем: fDM.TLichData.Append; fDM.TDoljnost.Append; fDM.TAdres.Append; fDM.TTelephones.Append; DBEdit1.SetFocus;
С этой формой мы закончили, переходим к главной форме. Не забывайте время от времени сохранять проект. Если вы еще не подключили модуль Editor к главной форме командой File -> Use Unit, то сделайте это сейчас, чтобы можно было вызывать окно редактора из главной формы. Начнем с кнопки «Новый сотрудник». Как и в предыдущем примере, нам потребуется добавить новую запись в каждую таблицу, после чего открыть окно редактора: fDM.TLichData.Append; fDM.TDoljnost.Append; fDM.TAdres.Append; fDM.TTelephones.Append; fEditor.ShowModal;
Сгенерируйте процедуру onClick для кнопки «Редактировать». Тут будет лишь одна строчка кода: fEditor.ShowModal;
В результате откроется окно редактора, и компоненты будут отображать данные текущей записи. Предположим, пользователю будет удобней дважды щелкнуть по записи в верхней сетке DBGrid, чем 22
нажимать кнопку. Поэтому выделите сетку с главной таблицей и сгенерируйте для нее событие onDBLClick. Там введите такую же строчку кода. Блок поиска по фамилии оставим на следующую лекцию и перейдем к программированию радиокнопок. По нашему замыслу, при открытии программы в верхней сетке DBGrid будут отображаться данные из главной таблицы, а в нижней – из таблицы Adres. Также будет выделена радиокнопка с надписью «Адрес». Если пользователю захочется посмотреть должность или телефоны текущего сотрудника, он будет щелкать соответствующую радиокнопку, и эти данные должны быть отображены в нижней DBGrid. Выделите первую радиокнопку с надписью «Адрес» и сгенерируйте для нее событие onClick, которое будет возникать, когда пользователь щелкнет по ней. В процедуре этого события впишите следующий код: if RadioButton1.Checked then DBGrid2.DataSource := fDM.DSAdres;
Здесь мы проверили, включена ли данная радиокнопка. Если да, то мы меняем связь нижней сетки DBGrid и подключаем ее к таблице Adres. Ведь связь сетки с таблицей осуществляется через соответствующий компонент DataSource, а у нас их четыре. Подключаясь то к одному, то к другому DataSource, мы можем программно менять отображенную в сетке таблицу. Для события onClick радиокнопки с надписью «Телефоны» код будет таким: if RadioButton2.Checked then DBGrid2.DataSource := fDM.DSTelephones;
А для события onClick радиокнопки с надписью «Должность», соответственно, следующим:
код будет
if RadioButton3.Checked then DBGrid2.DataSource := fDM.DSDoljnost;
Таким образом, в нижней сетке мы отображаем то одну, то другую подчиненную таблицу, и всякий раз в этих таблицах будут показаны данные текущего сотрудника. Вот и весь код! Сохраните проект и скомпилируйте его. На следующей лекции мы будем изучать методы поиска и фильтрации, поэтому введите в базу данные десятка на два – три сотрудников, чтобы было, что искать.
23
Лекция 3. Поиск, фильтрация и индексация таблиц. Последовательный перебор В программах, работающих с базами данных, часто используют поиск данных. Для чего еще нужны базы данных, как не для этого? Самый простой, но в то же время и самый медленный, «тяжеловесный» поиск, это, пожалуй, последовательный перебор. Вы переходите на первую запись таблицы, создаете цикл, который длится до последней записи, и внутри этого цикла проверяете необходимое условие. Также можно делать и обратный перебор, от последней записи к первой. В таблице 3.1 приведены все свойства и методы наборов данных (TTable/ADOTable, TQuery/ADOQuery), которые могут быть использованы при организации последовательного перебора: Таблица 3.1. Свойства и методы набора данных, которые могут быть задействованы при последовательном переборе Свойства и Описание методы Свойство логического типа. Принимает значение True, если достигнут конец таблицы, Eof или если таблица пуста, и False в противном случае. Свойство логического типа. Принимает значение True, если достигнуто начало Bof таблицы, и False в противном случае. Метод. Делает текущей следующую запись набора данных. Next Метод. Делает текущей предыдущую запись набора данных. Prior Метод. Делает текущей первую запись набора данных. First Метод. Делает текущей последнюю запись набора данных. Last Пример: //перешли на первую запись: fDM.TLichData.First; //делать, пока не конец таблицы: while not fDM.TLichData.Eof do begin if fDM.TLichData['Фамилия'] = 'Иванов' then break; //нашли нужную запись, и вышли из цикла fDM.TLichData.Next; //иначе перешли на следующую запись end; //while
Как видно из примера, мы делаем прямой последовательный перебор от первой записи до последней. Получить или изменить значение нужного поля можно, указав имя поля в квадратных скобках после имени набора данных. Например: Edit1.Text := fDM.TLichData['Фамилия']; //получили значение fDM.TLichData['Фамилия']:= Edit1.Text; //изменили значение
Приведенный пример поиска нужной записи допустим, если в таблице имеется не более сотнидругой записей, а условная проверка достаточно сложна. Но обычно программисты этот способ не используют, или используют только в крайнем случае. Далее рассмотрим другие способы поиска.
Метод Locate Метод Locate ищет первую запись, удовлетворяющую условию поиска. Если запись найдена, метод делает ее текущей и возвращает True. В противном случае метод возвращает False и курсор не меняет положения. Поле, по которому ведется поиск, не обязательно должно быть индексировано. Однако если поле индексировано, то метод ищет запись по индексу, что значительно ускоряет поиск. Поиск может вестись как по одному полю, так и по нескольким полям. Метод имеет три параметра: function Locate (const KeyFields: String; const KeyValues: Variant; Options: TLocateOptions) : Boolean;
24
Параметр KeyFields задает поле или список полей, по которым ведется поиск. Если имеется несколько полей, их разделяют точкой с запятой. Параметр KeyValues является вариантным массивом, в котором задаются критерии поиска. При этом первое значение KeyValues ставится в соответствие с первым полем, указанным в KeyFields. Второе – со вторым, и так далее. Третий параметр Options позволяет задать некоторые опции поиска: loCaseInsensitive – поиск ведется без учета высоты букв, то есть, считаются одинаковыми строки «строка», «Строка» или «СТРОКА». loPartialKey – запись будет удовлетворять условию, если ее часть содержит искомый текст. То есть, если мы ищем «ст», то удовлетворять условию будут «строка», «станция», «стажер» и т.п. Пустой набор [] указывает, что настройки поиска игнорируются. То есть, строка ищется «как есть». Примеры использования метода Locate: Table1.Locate('Фамилия', Edit1.Text, []); Table1.Locate('Фамилия;Имя', VarArrayOf(['Иванов', 'Иван']), [loCaseInsensitive]);
Как видно из примера, если для поиска вы используете одно поле, то значение может передаваться напрямую из компонента Edit. Если же вы используете список полей, то должны передать в метод массив вариантов, в которых содержатся искомые значения, по одному на каждое поле. При установке компонента ADOTable в раздел uses прописывается модуль ADODB, который содержит описания всех свойств, методов и событий компонента. Желательно использовать метод в том модуле, где установлен этот компонент. Рассмотрим применение этого метода на примере. Откройте проект. Перейдите на модуль DM, где у нас хранятся компоненты доступа к базе данных. Процедуру поиска реализуем в этом модуле, а чтобы с ней можно было работать из других форм, опишем ее в разделе public: public { Public declarations } procedure MyLocate(s: String);
Как видите, в процедуру передается параметр – строка. В ней мы будем передавать искомую фамилию. Если курсор находится на описании нашей процедуры, то нажмите , чтобы сгенерировать процедуру автоматически. Процедура будет иметь следующий код: procedure TfDM.MyLocate(s: String); begin TLichData.Locate('Фамилия', s, [loPartialKey]); end;
Таким образом, при нахождении подходящей записи курсор будет перемещаться к ней. На главной форме выделите компонент Edit, предназначенный для поиска по фамилии. Создайте для него событие onChange, которое наступает при изменении текста в поле компонента. В созданной процедуре пропишите вызов поиска: fDM.MyLocate(Edit1.Text);
Сохраните пример, скомпилируйте и опробуйте результаты поиска. Метод Locate рекомендуется использовать везде, где это возможно, поскольку он всегда пытается применить наиболее быстрый поиск. Если поле индексировано, и использование индекса ускорит процесс поиска, Locate использует индекс. Если поле не имеет индекса, Locate все равно ищет данные наиболее быстрым способом. Это делает вашу программу независимой от индексов. 25
Метод Lookup Метод Lookup, в отличие от Locate, не меняет положение курсора в таблице. Вместо этого он возвращает значения некоторых ее полей. Причем в отличие от Locate, этот метод осуществляет поиск лишь на точное соответствие. Такой способ поиска востребован реже, однако в иных случаях этим методом очень удобно пользоваться. Рассмотрим синтаксис этого метода. function Lookup (const KeyFields: String; const KeyValues: Variant; const ResultFields: String) : Variant;
Как вы видите, первые два параметра такие же, как у Locate. А вот третий параметр и возвращаемое значение отличаются. В строке ResultFields через точку с запятой перечисляются поля таблицы, значения которых метод должен вернуть. Возвращаются эти значения в виде вариантного массива. Проблема в том, что вернуться может значение Null, то есть, ничего, или Empty (пустой) и это нужно проверять. Рассмотрим работу метода Lookup на примере нашей программы. Прежде всего, вспомним, как работает тип данных Variant. В переменную типа Variant можно поместить любое значение, в том числе и массив. Этот тип данных обычно используют, когда не известно заранее, данные какого типа нам понадобятся на этапе выполнения программы. Когда переменной типа Variant присвоено значение, имеется возможность проверить тип данных этого значения. Для этого служит функция VarType(): function VarType(const V: Variant): TVarType;
В качестве параметра в функцию передается переменная вариантного типа. Функция возвращает значение типа TVarType. Это значение указывает, какого типа данные содержаться в переменной. Значение может быть varSmallint (короткое целое), varInteger (целое), varCurrency (денежный формат) и так далее. Чтобы увидеть полный список возвращаемых функцией значений, в редакторе кода установите курсор на название функции и нажмите , вызвав контекстный справочник. Нас же в данном примере интересуют всего два значения: varNull (записи нет) и varEmpty (запись пустая). Если в программе мы заранее не проведем проверку на эти значения, то вполне можем вызвать ошибку программы. Если же поиск прошел успешно, то будет возвращен массив вариантных значений, элементы которого начинаются с нуля. Каждый элемент массива будет содержать данные одного из указанных полей. Загрузите проект программы. Для поиска воспользуемся кнопкой с надписью «Найти», расположенной в верхней части главной формы. Идея такова: пользователь вводит в поле Edit1 какую то фамилию и нажимает кнопку «Найти». Событие onClick этой кнопки собирает в строковую переменную значения четырех указанных полей найденной записи. Причем после каждого значения в строку добавляется символ «#13» (переход на новую строку), формируя многострочный отчет. Затем эту строку мы выведем на экран функцией ShowMessage(). Итак, в окне главной формы дважды щелкните по кнопке «Найти», генерируя событие onClick. Полный листинг процедуры приведен ниже: {щелкнули по кнопке Найти} procedure TfMain.BitBtn1Click(Sender: TObject); var myLookup: Variant; //для получения результата s : String; //для отчета begin //получаем результат: myLookup := fDM.TLichData.Lookup('Фамилия', Edit1.Text, 'Фамилия;Имя;Отчество;Образование'); //проверяем, не Null ли это: if VarType(myLookup) = varNull then ShowMessage('Сотрудник с такой фамилией не найден!') else if VarType(myLookup) = varEmpty then ShowMessage('Запись не найдена!')
26
//если это массив, то из его элементов собираем //многострочную строку: else if VarIsArray(myLookup) then begin s := myLookup[0] + #13 + myLookup[1] + #13 + myLookup[2] + #13 + myLookup[3]; //и выводим ее на экран: ShowMessage(s); end; //else if end;
Комментарии достаточно подробны, чтобы вы разобрались с кодом. Сохраните проект, скомпилируйте его и запустите. Опробуйте этот способ поиска.
Фильтрация данных Фильтрацию данных применяют не реже а, пожалуй, даже чаще, чем поиск. Разница в том, что при поиске данных пользователь видит все записи таблицы, при этом курсор либо переходит к искомой записи, либо он получает данные этой записи в виде результата работы функции. При фильтрации дело обстоит иначе. Пользователь в результате видит только те записи, которые удовлетворяют условиям фильтра, остальные записи становятся скрытыми. Конечно, таким образом искать нужные данные проще. Можно указать в условиях фильтра, что требуется вывести всех сотрудников, чья фамилия начинается на «И». Пользователь увидит только их. А можно и по-другому: вывести всех сотрудников, которые поступили на работу в период между 2000 и 2005 годом. Короче говоря, удобство работы пользователя с вашей программой зависит от вашей фантазии. Рассмотрим основные способы фильтрации записей.
Свойство Filter Свойство Filter – наиболее часто используемый способ фильтрации записей, имеет тип String. Вначале программист задает условия фильтрации в этом свойстве, затем присваивает логическому свойству Filtered значение True, после чего таблица будет отфильтрована. Условия фильтрации должны входить в строку, например: fDM.TLichData.Filter := 'Фамилия =''Иванов''';
По правилам синтаксиса, если внутри строки встречается апостроф, его нужно дублировать. Приведенный выше пример в результате содержит условие: Фамилия = 'Иванов'
Применяя это свойство, достаточно сложных условий задать невозможно, но если условия фильтрации просты, то данный способ незаменим. Опробуем фильтрацию записей на примере нашего приложения. Откройте событие onChange компонента Edit, изменим его немного. Закомментируйте или удалите вызов процедуры поиска MyLocate, и впишите следующий код:
Откомпилируйте проект и запустите его на выполнение. При введении только первой буквы фамилии записи уже начинают фильтроваться. К примеру, если мы ввели букву «Л», то остаются записи с фамилиями, начинающимися от буквы «Л» до конца алфавита. Можно также улучшить поиск, если при этом еще отсортировать записи по индексу, но об этом чуть позже. Функция QuotedStr() возвращает переданный ей текст, заключенный в апострофы. Условие фильтра можно было бы описать и так: 27
Сложность заключается в том, что в этом случае приходится считать апострофы. Функция QuotedStr() помогает решить эту проблему.
Событие onFilterRecord Это событие возникает при установке значения True в свойстве Filtered. Применение этого способа имеет большой плюс, и большой минус. Плюс в том что, сгенерировав это событие, программист получает возможность задать гораздо более сложные условия фильтрации. Минус же заключается в том, что проверка заключается перебором всех записей таблицы. Если таблица содержит очень много записей, процесс фильтрации может затянуться. В событие передаются два параметра. Первый параметр – набор данных DataSet. С ним можно обращаться, как с именем фильтруемой таблицы. Второй параметр – логическая переменная Accept. Этой переменной нужно передавать результат условия фильтра. Если условие возвращает False, то запись не принимается, и не будет отображаться. Соответственно, если возвращается True, то запись принимается. Рассмотрим этот способ на примере. Суть примера в следующем: необходимо отфильтровать записи по начальным (или всем) буквам фамилии, вводимым пользователем в поле Edit1. В предыдущем примере, если бы мы ввели букву «И», то вышли бы фамилии, первой буквой которых были бы «И» - «Я». Это не так удобно. Сделаем так, чтобы если пользователь введет букву «И», то останутся только фамилии, начинающиеся на «И». Если пользователь введет еще букву «в», то останутся только фамилии, начинающиеся на «Ив», и так далее. Поочередно вводя начальные буквы, пользователь доберется до нужных фамилий. Для начала подготовим модуль данных. В нем нам потребуется создать глобальную переменную ed, чтобы мы могли передавать в нее текст из компонента Edit1: var fDM: TfDM; ed: String; //текст из Edit1
Этого действия можно было бы избежать, если бы компонент ADOTable, подключенный к таблице LichData, располагался на главной форме. Но поскольку он находится в модуле данных, то и событие onFilterRecord будет сгенерировано в нем. А в этом событии нам нужно будет знать, что в данный момент находится в поле ввода Edit1. Именно для этого и нужна глобальная переменная ed. Далее выделяем TLichData, то есть, компонент ADOTable, подключенный к таблице LichData. На вкладке Events (События) инспектора объектов найдите событие onFilterRecord и дважды щелкните по нему, сгенерировав процедуру. Полный листинг процедуры: {onFilterRecord главной таблицы} procedure TfDM.TLichDataFilterRecord(DataSet: TDataSet; var Accept: Boolean); var s : String; //для значения поля begin //получаем столько начальных букв из поля Фамилия, //сколько букв имеется в переменной ed: s := Copy(DataSet['Фамилия'], 1, Length(ed)); //делаем проверку на совпадение значений: Accept := s = ed; end;
Здесь в переменную s попадает столько начальных букв из поля «Фамилия», сколько букв содержит в данный момент компонент Edit1 на главной форме (эти буквы мы передадим в переменную ed чуть позже). Если текст в переменной s совпадает с текстом из поля Edit1, то переменной Accept присваивается True, и запись принимается. Иначе запись отфильтровывается. Не забудьте сохранить проект. 28
Далее перейдем в главную форму. Нужно удалить весь текст из события onChange компонента Edit1, и вписать новый: {Изменение поиска по фамилии} procedure TfMain.Edit1Change(Sender: TObject); begin //если в поле Edit1 есть хоть одна буква, if Edit1.Text <> '' then begin fDM.TLichData.Filtered := False; //отключаем фильтр DM.ed := Edit1.Text; //передаем в DM новый текст fDM.TLichData.Filtered := True; //включаем фильтр end //если букв нет, фильтрацию отключаем: else fDM.TLichData.Filtered := False; end;
Вот и все. Что же тут у нас происходит? Как только пользователь введет хоть одну букву, срабатывает событие onChange компонента Edit1. Если в Edit1 есть хоть одна буква, то мы вначале отключаем фильтрацию, отменяя прошлый фильтр, если он был. Затем мы передаем в глобальную переменную ed, расположенную в модуле данных, текст из Edit1. Далее снова включаем фильтр. При этом срабатывает событие onFilterRecord нашей таблицы, и в этом событии сравнивается текущее значение переменной ed и записей поля «Фамилия». Сохраните проект, скомпилируйте и запустите программу. Проверьте, как фильтруются записи. Имея воображение, в событии onFilterRecord можно устраивать сколь угодно сложные проверки. Ведь в этом событии можно сравнивать не одно поле, а несколько, причем поля не обязательно должны быть индексированы. Вы можете проверять на совпадение хоть все поля таблицы, и поскольку фильтрация происходит путем перебора записей, то усложнение условных проверок заметно не замедлит этот процесс.
Использование индексов Создание индексных полей обеспечивает сортировку данных по этим полям, что также облегчает поиск данных – ведь найти нужную фамилию или имя проще, если они отсортированы по алфавиту. Причем имеется возможность сортировать записи не только по возрастанию, но и по убыванию, хотя в большинстве руководств по Delphi эта возможность не описывается. При создании в базе данных таблицы LichData мы указали поля «Фамилия» и «Имя», как индексированные. Этим и воспользуемся. Чтобы включить сортировку записей по полю «Фамилия», достаточно указать название поля в свойстве IndexFieldNames таблицы: fDM.TLichData.IndexFieldNames := 'Фамилия';
Если требуется отключить сортировку, этому свойству присваивается пустая строка: fDM.TLichData.IndexFieldNames := '';
Существует еще одна хитрость, о которой мало где можно прочитать. При индексировании таблицы к имени поля можно прибавить строку “ASC”, если мы желаем сортировать в возрастающем порядке (по умолчанию), или “DESC”, если сортируем в убывающем порядке. Сортировка “ASC” используется по умолчанию. Добавим возможность сортировки по фамилии и имени в нашу программу. Для этого на главную форму установим компонент TPopupMenu с вкладки Standard палитры компонентов. Дважды щелкните по компоненту, чтобы открыть редактор меню. Создадим следующие пункты:
29
Сортировать по фамилии Сортировать по имени Не сортировать Обратная сортировка В редакторе меню выделите пункт «Сортировать по фамилии» и измените свойство Name этого пункта на NFam. Пункт «Сортировать по имени» переименуйте в NImya. Пункт «Не сортировать» - в NNet, а пункт «Обратная сортировка» - в NObrat. Вначале создайте обработчик событий для пункта «Не сортировать» (дважды щелкните по пункту). Тут все просто: {Не сортировать} procedure TfMain.NNetClick(Sender: TObject); begin fDM.TLichData.IndexFieldNames := ''; end;
Для обработчика событий пункта «Сортировать по фамилии» код немного сложней: {Сортировать по фамилии} procedure TfMain.NFamClick(Sender: TObject); var stype : String; begin //выбираем направление сортировки: if NObrat.Checked then stype := ' DESC' //обратная сортировка else stype := ' ASC'; //прямая сортировка //сортируем fDM.TLichData.IndexFieldNames := 'Фамилия' + stype; end;
Здесь, в зависимости от состояния свойства Checked пункта «Обратная сортировка» мы присваиваем строковой переменной stype либо значение ‘ ASC’ (прямая сортировка), либо ‘ DESC’ (обратная сортировка). Обратите внимание, что перед строкой имеется пробел, он нужен, чтобы строка не «прилепилась» к названию поля. Далее мы устанавливаем индекс, указывая имя поля и добавляя к нему значение переменной stype. Таким образом, если Checked пункта «Обратная сортировка» имеет значение True (галочка установлена), мы добавляем ‘ DESC’, или ‘ ASC’ в противном случае. В результате имя индексного поля может быть либо «Фамилия ASC», либо «Фамилия DESC». Сортировку по имени кодируем аналогичным образом: {Сортировать по имени} procedure TfMain.NImyaClick(Sender: TObject); var stype : String; begin //выбираем направление сортировки: if NObrat.Checked then stype := ' DESC' else stype := ' ASC'; //сортируем fDM.TLichData.IndexFieldNames := 'Имя' + stype; end;
Нам осталось указать код пункта всплывающего меню «Обратная сортировка». Тут нам нужно не просто установить галочку, если ее не было, но также проверить – есть ли сортировка по какому либо полю? Если таблица отсортирована, требуется ее пересортировать по этому же полю, но уже в обратном порядке. Вот код:
30
{Команда "Обратная сортировка"} procedure TfMain.NObratClick(Sender: TObject); begin //изменяем направление сортировки NObrat.Checked := not NObrat.Checked; //если сортировка по фамилии, пересортируем if Pos('Фамилия',fDM.TLichData.IndexFieldNames)>0 then fMain.NFamClick(Sender); //если сортировка по имени, пересортируем if Pos('Имя',fDM.TLichData.IndexFieldNames)>0 then fMain.NImyaClick(Sender); end;
Как видите, мы использовали функцию Pos(), которая возвратит ноль, если в строке не найдено указанной подстроки, или номер символа, с которого эта подстрока начинается, если она есть. Нам нужно определить, не входит ли в имя индексного поля «Фамилия» или «Имя». Ведь к имени поля добавлена строка ‘ ASC’ или ‘ DESC’, так что прямая проверка if fDM.TLichData.IndexFieldNames = 'Фамилия' then
результата не даст, в любом случае результатом было бы False. Ну а для пересортировки мы вызываем соответствующий пункт меню, чтобы не писать код сортировки еще раз, например: fMain.NFamClick(Sender);
Следует заметить, что при большом количестве записей в таблице смена индексного поля будет несколько замедлять работу приложения. Тем не менее, индексация таблицы – очень удобный и часто применяемый способ организации вывода записей. В свойстве PopupMenu верхней сетки DBGrid1 выберите созданное только что всплывающее меню, чтобы оно открывалось только над этой сеткой, сохраните проект, скомпилируйте его и опробуйте сортировку данных. Напоследок заметим, что мы имеем возможность применить одновременно и фильтрацию записей, и их индексацию. Это позволяет нам создать достаточно мощный и удобный для пользователя механизм поиска записей в нашей программе.
31
Лекция 4. Наборы данных. Основные свойства, методы и события До сих пор мы работали с таблицами с помощью компонента TADOTable. На самом деле мы работали не с самими таблицами, а с Набором данных (DataSet). Набор данных – это коллекция записей из одной или нескольких таблиц базы данных. Наборы данных можно получить с помощью компонент TADOTable, TADOQuery или TADOStoredProc, который необходим для архитектуры клиентсервер. Каким образом получаются наборы данных? Когда мы открываем таблицу, то есть, присваиваем True свойству Active компонента TADOTable, например, специальный механизм делает выборку записей в соответствии с заданными параметрами, и возвращает нам эти записи в виде таблицы. Можно сказать, что наборы данных – это прослойка между нашим приложением и реальными таблицами, хранящимися в базе данных. Все указанные выше компоненты являются наборами данных, имеют общего предка – класс TDataSet и заимствовали от него свойства, методы и события, добавляя собственные возможности. Об этом и поговорим на этой лекции.
Свойства Active – Свойство имеет логический тип и позволяет открыть или закрыть набор данных, если свойству присвоить True или False соответственно. В зависимости от свойства CanModify данные можно либо только просматривать, либо можно также редактировать их. AutoCalcFields – Свойство логического типа. Если установить значение False, то возникновение события OnCalcFields будет подавляться, вычисляемые поля обрабатываться не будут. Значение True разрешает расчет вычисляемых полей. Bof – Свойство имеет логический тип и содержит True, если курсор находится на первой записи набора данных, и False в противном случае. Bof содержит True, когда: Не пустой набор данных открывается. При вызове метода First. При вызове метода Prior, если курсор при этом на первой записи набора данных. При вызове метода SetRange в пустом наборе данных или диапазоне. Bookmark – Свойство позволяет установить закладку на текущей записи набора данных. Количество закладок может быть неограниченно, работа с закладками рассматривалась на курсе «Введение в программирование на Delphi». Свойство имеет тип TBookmarkStr. CanModify – Свойство имеет логический тип, и показывает, можно ли редактировать полученный набор данных, или он доступен только для чтения. При открытии набора данных автоматически запрашивается доступ для редактирования. В таком доступе может быть отказано по разным причинам, например, таблица открыта другим пользователем в эксклюзивном режиме. В этом случае CanModify получает значение False, и мы можем только просматривать данные, но не вносить в них изменения. DatabaseName – Свойство строкового типа, содержит адрес базы данных или ее псевдоним. Однако это справедливо к наборам данных BDE. В случае использования механизма ADO, это свойство недоступно – вместо него для подключения к базе данных следует использовать свойство Connection или ConnectionString. DataSource – Свойство используется в наборах данных для указания детального набора данных в отношениях один-ко-многим. DefaultFields – Свойство логического типа, содержит True, если программист не создал ни одного поля в редакторе полей набора данных. В этом случае все поля определяются автоматически, в соответствии с данной таблицей.
32
Eof – Свойство, противоположное свойству Bof. Имеет логический тип, и имеет значение True в случаях, когда: Открыт пустой набор данных. Вызван метод Last. Вызван метод Next, если указатель при этом находится на последней записи таблицы. При вызове метода SetRange в пустом наборе данных или диапазоне. FieldCount – Свойство целого типа, содержит количество полей в наборе данных. Fields – Свойство позволяет получить значение нужного поля по его индексу. Поля при этом индексируются с нуля. Например, получить значение седьмого по счету поля набора данных можно так: Edit1.Text := CustTable.Fields[6].Value;
FieldValues – Свойство позволяет получить значение нужного поля по его имени. Это свойство используется по умолчанию, поэтому его можно не указывать. Примеры: Edit1.Text := CustTable.FieldValues['Order']; Edit1.Text := CustTable['Order'];
Filter – Свойство строкового типа. Содержит строку, которая определяет правила фильтрации набора данных. Filtered – Свойство логического типа. Если в свойстве Filter имеется строка, определяющая порядок фильтрации, то присвоение значения True свойству Filtered приводит к фильтрации набора данных. Присвоение этому свойству False отменяет фильтрацию. FilterOptions – Свойство имеет тип TFilterOptions и применяется для строковых или символьных полей. Свойству можно присвоить значение foCaseInsensitive или foNoPartialCompare. В первом случае фильтрация будет учитывать регистр букв, во втором учитывается лишь точное совпадение образцу. Modified – Очень важное свойство логического типа. Содержит True, если набор данных был изменен, и False в противном случае. Часто применяется для проверок: если набор данных изменен, то вызвать метод Post, чтобы сохранить изменения. RecNo и RecordCount – Свойства целого типа. Первое содержит номер текущей записи в наборе данных, второе – общее количество записей. State – Очень важное свойство, определяющее состояние набора данных. Может иметь следующие значения: dsInactivate – набор данных закрыт. dsBrowse – режим просмотра. dsEdit – режим редактирования. dsInsert – режим вставки. dsSetKey – поиск записи. dsCalcFields – состояние установки вычисляемых полей. dsFilter – режим фильтрации записей. dsNewValue – режим обновления свойства TField.NewValue. dsOldValue – режим обновления свойства TField.OldValue. dsCurValue – режим обновления свойства TField.CurValue. dsBlockRead – состояние чтения блока записей. dsInternalCalc – обновление полей, у которых свойство FieldKind соответствует значению fkInternalCalc.
33
Методы Append – Метод добавляет новую запись в конец набора данных. При этом набор данных автоматически переходит в режим редактирования. AppendRecord(const Values: array of const) – Метод добавляет новую запись в конец набора данных, и заполняет поля этой записи значениями из массива, переданного в метод как параметр. Cancel – Отменяет все изменения набора данных, если они еще не сохранены методом Post или переходом на другую запись. ClearFields – Метод очищает все поля текущей записи. Close – Закрывает набор данных. Метод является альтернативой присваивания False свойству Active набора данных. Delete – Метод удаляет текущую запись. Следует заметить, что во многих форматах данных удаляемая запись лишь помечается, как удаленная, и скрывается от пользователя. Физически же такая запись из файла не удаляется. В этом случае обычно время от времени приходится «паковать» таблицы, избавляясь от таких записей. Edit – Метод переводит набор данных в состояние редактирования. Если этого не сделать, изменение записи будет невозможным. FieldByName – Еще один способ получить значение поля или изменить его, указывая имя поля. При этом можно использовать явное преобразование данных в нужный тип, например, AsInteger, AsString и т.п. Пример: Table1.FieldByName('QUANTITY').AsInteger := StrToInt(Edit1.Text);
FindFirst, FindLast, FindNext и FindPrior – Методы пытаются установить курсор соответственно, на первую, на последнюю, на следующую и на предыдущую запись. В случае успеха методы возвращают True. Переход к другой записи приводит к автоматическому сохранению изменений, если изменения были. First, Last, Next и Prior – просто устанавливают указатель соответственно на первую, последнюю, следующую и предыдущую запись. Переход к другой записи приводит к автоматическому сохранению изменений, если изменения были. FreeBookmark – Метод освобождает память, связанную с закладкой Bookmark. Обычно вместо вызова этого метода достаточно присвоить закладке пустую строку (см. лекцию 30 курса «Введение в программирование на Delphi»). GotoBookmark – Метод обеспечивает переход на закладку Bookmark, переданную в качестве параметра. Insert – Метод вставляет новую запись в указанную в параметре позицию набора данных. При этом набор данных автоматически переходит в режим редактирования. InsertRecord(const Values: array of const) – Метод вставляет новую запись в набор данных, и заполняет поля этой записи значениями из массива, переданного в метод как параметр. Пример: Customer.InsertRecord([CustNoEdit.Text, CoNameEdit.Text, AddrEdit.Text, Null, Null, Null, Null, Null, Null, DiscountEdit.Text]);
34
Обратите внимание, что в некоторые поля были вставлены значения Null, то есть, ничего. То же самое происходит, когда пользователь при редактировании записи вносит значения не во все поля. IsEmpty – Метод возвращает True, если в наборе данных нет записей. Применяется для проверки – не пуста ли таблица? Locate – Метод ищет запись в наборе данных (см. предыдущую лекцию). Lookup - Метод ищет запись в наборе данных (см. предыдущую лекцию). В отличие от Locate не переводит указатель на найденную запись, а лишь возвращает значения ее полей. Open – Метод открывает набор данных. То же самое происходит, если свойству Active набора данных присвоить значение True. Post – Метод сохраняет сделанные изменения в наборе данных. Refresh – Метод заново перечитывает таблицу и обновляет набор данных. Имеет смысл использовать в приложениях, где несколько пользователей работают с одной базой данных.
События After… - События, возникающие после вызова соответствующего метода: AfterCancel – Событие возникает после отмены изменений в текущей записи. AfterClose – Событие возникает после закрытия набора данных. AfterDelete – Событие возникает после удаления текущей записи. AfterEdit – Событие возникает после перехода набора данных в режим редактирования. AfterInsert – Событие возникает после вставки новой записи. AfterOpen – Событие возникает после открытия набора данных. AfterPost – Событие возникает после вызова метода Post. AfterScroll – Событие возникает после перехода на другую запись. Before… - События, возникающие перед вызовом соответствующего метода: BeforeCancel - Событие возникает перед отменой изменений в текущей записи. BeforeClose - Событие возникает перед закрытием набора данных. BeforeDelete - Событие возникает перед удалением текущей записи. BeforeEdit - Событие возникает перед переходом набора данных в режим редактирования. BeforeInsert - Событие возникает перед вставкой новой записи. BeforeOpen - Событие возникает перед открытием набора данных. BeforePost - Событие возникает перед вызовом метода Post. BeforeScroll - Событие возникает перед переходом на другую запись. OnCalcFields – Событие возникает при необходимости переопределения вычисляемых полей. Такое событие возникает всякий раз, когда программа должна сформировать значения для вычисляемых полей. Событие возникает также при открытии набора данных, и при любом его изменении. Если алгоритм вычислений достаточно сложен, база данных большая, а пользователь интенсивно с ней работает, событие OnCalcFields может значительно замедлить работу с базой данных. В этом случае следует отключать это событие. Для этого достаточно присвоить значение False свойству AutoCalcFields текущего набора данных. OnFilterRecord – Событие возникает при включении фильтрации записей. OnNewRecord – Событие возникает при вызове методов Append или Insert. 35
Блокировка таблиц в архитектуре файл-сервер При работе в архитектуре файл-сервер с единой сетевой базой данных работают несколько клиентских приложений. При этом нередко возникает ситуация, когда один пользователь вносит изменения в базу данных. В этот момент, во избежание потери или порчи данных, следует запретить внесение изменений другими пользователями. Такая блокировка достигается методом LockTable(). При этом значение свойства LockType этого набора данных определяет вид запрета. Значение ltReadLock запрещает чтение, а ltWriteLock – запись в набор данных. Можно запретить и чтение, и запись, но для этого следует вызвать метод LockTable() дважды. Блокировка таблиц методом LockTable() справедлива для таблиц Paradox или dBase, если вы используете механизм BDE. Когда изменения внесены, и необходимость блокировки пропадает, можно снять блокировку методом UnlockTable(), указав в параметре тип снимаемого запрета (ltWriteLock, ltReadLock). Свойство LockType набора данных ADO имеет тип TADOLockType: type TADOLockType = (ltUnspecified, ltReadOnly, ltPessimistic, ltOptimistic, ltBatchOptimistic);
Это свойство позволяет определить тип блокировки при открытии набора данных. Как видно из описания типа, свойство может иметь следующие значения: ltUnspecified – тип блокировки не определен. ltReadOnly – блокировка записи, читать данные можно. ltPessimistic – пессимистическая блокировка. Свойство указывает, что если вы редактируете запись, то другие пользователи не смогут редактировать ее, пока вы не сохраните изменения. ltOptimistic – оптимистическая блокировка. Блокировка подразумевает, что возникновение конфликта маловероятно. В связи с этим любой пользователь в любое время может редактировать любую запись. Проверка на наличие конфликтов производится только в момент сохранения изменений. ltBathOptimistic – свойство устанавливает блокировку на пакет записей, а не на отдельную запись. При этом все обновления, сделанные пользователем, не записываются сразу, а накапливаются в оперативной памяти. Позже они сохраняются одним пакетом. Такой подход увеличивает производительность приложения, но также увеличивается риск возникновения конфликтов.
Курсоры в наборах данных ADO Наборы данных ADO имеют два специфичных свойства, неразрывно связанные друг с другом: CursorLocation и CursorType. Курсоры оказывают большое влияние на то, каким образом извлекаются данные из таблиц, каким образом вы можете перемещаться по ним и т.д. Фактически, курсор – это механизм перемещения по записям набора данных. От того, какой курсор используется в многопользовательской среде, зависит способ перемещения по записям: только вперед или в обе стороны. Будете ли вы видеть изменения, сделанные другими пользователями, также зависит от типа применяемого курсора.
CursorLocation (положение курсора) Это свойство определяет, каким образом извлекаются и модифицируются данные. Значений только два: clUseClient – курсор на стороне клиента. clUseServer – курсор на стороне сервера. Клиентский курсор обслуживается механизмом ADO Cursor Engine. В момент открытия набора данных все данные перекачиваются с сервера на клиентский компьютер. После этого данные хранятся в оперативной памяти. Перемещения по данным и их модификация происходит значительно быстрее, кроме того, клиентский курсор обладает более широкими возможностями. 36
Серверный курсор обслуживается операционной системой. Благодаря тому, что курсор находится на стороне сервера, приложению нет смысла перекачивать все данные разом, это повышает скорость работы с БД. Серверные курсоры больше подходят для обслуживания больших наборов данных. Следует заметить, что если вы работаете с локальной базой данных (например, Access), то серверный курсор будет обслуживаться программой, обслуживающей эту базу данных.
CursorType (тип курсора) Имеется пять типов курсора:
Unspecified – не указанный. В Delphi такой тип не используется, он присутствует только потому, что имеется в ADO. Forward-only (только вперед). Этот тип курсоров обеспечивает самую высокую производительность, однако он позволяет перемещаться по записям только в одном направлении – от начала к концу, что делает его малопригодным для создания пользовательского интерфейса. Однако он хорошо подходит для программных операций, таких как перебор записей, формирование отчета и т.п. Static (статический) – пользователь имеет возможность перемещаться в обоих направлениях, однако изменения записей, выполненные другими пользователями, не видны таким курсором. Keyset (набор ключей). При открытии набора данных с сервера читается полный список всех ключей. Этот набор ключей хранится на стороне клиента. Если приложение нуждается в данных, провайдер OLE DB читает строки таблицы. Однако после открытия набора данных в этот набор нельзя добавлять новые ключи, или удалять имеющиеся. То есть, если другой пользователь добавил новую запись, текущий клиент ее не увидит. Однако он увидит изменения существующих записей, сделанные другими пользователями. Dynamic (динамический). Самый мощный тип курсора, но при этом и самый ресурсоемкий. Он позволяет видеть все изменения, все добавления или удаления, сделанные другими пользователями, но при этом больше других замедляет работу с БД.
37
Лекция 5. Таблицы Paradox в ADO. Подключение таблиц Paradox 7 к приложению через ADO Изучение свойств полей лучше сразу проводить на примере. Для этого создадим небольшую демонстрационную базу данных, всего из двух таблиц, и приложение, работающее с ней. Попутно затронем темы, которых раньше не касались. Цель проекта: создать мини-меню для столовой, кафе или ресторана. Прежде всего, определимся с таблицами. Таблицы будем создавать в формате Paradox 7, описание типов полей которого подробно рассматривалось на лекции №30 курса «Введение в программирование на Delphi». Для доступа к данным этих таблиц используем механизм ADO. Создавать таблицы удобней с помощью утилиты Database Desktop, входящей в состав Delphi. Пусть главная таблица называется Food, ее поля описаны в таблице 5.1: Таблица 5.1 Имя поля: FKey FName FType
Поля таблицы Food Тип Auto increment (+) Alpha (A) Long Integer (I)
FVeget
Logical (L)
FCena
Money ($)
Описание Ключевое поле, служит счетчиком блюд. Текстовое поле размером 30, название блюда. Поле служит для связи с подчиненной таблицей, в которой хранятся названия типов (супы, напитки, салаты и т.п.) Логическое поле – вегетарианская еда, или нет. Потребуется для изучения свойств логических полей. Поле денежного типа. Стоимость блюда.
Подчиненная таблица будет еще проще: Таблица 5.2. Поля таблицы Tips Имя поля: Тип TKey Auto increment (+) TName Alpha (A)
Описание Ключевое поле, служит счетчиком типов. Текстовое поле размером 20, название типов.
Итак, начнем. Откройте утилиту Database Desktop. Чтобы облегчить работу и не искать каждый раз нужный каталог, укажем сразу рабочую папку, которую нужно вначале создать средствами Windows: C:\Menu Для этого выберите команду меню «File -> Working Directory». В открывшемся окне нажмите кнопку «Browse» и найдите эту директорию на диске. Когда вы выберите ее, нажмите кнопку «OK». Теперь эта папка стала папкой «по умолчанию». При попытке открыть или создать таблицу в утилите Database Desktop, эта папка всегда будет текущей (если в дальнейшем вы не смените рабочую папку). Далее выбираем команду «File -> New -> Table». Оставьте тип Paradox 7, нажмите «ОК». Далее вам предлагается ввести названия и типы полей. Сделайте это, как в таблице 5.1. Как только вы ввели названия, типы и размеры (размер есть только у только текстового поля), в списке «Table properties» выберите команду «Table Language» и нажмите кнопку «Modify». В выпадающем списке выберите язык, как на рисунке 5.1:
Рис. 5.1. Выбор языкового драйвера для таблицы Paradox 38
Если этого не сделать, у вас будут проблемы с отображением русских символов. Далее нажмите кнопку «Save as» и укажите имя таблицы: Food. Таким же образом сделайте таблицу Tips, руководствуясь таблицей 5.2. После этого вы можете закрыть утилиту Database Desktop, она больше не нужна. Пойдем дальше. Поскольку мы собираемся подключаться к таблицам Paradox с помощью механизма доступа к данным ADO, нам потребуется установить на компьютере нужный драйвер ODBC. Для этого откройте Панель управления (Пуск -> Настройка -> Панель управления). Если вы используете Windows 2000, XP или более новую, вам придется еще выбрать команду «Администрирование». Далее открываем «Источники данных ODBC». Нажимаем кнопку «Добавить», выбираем драйвер «Microsoft Paradox Driver (*.db)» и нажимаем кнопку «Готово». Далее в поле «Имя источника данных» укажите MenuParadox, этот источник мы будем использовать только для этой нашей программы. Затем уберите галочку «Использовать текущий каталог» и нажмите кнопку «Выбор каталога». В открывшемся окне выберите нашу папку C:\Menu:
Рис. 5.2. Установка драйвера ODBC Нажимаете «ОК», и драйвер готов. Теперь можете закрыть все остальные окна, они больше не нужны. Загружаете Delphi. Свойству Name главной формы присвойте имя fMain, сохраните модуль формы как Main, а проект в целом как MyMenu. В свойстве Caption формы напишите «Изучение свойств полей». На форму бросьте компонент Panel с вкладки Standard, свойство Align установите в alTop. Ниже с вкладки Data Controls установите компонент DBGrid, в свойстве Align которого выберите alClient, чтобы заполнить оставшееся пространство. Затем на панель установите простую кнопку, в свойстве Caption которой напишите «Типы блюд». У вас должна получиться такая форма:
39
Рис. 5.3. Главная форма проекта Раз у нас еще будет форма с типами блюд, следовательно, понадобится и модуль данных, общий для всех форм. Выберите команду File -> New -> Data Module. В свойстве Name модуля укажите fDM и сохраните модуль под именем DM. Теперь с вкладки ADO устанавливаем компонент ADOConnection. Сразу свойство Name для краткости обращения переименуйте в Con1. Займемся подключением. Дважды щелкните по компоненту, чтобы открыть редактор подключений. Нажмите кнопку «Build». На вкладке «Поставщик данных» по умолчанию должен быть «Microsoft OLE DB Provider for ODBC Drivers». Нам нужен именно этот поставщик. Переходим на вкладку «Подключение» (для этого можете просто нажать кнопку «Далее»). В выпадающем списке «Использовать имя источника данных» нам нужно выбрать MenuParadox, то подключение, которое мы создали ранее. К слову сказать, мы могли и не указывать адрес данных, могли оставить галочку «Использовать текущий каталог». В этом случае таблицы нужно было бы расположить там же, где и программа, или создавать подключение при работающей программе. Так удобно делать при использовании локальной базы данных. Способ, которым мы воспользовались сейчас, более удобен для многопользовательских файл-серверных БД. Если бы базы лежали где-то на сетевом диске, тогда мы могли бы в качестве папки указать сетевой путь, например: \\myserver\Menu но тогда эта папка должна быть открыта в сети как общий ресурс. Нажмите кнопку «Проверить подключение». Если вышло сообщение «Проверка подключения выполнена», значит, вы все сделали правильно, и ошибок нет. Нажимаем кнопку «ОК», чтобы подтвердить подключение, и еще раз «ОК», чтобы закрыть окно подключений. Сразу же свойство LoginPrompt компонента Con1 переводим в False, чтобы каждый раз при подключении программа не запрашивала имя пользователя и пароль. Затем в свойстве Connected устанавливаем True. Подключение произошло. Далее с вкладки ADO устанавливаем два компонента ADOTable. Выделите оба компонента, и в их свойстве Connection выберите наш Con1. Займемся вначале первой таблицей. В свойстве TableName выберите таблицу Food, свойство Name переименуйте в FoodT, а свойство Active переведите в True. Для второго компонента ADOTable выберите таблицу Tips, а компонент переименуйте в TipsT. Также переведите Active в True. Далее рядом с таблицами установите два компонента DataSource с вкладки Data Access. Первый переименуйте в FoodDS, второй – в TipsDS. В свойстве DataSet каждого выберите соответствующую таблицу. Не забудьте сохранить проект. 40
Перейдите на главную форму. Командой File -> Use unit подключитесь к созданному модулю данных. В свойстве DataSource сетки DBGrid выберите fDM.FoodDS. На сетке должны появиться столбцы с данными. Нажмите кнопку Run на панели инструментов или горячую клавишу F9. Проект компилируется, запускается, и… выходит ошибка:
Рис. 5.4. Ошибка при компиляции В чем дело? Вроде бы, мы все делали правильно, иначе на сетке DBGrid не появились бы нужные столбцы? Просто мы добрались до проблем с полями. Нажмите кнопку «ОК», затем выберите Run -> Program reset, чтобы закрыть повисшую программу. Теперь перейдите на окно модуля данных, щелкните дважды по компоненту FoodT, чтобы вызвать редактор полей. Затем щелкните по окну редактора правой кнопкой и выберите команду Add all fields (Добавить все поля). То же проделайте и со второй таблицей. Снова сохраните проект, скомпилируйте его и запустите – теперь полный порядок, программа запускается и выполняется нормально. Дело в том, что, используя драйверы ODBC с «неродными» форматами баз данных, такими как dBase или Paradox, приходится точно указывать, какие у нас поля, а не надеяться на авто-определение. Вообще, создавать поля для каждого набора данных считается хорошим тоном в программировании. Далее создадим еще одну форму, для редактирования типов блюд:
Рис. 5.5. Форма редактора типов блюд Форму назовите fMyTypes, сохраните модуль как MyTypes. Чтобы убрать из окна лишние кнопки системной строи и не позволять пользователю менять размеры окна, в свойстве BorderStyle формы выберите значение bsDialog. Не забудьте подключить к нему модуль данных DM. На форме две простых кнопки, поле DBEdit с вкладки Data Controls и сетка DBGrid с этой же вкладки. В свойстве DataSource и сетки, и поля выберите fDM.TipsDS. У поля DBEdit, кроме того, в свойстве DataField выберите поле TName. Обратите внимание, что я назвал форму и модуль как MyTypes, а не просто как Types. Слово Types (типы) довольно распространенное в языках программирования и может вызвать конфликт названий. Попробуйте, если не верите! Дважды нажимаем на верхнюю кнопку, и в обработчике пишем код: //добавляем запись: fDM.TipsT.Append; //переводим фокус: DBEdit1.SetFocus;
41
В коде обработки нижней кнопки просто закрываем окно: Close;
Однако нам нужно убедиться, что если изменения в таблице были, и пользователь желает их сохранить, то они сохраняются. Поскольку мы не знаем точно, каким образом пользователь закроет это окно, придется для проверки сгенерировать событие onClose для формы: {если изменения есть, спросим что с ними делать. если пользователь не желает их сохранять, отменяем изменения. иначе сохраняем: } if fDM.TipsT.Modified then if Application.MessageBox('Данные изменены! Сохранить?', 'Внимание!', MB_YESNO+MB_ICONQUESTION) <> IDYES then fDM.TipsT.Cancel else fDM.TipsT.Post;
Далее переходим на главную форму, командой File -> Use Unit подключаем модуль MyTypes, дважды щелкаем по кнопке «Типы блюд» и в сгенерированном событии вызываем новый модуль: fMyTypes.ShowModal;
Сохраните проект, скомпилируйте его и впишите 5-10 типов блюд, например, «Напитки», «Супы», «Салаты» и т.п. Это нам будет нужно для подстановочного поля. Если в поле автоинкремента у вас будут выходить нули, не обращайте внимания – после сохранения таблицы там окажутся правильные цифры. Вы сможете убедиться в этом, закрыв программу и загрузив ее еще раз. Теперь займемся формой для редактирования основной таблицы. Командой File -> New -> Form или аналогичной кнопкой на панели инструментов создайте новую форму. В свойство Caption этой формы впишите «Редактирование блюда», в свойстве Name укажите fEditor, а модуль сохраните как Editor. Сразу же командой File -> Use Unit подключите к этой форме модуль данных DM. Форма будет выглядеть так:
Рис. 5.6. Форма редактора блюда Как видно из рисунка, на форме присутствуют поясняющие компоненты Label, три компонента DBEdit, один DBLookupComboBox, один DBNavigator и кнопка BitBtn, в свойстве Kind которой выбрано значение bkClose. Компонент DBLookupComboBox немного сложней остальных. Это подстановочный компонент. Из основной таблицы Food он будет брать целое число – значение поля FType. А из дочерней таблицы Tips этот компонент будет просматривать все значения поля TName. Когда пользователь выберет какойнибудь тип блюда, целое число, соответствующее ключевому полю TKey, попадет в поле FType главной таблицы. Другими словами, у нас получилась связь один-ко-многим (многие блюда основной таблицы могут иметь одинаковый тип):
42
Рис 5.7. Связь между таблицами Выделите все компоненты, относящиеся к редактированию данных или перемещению по ним (начинающиеся на DB…), и в их свойстве DataSource выберите fDM.FoodDS. Затем с помощью свойства DataField подключите все DBEdit к соответствующему полю таблицы. У компонента DBLookupComboBox установите следующие значения: Таблица 5.3. Значения свойств компонента DBLookupComboBox Свойство Значение DataSource fDM.FoodDS DataField FType ListSource fDM.TipsDS KeyField TKey ListField TName
Как видно из таблицы, компонент DBLookupComboBox имеет такие важные свойства: DataSource – свойство содержит ссылку на компонент TDataSource, связанный с основной таблицей. DataField – свойство указывает на имя ссылочного поля основной таблицы. В это поле после выбора значения из списка DBLookupComboBox попадает значение ключевого поля подстановочной таблицы. ListSource – свойство содержит ссылку на компонент TDataSource, связанный с подстановочной (дочерней) таблицей. KeyField – свойство содержит имя ключевого поля подстановочной таблицы. По этому полю ищется нужная подстановочная запись. ListField – свойство содержит имя поля подстановочной таблицы, по которому формируется список значений DBLookupComboBox. Эти значения также можно подставлять в основную таблицу в виде lookup (подстановочного) поля.
Если говорить еще проще, то при открытии основного и подстановочного наборов данных, компонент DBLookupComboBox соединяется с подстановочной таблицей, указанной в свойстве ListSource, и формирует список значений из поля, указанного в ListField. Далее, при редактировании основной таблицы, пользователь выбирает из списка DBLookupComboBox одно из значений. При этом DBLookupComboBox смотрит, какое значение выбранной записи в подстановочной таблице имеет ключевое поле KeyField. Как правило, это целое число. Это число DBLookupComboBox и заносит в ссылочное поле основной таблицы DataField. Похожим образом действует и компонент DBLookupListBox, разумеется, учитывая специфику компонента. Форма fEditor предназначена для редактирования имеющегося блюда или добавления нового, в зависимости от того, каким способом вызвали форму. Поэтому здесь нам нужно лишь создать код для события закрытия формы onClose, куда пропишем: if fDM.FoodT.Modified then if Application.MessageBox('Данные изменены! Сохранить?', 'Внимание!', MB_YESNO+MB_ICONQUESTION) <> IDYES then fDM.FoodT.Cancel else fDM.FoodT.Post;
43
Перейдем на главную форму. Командой File -> Use Unit добавим к главной форме новое окно. Пользователь должен иметь возможность редактировать имеющуюся запись, поэтому сгенерируем событие onDblClick для сетки DBGrid1, и пропишем туда следующий код: fEditor.ShowModal;
Рядом с кнопкой «Типы блюд» добавим еще одну кнопку «Добавить блюдо». Сгенерируйте событие нажатия на эту кнопку и пропишите такой код: fDM.FoodT.Append; fEditor.ShowModal;
Как видите, отличие кода заключается лишь в том, что при нажатии на кнопку добавляется новая запись, открывается редактор и пользователь редактирует ее. А если он дважды щелкнет по записи на сетке, то откроется тот же редактор, в который будет загружена текущая запись. Впрочем, благодаря навигатору DBNavigator, пользователь и там имеет возможность перемещаться по записям, добавлять или удалять записи. Мы, собственно, получили далекую от идеала, но вполне работоспособную программу. Сохраните проект, скомпилируйте и введите для примера несколько блюд:
Рис. 5.8. Программа в действии Как видите, программа имеет множество недостатков: пользователь видит совершенно ненужные ему ключевые поля, в сетке он видит лишь номер типа блюда, но не видит название этого типа. В поле FVeget ему вручную приходится писать True или False вместо привычных Да/Нет. Еще недостаток: названия полей в сетке соответствуют названиям полей в таблице, а поле «FType» или «FVeget» мало что скажет пользователю. Исправлением этих недостатков займемся в следующей лекции, вместе с изучением свойств полей.
44
Лекция 6. Поля (TField) Каждая база данных состоит из таблиц, каждая таблица – из записей. Каждая запись в свою очередь представляет собой набор полей. Поле набора данных – это экземпляр достаточно мощного класса TField, о котором мы и поговорим на этой лекции. Изучение свойств полей будем проводить на примере приложения из прошлой лекции.
Подстановочные (Lookup) поля Подстановочное поле Lookup изначально в набор данных не входит, его нужно создавать самостоятельно. Такое поле отличается от обычного тем, что показывает данные из другого набора данных. Для использования такого поля два набора данных обязательно должны иметь релятивную связь. На прошлой лекции мы применяли компонент DBLookupComboBox, который является аналогом подстановочного поля, но который нельзя показать в сетке DBGrid. При создании подстановочного поля также необходимо указать набор данных, откуда поле будет просматривать значения, ключевые поля для релятивной связи и поле со значениями, которые нужно подставлять. Открываем проект из лекции № 5, открываем окно модуля данных. Дважды щелкаем по компоненту FoodT, чтобы открыть редактор полей. В этом редакторе у нас уже присутствуют пять полей, имеющихся в таблице, добавим шестое, подстановочное. Для этого щелкните правой кнопкой по редактору полей и выберите команду New Field (Новое поле):
Рис. 6.1. Создание подстановочного (Lookup) поля В разделе Field type (Тип поля) вы можете выбрать один из трех вариантов. Нас сейчас интересует тип Lookup. Заполните необходимые поля значениями, как на рисунке 6.1 и нажмите кнопку «ОК». Новое подстановочное поле будет добавлено в набор данных. В списке полей его можно переместить мышью на другое место, установите его сразу под FName. Перейдите на окно главной формы и убедитесь, что новое поле появилось. Однако оно пока еще не содержит данных – данные будут доступны только во время прогона программы. Сохраните проект, скомпилируйте и посмотрите, как работает программа. Как мы видим, теперь на главной форме два поля, которые ни к чему показывать пользователю – FKey с номерами записей, и FType – с номерами типов блюд, которые нам уже не нужны, поскольку мы показываем сами типы. Уберем их, точнее, сделаем невидимыми. Снова откройте редактор полей набора данных FoodT. Установите свойство Visible этих полей в False.
Вычисляемые (Calculated) поля Как и подстановочное, вычисляемое поле изначально не входит в набор данных, а добавляется в процессе проектирования приложения. Вычисляемые поля предназначены для показа данных, которые автоматически вычисляются в процессе работы программы, используя одно или несколько полей 45
набора данных. К примеру, в таблице имеется поле стоимости товара и количество, которое купил какой-то клиент. Вычисляемое поле, перемножив значения этих полей, может показать общую стоимость товара. В нашем примере мы создадим вычисляемое поле для показа стоимости блюда в долларах США. Для этого в модуле данных создадим глобальную переменную dollar: var fDM: TfDM; dollar: Currency = 30.36;
Вы можете указать текущий курс доллара к рублю, он так быстро меняется, что едва ли будет таким, как в моем примере. Итак, дважды щелкаем по набору данных FoodT, чтобы открыть редактор полей. Щелкаем правой кнопкой по этому редактору и выбираем команду «New field». В поле «Name» впишите название нового поля FDCena. В поле «Component» автоматически отобразится имя нового объекта-поля «FoodTFDCena», по которому в дальнейшем мы сможем к нему обращаться. Это имя составное – имя набора данных плюс имя нового поля, без всяких пробелов и разделителей. В поле «Type» выберите тип Float, так как у нас могут быть копейки, вернее, центы. Затем убедитесь, что переключатель установлен на «Calculated» и нажмите «ОК». В редакторе полей появилось новое поле. Чтобы мы не получили сумму с кучей цифр после запятой, выделите в редакторе полей поле FDCena, и в его свойстве DisplayFormat укажите маску «#.## $US» (разумеется, без кавычек). К слову сказать, при создании вычисляемого поля мы могли бы выбрать тип Currency (денежный), но тогда к цифре добавлялось бы «р.», если ваша Windows имеет российские настройки. Вещественные поля набора данных наряду с полями целого типа имеют четыре свойства, которые могут вам пригодиться: DisplayFormat – Определяет формат отображения числа. DisplayEdit – Определяет формат числа при редактировании. MaxValue – Определяет максимально возможное для поля число. MinValue – Определяет минимально возможное число. Свойства MaxValue и MinValue по умолчанию имеют значение 0, что указывает на отсутствие ограничений. Однако, это еще полдела. Поле мы сделали, осталось сделать вычисления. Код необходимых вычислений прописывается в свойстве OnCalcFields набора данных. Закройте редактор полей и выделите НД FoodT. Сгенерируйте для него обработчик события OnCalcFields и в этом обработчике пропишите следующую строчку: FoodTFDCena.Value := FoodTFCena.Value / dollar;
Как видно из примера, мы используем значения одного или нескольких полей текущего набора данных, производим над ними какие то вычисления, и результат этих вычислений присваиваем вычисляемому полю. Сохраните проект, скомпилируйте и посмотрите, как работает программа. Если вы все сделали правильно, у вас получится подобная картина:
46
Рис. 6.2. Подстановочное и вычисляемое поле в программе
Поле данных (Data) Если вы помните, при создании нового поля мы имеет три переключателя Field type. Переключатель «Data» предназначен для создания поля данных – пустого поля, которое программист использует по своему усмотрению. Наполнение этого столбца можно прописать в обработчике события OnGetText полученного объекта-поля. На практике такие поля используют редко, чаще они применяются для программного создания таблиц, о чем мы поговорим на одной из следующих лекций.
Свойство DisplayValues Свойство DisplayValues объекта-поля предназначено для отображения данных логического поля в нужном формате. Для примера изменим отображение данных поля FVeget. Откройте редактор полей компонента FoodT, выделите поле FVeget. В его свойстве DisplayValues укажите значение «Да;Нет». У логического поля вместо True и False здесь можно указать свою пару значений. Значение до точки с запятой считается истинным, значение после – ложным. Примеры: Да;Нет Муж;Жен Yes;No Y;N Д;Н и т.п. Указанные в свойстве значения пары «Истина;Ложь» будут отображаться в компонентах вывода данных, таких как DBGrid, DBEdit и т.п. Кроме того, эти же значения будут отображены, если вы будете получать значения этого поля с использованием свойства AsString, чтобы преобразовать значение в строковый тип. Для облегчения пользователю ввода данных немного изменим проект. Откройте окно fEditor и удалите DBEdit, предназначенный для ввода логического значения в поле FVeget. Вместо него установите компонент DBComboBox. Дважды щелкните по свойству Items этого компонента и в открывшемся редакторе значений впишите две строки: Да Нет В свойстве DataSource компонента выберите таблицу FoodT, а в свойстве DataField – поле FVeget. Сохраните проект, скомпилируйте и посмотрите, как он работает. Теперь пользователю не нужно вписывать значение – он может выбрать его из списка. Для полей других типов свойство DisplayValues недоступно. Вместо него предлагается использовать свойство DispalyFormat, которое доступно только для числовых полей и полей типа 47
TDataTime. При этом формат задается так же, как в функциях формата, например, FormatFloat() и FormatDateTime(), применение которых подробно рассматривалось на курсе «Введение в программирование на Delphi». Например, для поля типа Дата формат: dddd dd mmm yyyy выведет дату в формате «Понедельник 04 Янв 2010»
Другие наиболее важные свойства класса TField Aligment – Определяет выравнивание выводимого значения. Может иметь следующие значения: taLeftJustufy - выравнивание по левому краю taRightJustify - выравнивание по правому краю taCenter - выравнивание по центру. AsXXXX – Группа свойств этого типа преобразует значение поля к нужному типу. Вместо XXXX могут быть использованы: BCD – двоично-десятичный тип. Boolean – логический тип. Currency – денежный тип. DataTime – тип дата-время. Float – вещественный тип. Integer – целый тип. String – строка. Variant – variant. Пример: DBText1.Field.AsString := 'Santa Cruz Wharf';
Calculated – Содержит True, если значение поля вычисляется в обработчике OnCalcFields набора данных, и False в противном случае. CanModify – Содержит True, если значение поля можно изменить, и False в противном случае. Currency – Свойство доступно у вещественных полей. Если свойству при проектировании приложения присвоить True, то значения будут выходить в денежном формате. DataSize – Содержит размер данных. DataType – Содержит тип данных, определяемый перечислением TFieldType, например, ftString – строка, ftBoolean – логический тип, ftFloat – вещественный тип, и так далее. Класс TFieldType содержит достаточно большой список типов полей, более подробные данные вы можете посмотреть в справочной системе Delphi. DisplayLabel – Позволяет ввести строку – заголовок отображаемого столбца. Если заголовок не задан, по умолчанию будет использоваться имя поля. EditMask – Позволяет указать строку – маску для ввода данных. FieldName – Имя поля. Lookup – Содержит True, если поле подстановочное. Origin – Содержит имя поля в физической таблице. 48
ReadOnly – Если содержит True, значение поля нельзя менять. Required – Если содержит True, значение поля не может быть пустым. Size – Если поле имеет запись переменной длины, свойство указывает текущий размер данных. Value – Содержит значение поля. Visible – Если содержит True (по умолчанию), поле отображается в таких компонентах, как DBGrid.
Наиболее важные методы класса TField AssignValue() – Преобразует вариантное значение поля Value с помощью метода AsXXXX и помещает результат в переменную Value, переданную в метод как параметр. Create() – Создает поле-объект и инициализирует его. Destroy() – Уничтожает поле-объект.
Наиболее важные события класса TField OnChange – возникает после изменения данных поля и их успешной записи. OnGetText – в обработчике этого события можно подготовить текст для свойств DisplayText и Text. OnSetText – возникает при записи данных из параметра Text в свойство Text. OnValidate – возникает после изменения значения но до записи в буфер. Этот обработчик удобно использовать для проверки на правильность введенных данных.
Обращение к значению поля К значению поля можно обратиться через свойства Value или AsXXXX, например: Edit1.Text := FoodTFName.Value; Edit1.Text := FoodTFName.AsString;
Применение свойства AsXXXX приводит к преобразованию значения в нужный тип. Разумеется, типы должны быть совместимыми. Например, целое число можно преобразовать в вещественное, но не наоборот. Если вы не вызывали редактор полей и не создавали для набора данных ни одного объекта-поля, то значение поля можно получить через свойство FieldByName, например: FoofT.FieldByName('FName').AsString := Edit1.Text;
Кроме того, доступ к значению поля можно получить через свойства набора данных Fields или FieldValues: FoodT.Fields[1].AsString := Edit1.Text; FoodT.FieldValues['FName'] := Edit1.Text;
Как уже упоминалось, свойство FieldValues в наборах данных применяется по умолчанию, так что последний пример можно записать и так: 49
FoodT['FName'] := Edit1.Text;
Если для доступа к полю вы используете свойство Fields, имейте в виду, что индексация полей начинается с 0, то есть индекс 1 соответствует второму полю набора данных. Свойство FieldValues обладает еще одной особенностью: оно имеет вариантный тип и позволяет использование списка полей, таким образом, единственным оператором можно записать сразу несколько полей: var v : Variant; begin //создаем вариантный массив: v := VarArrayCreate([0, 2], varVariant); //читаем значения полей: v := FoodT['FName;FType;FCena']; Edit1.Text := v[0]; Edit2.Text := v[1]; Edit3.Text := v[2];
50
Лекция 7. Запросы Запросы (TQuery, TADOQuery) Запросы (TQuery, TADOQuery) – это такие же наборы данных, как и таблицы (TTable, TADOTable). Запросы, как и таблицы, происходят от общего предка – TDBDataSet, в связи с этим они имеют схожие свойства, методы и события. Но имеются и существенные различия. Прежде всего, если табличный набор данных TTable (TADOTable) получает точную копию данных из таблицы базы данных, то запрос TQuery (TADOQuery) получает этот набор, основываясь на запросе, сделанном на специальном языке SQL (Structured Query Language – Язык Структурированных Запросов). С помощью этого языка программист создает запрос, который передается параметру TQuery.SQL (TADOQuery.SQL). При открытии набора данных этот запрос обрабатывается используемым механизмом BDE, ADO или др. и в набор данных передаются запрошенные данные. Заметили разницу? Не копия таблицы, а именно запрошенные данные, причем в указанном порядке! Используя запросы, в одном наборе данных можно получить взаимосвязанные данные из разных физических таблиц. Отпадает надобность в подстановочных полях. Имеется два варианта работы с SQL-запросами. В первом случае, SQL-запрос запрашивает нужные данные из таблицы (таблиц) базы данных. При этом формируется временная таблица, созданная в каталоге запуска программы, и компонент-запрос становится ее владельцем. Работа с такими данными очень быстрая, но пользователь при этом не может изменять данные, он лишь просматривает их. Такой подход идеален для составления отчетности. Если же пользователю требуется вносить изменения в таблицу (таблицы), то с помощью специальных операторов SQL (INSERT, UPDATE, DELETE) формируется запрос, уведомляющий механизм доступа к данным изменить данные БД. В этом случае никаких временных таблиц не создается. Запрос передается механизму доступа, обрабатывается им, выполняются изменения, и механизм доступа уведомляет программу о благополучном (или нет) изменении данных. Сравним табличные наборы данных с запросами. При работе с локальными или файл-серверными БД, табличные наборы данных имеют преимущество в скорости доступа к данным, поскольку запросы создают и используют для этого временные таблицы, а табличные НД напрямую обращаются к физическим таблицам БД. Однако при этом, запросы позволяют формировать гибкие наборы данных, которые невозможно было бы получить с помощью табличных НД. При работе в архитектуре клиент-сервер, всякое преимущество табличных наборов данных пропадает. Ведь они должны получить точную копию запрошенной таблицы, в которой могут быть десятки и сотни тысяч записей. Если учесть, что все эти данные передаются по сети, и передаются не одному, а множеству клиентов, то мы получим очень медленную систему, постоянно перегружающую сеть.
Компонент TADOQuery Для демонстрации работы компонента TADOQuery создадим совсем маленькое приложение – простейший SQL-монитор (у Delphi имеется встроенный SQL-монитор, но ведь всегда приятно сделать что-то своими руками!). Итак, создайте папку для нового приложения. В эту папку скопируйте базу данных ok.mdb, с которой мы работали в четвертой лекции. Если вы еще помните, там у нас имеется четыре таблицы, предназначенные для программы отдела кадров. Создайте новый проект в Delphi, форму переименуйте в fMain, сохраните ее модуль под именем Main, а проект в целом как SQLMon. В свойстве Caption формы пропишите «Простой SQL-монитор». Далее на форму установите панель. В свойстве Align панели выберите alTop, чтобы панель заняла весь верх, а ее высоту растяните примерно на полформы. Очистите свойство Caption. На эту панель установите компонент Memo, именно в нем мы будем писать наши SQL-запросы. Дважды щелкните по свойству Lines этого компонента, чтобы вызвать редактор текста, и очистите весь текст. Также не помешает дважды щелкнуть по свойству Font и изменить размер шрифта на 12 для лучшего восприятия текста. В свойстве Align выберите alLeft, чтобы компонент Memo занял всю левую часть панели. В правой части панели установите две простые кнопки и компонент TDBNavigator с вкладки Data Controls панели инструментов. Для улучшения внешнего вида интерфейса ширину кнопок сделайте 51
такой же, как у навигатора базы данных. В свойстве Caption первой кнопки напишите «Выполнить SQLзапрос», на второй кнопке напишите «Очистить компонент Memo». Собственно, мы могли бы очищать Memo сразу при выполнении SQL-запроса, и обойтись без второй кнопки. Но многие запросы похожи, и проще изменить часть текста запроса, чем писать весь запрос заново. На нижнюю, свободную половину формы установите компонент TDBGrid с вкладки Data Controls для отображения данных. В свойстве Align сетки выберите alClient, чтобы сетка заняла все оставшееся место. У вас должна получиться такая картина:
Рис. 7.1. Внешний вид приложения. Еще нам потребуются три компонента: TADOConnection и TADOQuery с вкладки ADO для получения набора данных, и TDataSource с вкладки Data Access для связи сетки DBGrid и навигатора DBNavigator с этим набором данных. Дважды щелкните по ADOConnection1, чтобы вызвать редактор подключений. Нажмите кнопку «Build», выберите поставщика Microsoft Jet 4.0 OLE DB Provider, и нажмите «Далее». В поле «Выберите или введите имя базы данных» укажите нашу БД ok.mdb и нажмите «ОК». И еще раз «ОК», чтобы закрыть окно редактора подключений. Сразу же свойство LoginPrompt переводим в False, чтобы при каждом запуске программы у нас не запрашивался логин и пароль, а Connected в True. Подключение к базе данных произошло. В свойстве Connection компонента TADOQuery выберем ADOConnection1, а в свойстве DataSet компонента DataSource1 выберем наш НД ADOQuery1. Теперь набор данных ADOQuery1 соединен с базой данных, а DataSource1 – с этим набором данных. В свойстве DataSource компонентов DBGrid1 и DBNavigator1 выберем DataSource1, чтобы они могли взаимодействовать с набором данных. Нам осталось лишь запрограммировать обработчик события onClick для обеих кнопок. Щелкните дважды по кнопке «Выполнить SQL-запрос», чтобы сгенерировать это событие, и пропишите в нем такой код: //проверим - есть ли текст в Memo. Если нет, выходим: if Memo1.Text = '' then begin ShowMessage('Вначале введите запрос!'); Memo1.SetFocus; Exit;
52
end; //текст есть. Очистим предыдущий запрос в наборе данных: ADOQuery1.SQL.Clear; //добавим новый запрос из Memo: ADOQuery1.SQL.Add(Memo1.Text); //открываем набор данных, т.е. выполняем запрос: ADOQuery1.Open;
Комментарии здесь достаточно подробны, чтобы разобраться в происходящем. Заметим только, что набор данных ADOQuery1 обычно закрыт. После того, как мы изменяем его свойство SQL, прописывая туда новый SQL-запрос, этот набор данных открывается. В результате в БД передается SQL-запрос, получаются запрашиваемые данные, которые формируют набор данных ADOQuery1. Когда этот компонент активен, данные доступны. Можно также заполнить свойство SQL, дважды щелкнув по нему и открыв редактор запросов, и сделать активным во время проектирования программы. Тогда данные становятся доступны сразу. Такой подход удобен, когда программист не собирается в дальнейшем менять SQL-запрос этого набора данных. Однако чаще бывает наоборот – в зависимости от ситуации, используется то один, то другой запрос в одном и том же наборе данных. Так мы поступаем и в нашем примере – передача SQL-запроса и открытие набора данных мы будем делать программно. Еще мы можем заметить, что свойство SQL набора данных TADOQuery имеет тип TStrings, так же, как свойство Lines компонента Memo или свойство Items компонента ListBox. То есть, в свойстве SQL мы можем использовать все преимущества, которые нам дает тип TStrings, например, загрузка SQLзапроса из внешнего файла: ADOQuery1.SQL.LoadFromFile('c:\myfile.sql');
Подобный прием нередко используется программистами, когда нужно сделать программу более гибкой. Формируя файл с SQL-запросами можно получать различные наборы данных, в зависимости от обстоятельств. Но в нашей программе мы будем получать SQL-запрос из поля Memo. Поскольку тип TStrings используется и в Memo, и в ADOQuery, то следующие строки кода аналогичны, они одинаково сформируют SQL-запрос на основе текста в поле Memo: ADOQuery1.SQL.Add(Memo1.Text); ADOQuery1.SQL := Memo1.Lines;
Сгенерируйте событие нажатия на вторую кнопку, здесь мы должны просто очистить поле текста Memo1, и код совсем прост: Memo1.Clear;
Вот и вся программа! Сохраните ее, скомпилируйте и запустите программу на выполнение. В поле Memo впишите следующие строки: SELECT * FROM LichData; После этого нажмите кнопку «Выполнить SQL-запрос». В сетке DBGrid отобразятся данные, которые представляют собой точную копию таблицы LichData из базы данных ok.mdb. Строки в примере написаны по правилам и рекомендациям языка SQL, то есть, операторы пишутся заглавными буквами, каждый оператор на отдельной строке, а в конце ставится точка с запятой. Однако рекомендации можно нарушать, а правила в Delphi более мягкие. Так, мы можем написать весь текст маленькими буквами, в одну строку, не ставить точку с запятой и не обращать внимания на регистр букв: select * from lichdata
53
Запрос все равно будет выполнен. Однако лучше придерживаться рекомендаций и традиционного синтаксиса SQL, ведь этот язык имеет стандарты, и вы можете применять его не только при работе с Delphi. В других языках программирования или в клиент-серверных СУБД правила могут несколько отличаться, но в любом случае запрос, написанный в стандартном стиле, будет выполнен. Поэтому лучше сразу приучать себя к стандартному синтаксису. На данном курсе мы будем придерживаться рекомендаций SQL. Что же написано у нас в этом запросе? Оператор SELECT означает «выделить», звездочка означает «все поля», оператор FROM означает «из…». Таким образом, запрос означает: ВЫДЕЛИТЬ все поля ИЗ таблицы LichData Но такой запрос ничем не отличается от применения табличных компонентов, а ведь мы можем создавать и гораздо более сложные запросы! Предположим, нам нужно получить фамилию, имя и отчество сотрудника, а также город его проживания. Основные данные находятся в таблице LichData, а вот город находится в таблице Adres, связанной с таблицей LichData релятивной связью один-к-одному по полю «Ключ» таблицы LichData, и по полю «Сотрудник» таблицы Adres. В этом случае запрос будет выглядеть так: SELECT Фамилия, Имя, Отчество, Город FROM LichData, Adres WHERE Ключ = Сотрудник; Как видите, в операторе SELECT поля перечисляются через запятую. Также через запятую перечисляются используемые таблицы в операторе FROM. А вот оператор WHERE указывает, что нужны только те записи, в которых значения поля «Ключ» и «Сотрудник» равны. Если бы мы не использовали оператор WHERE, то получили бы кучу недостоверных записей, где к каждой записи одной таблицы добавлялись бы все записи другой. Оператор WHERE позволил нам получить связные данные, в которых к одной записи первой таблицы добавляется соответствующая запись из другой таблицы. С этими и другими операторами мы подробней познакомимся на следующей лекции. Теперь предположим, что в одном наборе данных нам нужно получить записи из двух таблиц, связанных релятивной связью один-ко-многим. Так, у одного сотрудника может быть несколько телефонов. В этом случае придется смириться, что некоторые данные будут продублированы. Например, запрос: SELECT Фамилия, Имя, Телефон FROM LichData, Telephones WHERE Ключ = Сотрудник; выдаст нам набор данных, в котором фамилия и имя сотрудника будут дублироваться для каждого номера его телефона. Компонент-запрос может формировать набор данных двух типов: изменяемый, в котором пользователь может менять (редактировать, удалять или добавлять) записи, и не изменяемый, предназначенный только для просмотра данных или для составления отчетности. Возможность получения «живого» набора данных зависит от разных факторов – от применяемого оператора, от механизма доступа к данным, от используемой клиент-серверной СУБД. В данном примере мы используем оператор SELECT, работаем с локальной БД посредством механизма ADO. Если вы воспользуетесь навигатором, то убедитесь, что записи можно добавлять и удалять, а в сетке DBGrid их можно редактировать. Однако при редактировании данных, полученных более чем из одной таблицы, могут возникнуть трудности. Зато набор данных из одной таблицы можно спокойно изменять. Подробней с «живыми» и неизменяемыми наборами данных мы познакомимся позднее. Компонент TQuery/TADOQuery может выполнять запросы двумя разными способами. Вначале в свойство SQL компонента помещается необходимый запрос. Это можно сделать программно, как в 54
нашем SQL-мониторе, так и на этапе проектирования приложения. Дальнейшие действия зависят от того, какой запрос нам нужно выполнить. Если это запрос на получение набора данных, то есть, оператор SELECT, то достаточно просто открыть TQuery/TADOQuery методом Open, или присвоив True свойству Active. Если же запрос должен модифицировать данные, то есть, используются такие операторы, как INSERT, UPDATE, DELETE, то тогда запрос выполняется методом ExecSQL. С работой компонента-запроса TQuery (TADOQuery) мы поработали на практике. Как и табличные компоненты, компонент-запрос произошел от родительского класса TDBDataSet. Унаследовав его свойства, методы и события, он имеет и собственные, отличительные черты. Так, например, запрос может быть изменяемым (живым), при котором пользователь может модифицировать записи набора данных, и не изменяемым, при котором данные доступны только для просмотра и составления отчетности. Наиболее важные свойства, методы и события, отличные от TDBDataSet, рассматриваются ниже.
Свойства компонента-запроса Constrained – Свойство логического типа. Если свойство имеет значение True, то в изменяемом наборе данных на модифицируемые записи накладываются ограничения блока WHERE оператора SELECT (с операторами SQL-запросов вплотную познакомимся на следующей лекции). DataSource – Указывает тот компонент TDataSource, который используется для формирования параметрического запроса. Local – Свойство логического типа. Если свойство имеет значение True, это означает, что компонент-запрос работает с локальной или файл-серверной базой данных. ParamCheck – Логическое свойство. При значении True список параметров автоматически обновляется при каждом программном изменении SQL-запроса. Params – Свойство имеет тип TParams и содержит массив объектов-параметров этого типа. На этом типе данных следует остановиться подробнее: Таблица 7.1. Свойства и методы типа TParams Свойство Описание Items Содержит массив параметров типа TParams и является свойством «по умолчанию». Индексация массива начинается с 0. ParamValues() Открывает доступ к значению параметра по его имени, указанному в скобках. Count Количество параметров в массиве. Метод AddParam() CreateParam() FindParam() RemoveParam()
Описание Добавляет параметр в массив параметров. Создает параметр и добавляет его к массиву. Ищет параметр по его имени, указанному в скобках. Удаляет параметр из массива.
Prepared – Свойство логического типа. Содержит значение True, если SQL-запрос был подготовлен методом Prepare. RequestLive – Логическое свойство. Если компонент-запрос содержит изменяемый (живой) набор данных, то RequestLive содержит True. RowsAffected – Свойство содержит количество записей, которые были удалены отредактированы в наборе данных в результате выполнения SQL-запроса. 55
или
SQL – Свойство типа TStrings, то есть, набор строк. Содержит SQL-запрос, который выполняется, как только компонент-запрос становится активным (открывается). При изменении этого свойства, компонент-запрос автоматически закрывается, так что программисту требуется перевести свойство Active набора данных в True (или вызвать метод Open), чтобы запрос выполнился, и в НД появились запрошенные данные. Помещать строки запроса в свойство SQL можно как при проектировании, так и программно. В случае если программист создает запрос и открывает набор данных при проектировании приложения, он имеет возможность создать объекты-поля (см. предыдущую лекцию), и настраивать их свойства по своему усмотрению. При программном формировании НД такой возможности у него нет. UniDirectional – Свойство логического типа. Содержит True, если курсор набора данных может перемещаться только вперед (типы курсоров см. в лекции №4). Это свойство используется, в основном, при работе с клиент-серверными СУБД, не поддерживающими курсоры, которые могут двигаться как вперед, так и назад.
Методы компонента-запроса ExecSQL() – Выполняет модифицирующие запросы, то есть запросы на изменение, добавление или удаление записей, а также создание или удаление таблиц. В случае обычных запросов, выполненных с помощью оператора SELECT, используется метод Open, или присвоение значения True свойству Active набора данных. ParamByName() – Метод дает доступ к значению параметра по его имени, указанному в скобках. Prepare() – Метод используется для передачи SQL-запроса механизму доступа к данным, чтобы последний оптимизировал запрос. Оптимизация запроса происходит следующим образом: при выполнении любого запроса, механизм доступа к данным проверяет его синтаксис, что отнимает некоторое время. В случае многократного применения запроса, его можно выполнить методом Prepare(). При этом запрос компилируется и запоминается в буфере. При повторном выполнении этого запроса его синтаксис уже не проверяется. UnPrepare() – Этот метод отменяет результаты действия метода Prepare(), и освобождает буфер от хранения компилированного запроса.
56
Лекция 8. Краткий курс языка запросов SQL. SQL (Structured Query Language) – Это Язык Структурированных Запросов. Он не такой богатый, как языки программирования высокого уровня. Тем не менее, это язык, без владения которым программисту, работающему с базами данных, не обойтись. Запросы, написанные на SQL, часто называют скриптами. Как вы уже знаете, эти скрипты можно непосредственно вводить в свойство SQL компонента-запроса в момент проектирования приложения, а можно значение этого свойства менять и в процессе прогона программы. Однако нередко используют и третий способ: программист создает набор скриптовых файлов, в процессе работы программа считывает из них SQL-инструкции в компоненты запросов и выполняет их. Это простые текстовые файлы, созданные в любом редакторе текстов, например, стандартном Блокноте Windows. Расширение может быть любым, но традиционно используется *.sql. Все это позволяет создавать гибкие программы. Если организации, использующей ваше приложение, в дальнейшем потребуются какие-то новые возможности, например, им нужно дополнительно создать еще один отчет, то применение скриптовых файлов избавит вас от необходимости переделывать программу, для этого достаточно будет написать скрипт. В этой лекции мы разберем работу основных операторов SQL, после чего вы сможете создавать простые и сложные запросы и получать необходимые наборы данных. Тем, кто пожелает расширить свои познания SQL, рекомендую пройти соответствующий курс, посвященный этому языку, или прочитать книгу М. Грубера «Понимание SQL». Книга описывает стандартный синтаксис языка SQL и затрагивает все его возможности.
Команда SELECT Команда SELECT является основой запроса. Большинство SQL-запросов начинаются с нее. Множество других команд вкладываются в блок SELECT. Полный синтаксис этой команды таков: SELECT * | { [ DISTINCT | ALL] .,..} FROM {
[ ] }.,.. [ WHERE <predicate>] [ GROUP BY { | }.,..] [ HAVING <predicate>] [ ORDER BY { | }.,..];
Здесь используются следующие элементы: Таблица 8.1. Элементы команды SELECT
Элемент
<predicate>
Описание Выражение, которое производит значение. Оно может включать имена столбцов. Имя или синоним таблицы или представления Временный синоним для
, определенный в этой таблице и используемый только в этой команде. Условие, которое может быть верным или неверным для каждой строки или комбинации строк таблицы в предложении FROM. Имя столбца в таблице. Число с десятичной точкой. В этом случае, оно показывает в предложении SELECT с помощью идентификации его местоположения в этом предложении.
В простейшем случае применение команды SELECT выглядит так: SELECT * FROM Table_Name; 57
Звездочка указывает, что нужно показать все поля. Вместо звездочки можно указать конкретное поле или поля, разделяя их запятыми. Иногда бывает, что требуются данные из разных таблиц, которые имеют поля с одинаковым именем. В этом случае, перед именем полей указывают имя таблицы, или ее псевдоним, разделяя имена таблицы и поля точкой: SELECT Field1, Table1.Field2, Table2.Field2… FROM Table1, Table2; Команда FROM определяет имена таблиц, из которых осуществляется выборка данных. Если таблиц несколько, их имена разделяются запятыми. Иногда таблицы имеют длинные имена. В этом случае бывает выгодно использовать псевдонимы (alias) имен таблиц, указывая их через пробел после имени таблицы: SELECT Field1, f.Field2, s.Field2 FROM Table1 f, Table2 s;
Команда WHERE Команда WHERE позволяет использовать условие, которые может быть верным или нет для каждой записи НД. Если условие верное, то запись добавляется в набор данных, иначе отвергается. Давайте рассмотрим пример. Загрузите SQL-монитор из прошлой лекции. Предположим, нам нужно получить следующие данные на каждого сотрудника: Фамилия, Имя, Отдел, Должность. Пишем соответственный SQL-запрос: SELECT Фамилия, Имя, Отдел, Должность FROM LichData, Doljnost; Выполнив этот запрос, вы получите нечто непонятное. В полученном наборе данных всем сотрудникам подряд присваивается вначале первая должность, затем вторая, и так до конца. Другими словами, если у вас 10 сотрудников и 10 должностей, то вместо ожидаемых десяти записей вы получите 10 * 10 = 100 записей! Полученные данные называют недостоверными. Чтобы избежать этого, существует команда WHERE, которая позволяет задать условие выборки данных:
SELECT Фамилия, Имя, Отдел, Должность FROM LichData, Doljnost WHERE Ключ = Сотрудник; Теперь все в порядке, отдел и должность соответствуют каждому сотруднику. Мы указали, что нам нужны лишь те записи, значения которых в поле «Ключ» одной таблицы соответствуют значениям в поле «Сотрудник» другой таблицы. Полный синтаксис требует указания таблицы вместе с полем: WHERE LichData.Ключ = Doljnost.Сотрудник; Однако поскольку у нас в этих таблицах нет полей с похожими именами, мы можем воспользоваться упрощенным вариантом. И в том, и в другом случае мы получим одинаковый набор данных. Как и в любом условии, здесь можно применять различные операторы сравнения: Таблица 8.2. Операторы сравнения Оператор Описание = Равно > Больше < Меньше 58
>= <= <>
Больше или равно Меньше или равно Не равно
Кроме того, мы можем использовать логические операторы AND, OR и NOT, формируя более сложные запросы: SELECT Фамилия, Имя, Отдел, Должность FROM LichData, Doljnost WHERE (LichData.Ключ = Doljnost.Сотрудник) AND (Должность = "Бухгалтер"); Логические операторы имеют более высокий приоритет, поэтому в приведенном примере можно обойтись и без скобок. Данный запрос выдаст нам данные только на бухгалтеров. Как вы могли заметить, в отличие от Delphi, строка в SQL заключается не в одинарные, а в двойные кавычки! Однако SQL более демократичен, одинарные кавычки тоже принимаются. Обычно их используют, если внутри строки требуется указать кавычки, например, ‘Строка “в кавычках” будет отображена’. Еще следует заметить, что подобное связывание таблиц не требует наличия индексных полей. Но если такие поля есть, механизм доступа к данным будет их использовать для более эффективной выборки данных.
Команда ORDER BY Команда ORDER BY позволяет сортировать записи по определенному полю как в возрастающем, так и в убывающем порядке. Воспользуемся предыдущим примером, и отсортируем записи по полю «Фамилия»: SELECT Фамилия, Имя, Отдел, Должность FROM LichData, Doljnost WHERE Ключ = Сотрудник ORDER BY Фамилия; Как уже говорилось, мы можем сортировать данные как по возрастанию (ASC), так и по убыванию (DESC) значений. Сортировка по возрастанию установлена «по умолчанию», а вот чтобы сортировать записи по убыванию, после имени поля следует поставить служебное слово DESC: ORDER BY Фамилия DESC; Опять заметим, что для сортировки записей наличие индексных полей необязательно. Часто бывает, что нужно вывести данные из двух таблиц, имеющих связь один-ко-многим. При этом нужно сортировать данные не по одному, а по двум полям: SELECT Фамилия, Имя, Телефон, Примечание FROM LichData, Telephones WHERE Ключ = Сотрудник ORDER BY Фамилия, Телефон; В этом случае мы получим набор данных, в котором записи отсортированы вначале по фамилии сотрудника, затем по его номеру телефона:
Рис. 8.1. Двойная сортировка данных 59
Оператор IN Оператор IN позволяет определить набор значений. Предположим, нам нужны сотрудники, проживающие в городах Москва и Санкт-Петербург. Мы можем сформировать сложный запрос: SELECT Фамилия, Имя, Город FROM LichData, Adres WHERE Ключ = Сотрудник AND (Город = "Москва" OR Город = "Санкт-Петербург"); Последнюю строку запроса можно упростить, если использовать оператор IN: WHERE Ключ = Сотрудник AND Город IN ("Москва", "Санкт-Петербург"); Представьте, если нужны данные не по двум, а по десятку городов. В какого бы монстра превратился запрос со сложным условием, если не использовать IN! При перечислении строк можно использовать как двойные, так и одинарные кавычки, при перечислении числовых значений кавычки не нужны. Все значения разделяются запятой.
Оператор BEETWEEN Оператор BEETWEEN работает примерно так же, как IN, но задает не список, а диапазон значений. Предположим, нам нужно выявить сотрудников, которые имеют стаж работы от 4 до 10 лет включительно. Подобный запрос выглядит так: SELECT Фамилия, Имя, Стаж FROM LichData WHERE Стаж BETWEEN 4 AND 10;
Оператор LIKE Оператор LIKE работает только с символьными и строковыми полями. Этот оператор позволяет находить записи, имеющие заданную подстроку. Предположим, нам требуется вывести всех сотрудников, чья фамилия начинается на букву «Л». Запрос будет таким: SELECT Фамилия, Имя, Отчество FROM LichData WHERE Фамилия LIKE 'Л%'; Следует учитывать, что оператор LIKE чувствителен к регистру букв. Если вы будете производить поиск записи в программе при помощи SQL-запроса, позаботьтесь заранее привести буквы к нужному регистру. Оператор LIKE использует маску символов, что позволяет задавать довольно сложные условия. Маска может иметь два специальных символа: «_» - Символ подчеркивания обозначает, что в этом месте должен быть любой символ. Например, «м_р» может выводить такие слова, как «мир», «мор» или «мур», но не сможет вывести слово «мера». «%» - Символ процента обозначает, что в этом месте может быть любое количество любых символов. Например, маска '_и_и%' выведет такие фамилии, как Лисичкин, Синичкин, Милиев.
Агрегатные функции Агрегатные функции используются в запросах SQL, чтобы из группы записей сформировать одиночное значение одного поля. Имеются следующие агрегатные функции: 60
AVG – Функция возвращает среднее арифметическое значение из всех значений данного поля. Предположим, нам требуется выяснить средний стаж всех сотрудников предприятия. Такие данные могут быть сформированы следующим запросом: SELECT AVG (Стаж) FROM LichData; MAX – Функция возвращает максимальное значение указанного поля. Синтаксис аналогичен функции AVG. MIN – Функция возвращает минимальное значение указанного поля. Синтаксис аналогичен функции AVG. SUM – Функция возвращает максимальное значение указанного поля. Синтаксис аналогичен функции AVG. COUNT – Функция возвращает общее количество строк, сформированных запросом. В нашем случае это количество будет равно количеству сотрудников. Однако так называемые NULL-строки, то есть строки без значения, функция не учитывает. Если у какого-то сотрудника нет указания стажа его работы, то COUNT вернет меньшее количество, чем имеется сотрудников на предприятии. Синтаксис аналогичен функции AVG. С помощью агрегатных функций можно формировать более сложные значения. Предположим, у нас есть таблица покупателей. В ней указан покупатель, код приобретенного товара, его стоимость и количество. Если требуется, например, вычислить общую сумму проданного товара, то можно выполнить запрос: SELECT SUM (Стоимость * Количество) FROM Pokupateli; В скобках функции указаны нужные параметры. Вначале для каждой записи вычисляется значение, в нашем случае, значение поля «Стоимость» умножается на значение поля «Количество». Таким образом, мы находим общую сумму, которую заплатил данный покупатель. Обойдя все поля таблицы, получим все суммы всех покупателей, после чего функция SUM вернет нам общую сумму, полученную от всех покупателей. В выражении агрегатных функций может участвовать любое количество полей таблицы, а само выражение может быть сколь угодно сложным, содержать различные арифметические операции, иметь вложенные скобки и т.д.
Команда GROUP BY Команда GROUP BY позволяет группировать записи по какому-то определенному значению, и применяется совместно с агрегатными функциями. Предположим, нам требуется не просто получить средний стаж всех сотрудников, а еще и разбить эти данные по отделам. Вдруг директору приспичит узнать, в каком отделе у него работает больше всего молодых или старых специалистов! В нашу задачу входит выявить средний стаж сотрудников для каждого имеющегося отдела. Это мы можем выявить таким запросом: SELECT AVG(Стаж), Отдел FROM LichData, Doljnost WHERE Ключ = Сотрудник GROUP BY Отдел; Как видите, команда GROUP BY используется после команды WHERE и группирует записи по значению поля «Отдел». В результате получим таблицу из двух полей. Первое поле будет 61
сформировано агрегатной функцией, второе поле «Отдел» из таблицы Doljnost. Для каждого отдела будет рассчитано среднее значение поля «Стаж». Команда GROUP BY позволяет группировать записи не только по одному, но и по множеству полей. Предположим, одну и ту же должность могут иметь несколько человек (например, пять бухгалтеров в бухгалтерии). Нам требуется найти самого старого сотрудника не только по отделу, но и по занимаемой должности. Нас выручит следующий запрос: SELECT Отдел, Должность, MAX(Стаж) FROM LichData, Doljnost WHERE Ключ = Сотрудник GROUP BY Отдел, Должность; В результате мы получим набор данных, сгруппированный не только по отделам, но и по должностям. Если должность занимает только один человек, то мы получим его стаж. Если несколько – то наибольший из них.
Команда DISTINCT | ALL Команда DISTINCT (Отличие) предназначена для удаления избыточных (дублирующих) данных. Предположим, нам нужно получить список отделов на предприятии. Мы можем воспользоваться запросом:
SELECT Отдел FROM Doljnost; Однако поле «Отдел» не является уникальным в этой таблице, то есть, если в отделе работает десяток специалистов с разными должностями, то мы получим десяток повторяющихся записей. Команда DISTINCT позволяет убрать такие избыточные данные: SELECT DISTINCT Отдел FROM Doljnost; В результате мы получим все названия имеющихся отделов, но эти названия не будут повторяться. Обратной командой является ALL, принятая по умолчанию. Если вы не используете DISTINCT, то автоматически используется ALL (то есть, показываются все записи).
Команда HAVING Команда HAVING позволяет определить условия, чтобы удалить определенные группы из полученного набора данных, точно так же, как команда WHERE делает это для отдельных записей. Предположим, нам нужно получить максимальный стаж работы по каждой должности, как это мы делали выше, но при этом указать, что этот максимальный стаж должен быть более 7 лет. Следовательно, если на какой-то должности работают молодые сотрудники, имеющие меньший стаж работы, эта должность не будет приниматься. Для задания условия мы обычно используем команду WHERE, но в этой команде нельзя использовать агрегатные функции, формирующие значение из группы записей. Другими словами, запрос, подобный этому: SELECT Отдел, Должность, MAX(Стаж) FROM LichData, Doljnost WHERE Ключ = Сотрудник AND (MAX(Стаж) > 7) GROUP BY Отдел, Должность; вызовет ошибку. Использование в запросе команды HAVING решает эту проблему: 62
SELECT Отдел, Должность, MAX(Стаж) FROM LichData, Doljnost WHERE Ключ = Сотрудник GROUP BY Отдел, Должность HAVING MAX(Стаж) > 7; Мы изучили все основные команды SQL-запросов. На основе этих команд можно создавать запросы любой сложности. Однако еще раз заметим, что хорошие знания языка SQL позволят вам создавать более гибкие и мощные приложения, так что не ленитесь, изучайте SQL. Ведь вам наверняка придется создавать клиент-серверные базы данных, а в этой архитектуре вся работа с данными осуществляется только посредством SQL.
63
Лекция 9. Приемы создания и модификации таблиц программно. На прошлых лекциях мы изучили немало способов создания и обработки таблиц. В большинстве случаев, база данных и таблицы в ней проектируются и создаются заранее, например, такими утилитами, как Database Desktop, или СУБД MS Access. Однако иногда приходится создавать таблицы программно, то есть, не во время проектирования приложения, а во время его работы. Предположим, каждый пользователь программы должен иметь свою собственную таблицу в базе данных, или даже свою собственную БД. Сделать это заранее программист не может, ведь неизвестно, сколько пользователей на одном ПК будут работать с программой, и какие у них будут имена. К сожалению, в большинстве учебной литературы информация о программном создании таблиц или вовсе отсутствует, или очень скудна – описывается только какой то один способ создания только одного типа таблиц. А ведь программисту, в зависимости от обстоятельств, может понадобиться создание разного типа таблиц, разными способами. В данной лекции мы попробуем восполнить этот пробел и разберем три способа программного создания как простых, так и индексированных таблиц.
BDE. Простая таблица. Наиболее простой способ создания таблицы без индексов предлагает механизм доступа к данным BDE. Плюсы данного способа в простоте выполнения, в возможности создания таблиц как текстового типа, так и dBase, Paradox или FoxPro. Суть данного способа заключается в предварительном создании объектов-полей в редакторе полей компонента TTable. Это также означает, что еще на этапе проектирования можно настроить формат объектов-полей по своему усмотрению. Рассмотрим этот способ на примере. Создайте новое приложение. Форму как всегда назовите fMain, сохраните модуль под именем Main, а проект в целом назовите как угодно. На форму установите простую панель, очистите ее свойство Caption, а свойству Align присвойте значение alTop. В левой части панели установите рядом две простые кнопки, а в правой – компонент TDBNavigator с вкладки Data Controls Палитры компонентов. Ниже панели установите сетку TDBGrid, в ее свойстве Align выберите значение alClient. У кнопок измените свойство Caption: на первой кнопке напишите «Создать таблицу», на второй – «Открыть таблицу». Также нам потребуется еще четыре не визуальных компонента. Прямо на сетку, или в любое другое место установите компонент TTable с вкладки BDE, компонент TDataSource с вкладки Data Access, и компоненты TSaveDialog и TOpenDialog с вкладки Dialogs. Подготовим диалоговые компоненты. Выделите их и присвойте свойству Filter обоих компонентов строку Таблицы dBase|*.dbf Таким образом, мы указали, что диалоги будут работать только с таблицами типа dBase. Кроме того, у обоих диалогов измените свойство DefaultExt, указав там: dbf Это свойство указывает расширение файла по умолчанию, если пользователь не назначит расширения сам. В свойстве DataSet компонента DataSource1 выберите таблицу Table1. В свойстве DataSource сетки DBGrid1 и навигатора DBNavigator1 выберите имеющийся DataSource1. Теперь при открытии таблицы она будет отображаться в сетке, а навигатор позволит управлять ей. Теперь сложнее – настраиваем компонент Table1. Табличный компонент TTable имеет одно важное свойство TableType, с которым раньше нам не приходилось сталкиваться; компонент TADOTable такого свойства не имеет. Это свойство указывает на тип используемой или создаваемой таблицы. Свойство может иметь следующие значения:
64
Таблица 9.1. Значения свойства TableType компонента TTable Значение Описание Таблица содержится в формате обычного текстового файла. Строки и поля разделяются ttASCI специальными символами – разделителями. Имя файла таблицы имеет расширение *.TXT Таблица содержится в формате dBase, файл по умолчанию имеет расширение *.DBF ttDBase Компонент определяет тип таблицы по расширению имени файла таблицы. При создании ttDefault таблицы, если не указано расширение имени файла, принимается тип Paradox. Таблица содержится в формате FoxPro, файл по умолчанию также имеет расширение ttFoxPro *.DBF ttParadox Таблица содержится в формате Paradox, файл по умолчанию имеет расширение *.DB Если выбран тип таблицы (не ttDefault), то будет использован этот тип вне зависимости от расширения указанного имени файла таблицы. В свойстве TableType компонента Table1 выберите значение ttDBase, то есть, таблица будет работать только с типом dBase. Далее дважды щелкните по компоненту, открыв редактор полей. Нам нужно будет добавить запланированные ранее поля. Щелкните по редактору правой кнопкой, выберите команду New Field (Новое поле). В поле Name впишите имя поля, например, FCeloe. В поле Type выберите тип поля Integer. В поле Size нужно указывать размер поля, но это справедливо только для текстовых полей и полей типов Memo или BLOB. Убедитесь, что переключатель Field Type установлен на Data, это создаст пустое поле указанного типа. Нажав кнопку «ОК» добавьте объект-поле в редактор полей. Таким же образом создайте еще несколько разнотипных полей. Каждому полю присвойте уникальное имя (ведь в таблице не может быть двух полей с одинаковым именем!). Важно, чтобы вы добавляли только те типы полей, которые поддерживаются выбранным типом таблиц, в нашем случае это dBase. При добавлении типа Memo укажите размер от 1 до 255, например, 50. В этом случае в файле таблицы *.dbf будет сохранен текст поля в 50 символов. Текст, который не уместится в этот размер, будет сохранен в файле Memo с таким же именем, но с расширением *.dbt. Делать табличный компонент активным на этапе проектирования не нужно. Итак, не имея базы данных, не имея физической таблицы, мы заранее установили тип таблицы и нужные нам поля. Как вы, наверное, догадываетесь, мы также имеем возможность сразу настроить нужные нам форматы для каждого поля, изменяя такие его свойства, как DisplayFormat, EditMask, DisplayLabel и др. Далее нам осталось непосредственно создать и открыть таблицу. Дважды щелкните по кнопке «Создать таблицу», сгенерировав для нее событие. В процедуру этого события впишите код: //если пользователь не выбрал таблицу, выходим: if not SaveDialog1.Execute then Exit; //закроем таблицу, если вдруг уже есть открытая: Table1.Close; //вначале устанавливаем адрес базы данных: Table1.DatabaseName := ExtractFilePath(SaveDialog1.FileName); //теперь устанавливаем имя таблицы: Table1.TableName := SaveDialog1.FileName; //физически создаем таблицу: Table1.CreateTable; //и открываем ее: Table1.Open; //запишем имя открытой таблицы: fMain.Caption := 'Таблица - '+ Table1.TableName;
Комментарии к каждой строке достаточно подробны, чтобы вы самостоятельно разобрались с кодом. Метод CreateTable() компонента-таблицы создает файл таблицы, и дополнительные файлы (Memo, индексные), если они нужны. В свойстве DatabaseName табличного компонента вы можете установить любой необходимый вам адрес, мы использовали папку, выбранную диалогом SaveDialog. Для кнопки «Открыть таблицу» код будет почти таким же:
65
//если пользователь не выбрал таблицу, выходим: if not OpenDialog1.Execute then Exit; //закроем таблицу, если вдруг уже есть открытая: Table1.Close; //вначале устанавливаем адрес базы данных: Table1.DatabaseName := ExtractFilePath(OpenDialog1.FileName); //теперь устанавливаем имя таблицы: Table1.TableName := OpenDialog1.FileName; //открываем таблицу: Table1.Open; //запишем имя открытой таблицы: fMain.Caption := 'Таблица - '+ Table1.TableName;
Откомпилировав программу и поработав с ней, вы обнаружите, что можете создавать и открывать сколь угодно много таблиц программно. При этом на каждую таблицу создается по два файла (если вы используете поле Memo). Попробуйте таким же образом создать таблицу типа Paradox.
BDE. Таблица с ключом и индексами. В задачу данного раздела входит создание таблицы Paradox с различными типами полей, с первичным ключом и индексами по текстовому полю как в возрастающем, так и в убывающем порядке. Редактор полей компонента TTable при этом вызывать не нужно, добавлять поля мы тоже будем программно. В целях экономии места проектирование формы приложения не описывается – это несложная задача. Вы можете создать главную форму такой же, как в предыдущем примере, только кнопка там будет одна. При нажатии на эту кнопку мы должны открыть таблицу, если она существует, или создать и открыть новую таблицу. Располагаться таблица должна в той же папке, откуда запущено приложение. Файл с таблицей Paradox назовем Proba.db, файлы с Memo и индексные файлы сгенерируются автоматически, также с именем Proba, но с разными расширениями. На форму добавьте компонент TTable с вкладки BDE, свойству Name которого присвойте значение TMy (вместо Table1), а свойству TableType значение ttParadox. Если у вас в приложении есть сетка DBGrid и (или) навигатор DBNavigator, то добавьте также компонент DataSource, который необходимо подключить к таблице TMy, а сетку и навигатор – подключить к DataSource. Здесь следует иметь в виду одну деталь: описание методов создания полей и индексов хранится в модуле DBTables, который подключается к вашей форме сразу, как вы установите компонент TTable. Если же вы используете модуль данных, и устанавливаете табличный компонент там, то и создавать таблицу нужно тоже в этом модуле, а в главной форме лишь вызывать процедуру создания таблицы. Но в нашем простом примере модуля данных нет, модуль DBTables указан в разделе Uses главной формы, и никаких проблем возникнуть не должно. Код нажатия на кнопку выглядит так: {Если таблицы нет - создаем и открываем ее, если естьпросто открываем} procedure TfMain.Button1Click(Sender: TObject); begin //если таблица есть - открываем ее и выходим: if FileExists(ExtractFilePath(Application.ExeName) + 'Proba.db') then begin TMy.DatabaseName := ExtractFilePath(Application.ExeName); TMy.TableName := 'Proba.db'; TMy.Open; Exit; end; //if
{Если дошли до этого кода, значит таблицы еще нет. Указываем данные таблицы:} TMy.DatabaseName := ExtractFilePath(Application.ExeName); TMy.TableType := ttParadox; TMy.TableName := 'Proba';
66
{Создаем поля:} with TMy.FieldDefs do begin //вначале очистим: Clear; //добавляем поле-счетчик типа автоинкремент: with AddFieldDef do begin Name := 'Key'; DataType := ftAutoInc; Required := True; end; //with //добавляем текстовое поле: with AddFieldDef do begin Name := 'Name'; DataType := ftString; Size := 30; end; //with //добавляем поле дата: with AddFieldDef do begin Name := 'Date'; DataType := ftDate; end; //with //добавляем логическое поле: with AddFieldDef do begin Name := 'MyLog'; DataType := ftBoolean; end; //with //добавляем целое поле: with AddFieldDef do begin Name := 'MyInt'; DataType := ftInteger; end; //with //добавляем вещественное поле: with AddFieldDef do begin Name := 'MyReal'; DataType := ftFloat; end; //with //добавляем денежное поле: with AddFieldDef do begin Name := 'MyCurr'; DataType := ftCurrency; end; //with //добавляем поле Memo: with AddFieldDef do begin Name := 'MyMemo'; DataType := ftMemo; Size := 20; end; //with end; //with {Создаем ключ и индексы:} with TMy.IndexDefs do begin Clear; //делаем первичный ключ: with AddIndexDef do begin Name := ''; Fields := 'Key'; Options := [ixPrimary]; end; //делаем индекс в возрастающем порядке: with AddIndexDef do begin Name := 'NameIndxASC'; Fields := 'Name'; Options := [ixCaseInsensitive]; end; //делаем индекс в убывающем порядке: with AddIndexDef do begin Name := 'NameIndxDESC';
Разберем приведенный код. Первый блок выполняет проверку на наличие таблицы. Таблица ищется в папке, откуда была запущена программа. Если таблица найдена, то компоненту TMy присваиваются свойства DatabaseName (папка, где располагается таблица) и TableName (имя таблицы). В нашем случае таблица называется Proba.db, но вы можете усложнить программу, используя диалог OpenDialog, как в прошлом примере. В этом случае пользователь сможет выбрать не только имя таблицы, но и ее расположение. Далее таблица открывается, а оператор Exit досрочно завершает выполнение процедуры. Если выполнение процедуры продолжается, значит, таблица не была найдена. В этом случае мы заполняем свойства компонента-таблицы DatabaseName, TableType и TableName необходимыми значениями. Далее начинаем добавлять поля. Чтобы уменьшить код, мы используем оператор with. Напомню, что этот оператор создает блок кода, который относится к указанному в with объекту. Так, вместо with TMy.FieldDefs do begin Clear;
можно было бы написать TMy.FieldDefs.Clear;
В случае одиночного оператора это допустимо, но в случае множественных команд ссылаться в каждой строчке на объект будет утомительно. Свойство FieldDefs таблицы содержит описание полей этого набора данных. Таким образом, мы начинаем с того, что очищаем это описание. Далее у нас идет метод AddFieldDef, предназначенный для добавления поля в описание. Опять же, чтобы не ссылаться каждый раз на этот метод, мы используем вложенный оператор with для каждого добавляемого поля. В простейшем случае в блоке добавления нового поля требуется указать только два свойства объекта-поля: Name (имя поля) и DataType (тип поля). С именем все понятно, а что касается типа поля, то он определяется свойством DataType класса TField. Чтобы получить подробную справку по возможным типам полей, установите курсор в редакторе кода на слове DataType и нажмите , чтобы вызвать контекстную справку. В списке тем выберите ту тему, которая относится к классу TField, а в открывшейся справке щелкните по ссылке TFieldType (относится к Delphi 7, хотя возможно, имеется и в предыдущих версиях). Откроется страница с подробным описанием типов полей. При использовании этого метода следует сверяться, имеется ли выбранный тип поля в таблицах используемого формата. Помимо этих двух свойств, при необходимости могут использоваться и другие: Required – Логическое свойство. Если равно True, то значения поля должны быть уникальными (не могут повторяться). В нашем примере такое свойство имеется у поля, которое мы будем использовать как первичный ключ. Size – Указывает размер поля. Используется в основном, со строковыми и Memo- полями. После того, как в список полей были добавлены все необходимые поля, начинаем создание первичного ключа и индексов. Если за список полей отвечает свойство FieldDefs таблицы, то за список индексов отвечает свойство IndexDefs, а за добавление нового индекса – метод AddIndexDef. По 68
аналогии с полями, используем оператор with для уменьшения кода. Для каждого индекса требуется указать по три свойства: Name (имя индекса), Fields (имя поля, по которому строится индекс) и Options (параметры индекса). Параметры индекса указаны в таблице 9.2: Таблица 9.2. Параметры типов индекса Тип Описание Первичный индекс (ключ). Не применяется с таблицами типа dBase. ixPrimary Уникальный индекс. Значения этого поля не могут повторяться. ixUnique Индекс в убывающем (обратном) порядке. ixDescending Ключевой индекс для таблиц dBase. ixExpression ixCaseInsensitive Индекс, нечувствительный к регистру букв. ixNonMaintained Этот тип используется редко. Он подразумевает, что при редактировании пользователем значения индексируемого поля, индексный файл автоматически не обновляется. Как видно из примера, свойству Options можно присвоить не один параметр, а список параметров: Options := [ixCaseInsensitive, ixDescending];
Далее все просто: указав необходимые поля и индексы, методом CreateTable формируются физические файлы таблицы. Сама таблица имеет расширение *.db, файл с полем Memo - *.mb, остальные файлы содержат созданные индексы. Для сортировки данных используем индексы. У нас их два –'NameIndxASC' (в возрастающем порядке) и 'NameIndxDESC' (в убывающем порядке). Чтобы сортировать данные, например, в убывающем порядке, нужно указать имя соответствующего индекса в свойстве IndexName компонентатаблицы: TMy.IndexName := 'NameIndxDESC';
Если же мы хотим снять сортировку, то достаточно просто присвоить этому свойству пустую строку: TMy.IndexName := '';
Описываемый выше пример взят из справочника Delphi и приведен с небольшими доработками. Пример описывает практически все аспекты создания таблицы; по аналогии вы сможете создавать таблицы любой сложности.
ADO. Создание простой таблицы посредством запроса SQL Создание таблицы выполняется SQL-запросом CREATE TABLE. Тут есть одно но: дело в том, что существует два типа SQL-запросов. Запрос, который возвращает набор данных и начинается оператором SELECT, выполняется простым открытием компонента-запроса. При этом выполняется запрос, который содержится в свойстве SQL компонента. С модифицирующими командами дело обстоит иначе. Команда CREATE TABLE принадлежит к той части SQL, которая называется DDL (Data Definition Language) – Язык Определения Данных. Этот язык предназначен для изменения структуры базы данных. Команды INSERT, DELETE, UPDATE относятся к DML (Data Manipulation Language) – Язык Обработки Данных, предназначенный для модификации данных. Эти команды объединяет то, что они не возвращают результирующий набор данных. Чтобы выполнить эти команды, нужно присвоить соответствующий SQL-запрос свойству SQL, а затем вызвать метод ExecSQL. Синтаксис создания таблицы несложный: CREATE TABLE ( [<Size>], …) 69
Здесь, TableName – имя таблицы; ColumnName – имя столбца; DataType – тип данных и Size – размер, который указывается для некоторых типов данных, например, строки. Описания столбцов таблицы разделяются запятыми. В различных СУБД синтаксис и типы данных SQL могут отличаться. Поэтому запрос, прекрасно работающий в одной СУБД, может вызвать ошибку в другой. Чтобы избежать ошибок, рекомендуется везде использовать типы ANSI, являющиеся стандартом SQL. Увы, но этих типов очень немного. Рассмотрим их: Таблица 9.3. Типы ANSI Тип Описание CHAR (CHARACTER) Строковые типы данных. Обычно имеют размер до 255 символов. Требуют указания размера. TEXT Целое число. Размер не указывается. INT (INTEGER) Короткое целое. Размер не указывается. SMALLINT Вещественные числа. Размер не указывается. FLOAT REAL Как видите, многих типов просто нет. Вместо логического типа, вероятно, придется использовать строковый тип с размером в один символ; при этом ‘Y’ или ‘1’ будут означать истину, а ‘N’ или ‘0’ – ложь. Программисту придется самостоятельно делать проверку на это значение. Нет типа Memo. Нет автоинкрементного типа. Однако стандартные типы непременно будут корректно работать в любой СУБД. Ниже приведен пример создания и открытия простой таблицы. В приложении должен иметься компонент ADOQuery, а если есть сетка и навигатор, то и DBSource. Для подключения к нужному провайдеру данных желательно использовать компонент TADOConnection. В его свойство ConnectionString нужно прописать строку подключения, например: Provider=MSDASQL.1;Persist Security Info=False;Data Source=Файлы dBASE Эту строку можно ввести программно, или создать подключение при проектировании (я так и сделал). Поставщик данных в примере оставлен по умолчанию – Microsoft OLE DB Provider for ODBC Drivers, а в качестве источника данных (вкладка «Подключение» редактора связей TADOConnection) используются файлы dBase. Не следует забывать и про свойство LoginPrompt, которое следует переводить в False, чтобы программа не запрашивала имя пользователя и пароль при каждом подключении. А также нужно сразу открыть TADOConnection, установив его свойство Connected в True. В свойстве Connection компонента TADOQuery следует выбрать ADOConnection1. Пример реализован, как событие нажатия на кнопку: procedure TfMain.Button1Click(Sender: TObject); var s: String; begin {Создаем текст запроса} s := 'CREATE TABLE MyTab(Key1 INT, Name CHAR(20), '+ ' MyFloat FLOAT, MyDate DATE)'; {Создаем таблицу} ADOQuery1.SQL.Clear; ADOQuery1.SQL.Add(s); ADOQuery1.ExecSQL; {Открываем таблицу} ADOQuery1.SQL.Clear; ADOQuery1.SQL.Add('SELECT * FROM MyTab'); ADOQuery1.Open; end;
70
Как видите, создается четыре поля – целый тип, строковый размером 20 символов, вещественный и тип Дата. Последний тип не входит в стандартное описание ANSI-типов, тем не менее, работает в большинстве СУБД. Можете также поэкспериментировать и с типом BOOLEAN (Логический). Итак, в переменную s мы вносим строку записи SQL-запроса. Затем очищаем свойство SQL, на случай, если там уже имелся запрос. Далее этот запрос мы заносим в свойство SQL, и методом ExecSQL выполняем его. С открытием таблицы мы уже неоднократно сталкивались. В результате выполнения кода создается и открывается файл MyTab.dbf, который находится в той же папке, что и приложение.
71
Лекция 10. Сохранение древовидных структур в базе данных. Древовидные структуры не относятся напрямую к программированию баз данных, тем не менее, программисту нередко приходится «изобретать велосипед», придумывая различные решения сохранения таких структур в таблице, и обратное их считывание в дерево. Типичный пример дерева – всем знакомое дерево каталогов. Примеров таких структур множество – это могут быть отделы в каком-либо учреждении или разделы библиотеки. Посмотрим на рисунок с фрагментом дерева разделов библиотеки:
Рис. 10.1. – Дерево разделов Основная сложность хранения деревьев в таблице – это то, что мы не знаем заранее, какова будет глубина вложенности разделов. Можно было бы создать таблицу с 10 полями, например. Но если вложенных разделов будет меньше, то таблица будет неэффективна – останется много пустых полей. А если больше – ограничивать пользователя? Самый простой способ сохранения структуры дерева и ее считывания обратно – воспользоваться тем, что дерево – это список узлов, и имеет хорошо знакомые нам методы: //сохраняем в файл: TreeView1.SaveToFile('myfile.txt'); //читаем из файла: TreeView1.LoadFromFile('myfile.txt');
Однако этот способ имеет массу недостатков. Во-первых, в результате получим простой текстовый файл, в котором вложенные узлы располагаются ниже родителя и имеют отступ. Пользователь легко может случайно или намеренно испортить такой файл, отредактировав или просто удалив его с диска, и программа будет работать с ошибками. Во-вторых, обычно древовидная структура тесно связана с другими данными, например, таблица отделов предприятия связана со служащими этого предприятия – запись каждого служащего имеет ссылку на отдел, где он работает. Если структуру предприятия хранить в простом текстовом файле, то такую связь сложно будет обеспечить. Когда программист впервые сталкивается с необходимостью хранения древовидных структур в базе данных, обычно он первым делом подключается к Интернету и ищет какой-нибудь компонент, который бы позволил это делать. Но не все нестандартные компоненты работают качественно, да и зачем искать какой-то новый компонент, когда имеется стандартный TreeView на вкладке Win32 Палитры компонентов? Именно с этим компонентом мы и будем работать в данной лекции. Рецептов работы с деревьями в базах данных много, мы рассмотрим лишь один из них, достаточно эффективный и в то же время простой. Смысл этого способа состоит в том, чтобы в каждой записи 72
таблицы сохранять номер узла раздела, номер его родителя, если он есть, и название узла. В случае если узел не имеет родителя (главный узел, например, «Художественная литература» в рисунке 10.1), то в соответствующее поле запишем ноль.
Подготовка проекта Для реализации примера нам потребуется новая база данных. Загрузите MS Access и создайте базу данных «TreeBD», а в ней таблицу «Razdels». Вообще-то, в базе данных MS Access как таблицы, так и поля могут иметь русские названия, однако мы будем использовать средства SQL, который не всегда корректно обрабатывает русские идентификаторы. Кроме того, данный способ можно использовать в любой СУБД, а далеко не все из них так предупредительны, как MS Access, поэтому название таблицы и ее полей выполним латиницей. Таблица «Razdels» будет иметь три поля:
№ 1 2 3
Таблица 10.1 Поля таблицы «Разделы» Имя поля Тип поля R_Num Счетчик R_Parent Числовой R_Name Текстовый
Дополнение Ключевое поле Целое Длина 50 символов
Созданную базу данных сохраните в папке, где будем разрабатывать наш проект (не забудьте сделать резервную копию пустой базы данных на всякий случай.). Далее создадим в Delphi новый проект и простую форму:
Рис. 10.2. Форма для работы с деревом Как всегда, назовите форму fMain, в свойстве Caption напишите «Реализация сохранения дерева в БД», модуль формы сохраните как Main, а проект в целом назовите, например, TreeToBD. Сделанная база данных TreeBD должна быть в той же папке, что и проект. Далее установите компонент TreeView (дерево) с вкладки Win32. Его свойству Align присвойте alLeft, чтобы дерево заняло весь левый край. Затем можете установить сплиттер – разделитель, ухватившись за который пользователь сможет менять ширину дерева. Компонент Splitter находится на вкладке Additional и его свойство Align по умолчанию равно alLeft – разделитель «прилепится» к правому краю дерева. 73
Правее установите сетку DBGrid с вкладки Data Controls, и его свойству Align присвойте alClient, чтобы сетка заняла все оставшееся место. Ни главное меню, ни панель инструментов нам здесь не потребуются, используем лишь два всплывающих PopupMenu – первый для дерева, второй для сетки (выберите соответствующие PopupMenu в свойстве PopupMenu этих компонентов). Далее с вкладки ADO нам потребуется компонент ADOConnection для соединения с базой данных, таблица ADOTable и запрос ADOQuery для вспомогательных нужд. С вкладки Data Access – компонент DataSource, для связи сетки с таблицей. Подключите ADOConnection к базе данных и откройте соединение (см. лекцию №2). Таблицу подключите к ADOConnection (свойство Connection), затем выберите в свойстве TableName нашу таблицу «Razdels», а свойство Name переименуйте в tRazdels – так будем обращаться к таблице. Для удобства отображения названия полей откройте редактор полей таблицы (дважды щелкнув по ней), добавьте все поля и у каждого поля измените свойство DisplayLabel, соответственно, на «№», «Родитель» и «Название». Не забудьте открыть таблицу. Компонент DataSource подключите к tRazdels, а сетку – к DataSource, в сетке должны отобразиться поля. Кроме того, переименуйте свойство Name запроса ADOQuery1 в Q1, ведь нам часто придется обращаться к нему по имени. Запрос также подключите к ADOConnection, но делать его активным не нужно. На этом приготовления закончены.
Создание и сохранение в таблицу дерева разделов Работа с деревьями состоит из двух этапов: 1) Сохранение дерева в таблицу. 2) Считывание дерева из таблицы. В этом разделе лекции разберем первый этап. Щелкните дважды по компоненту PopupMenu1, который «привязан» к дереву, и создайте в нем следующие разделы:
Создать главный раздел Добавить подраздел к выделенному Переименовать выделенный Удалить выделенный Свернуть дерево Развернуть дерево
Все эти команды относятся к работе с разделами дерева. Прежде всего, создадим обработчик для команды «Создать главный раздел». Листинг процедуры смотрите ниже: {Создать главный раздел} procedure TfMain.N1Click(Sender: TObject); var s: String; //для получения имени раздела (подраздела) NewRazd: TTreeNode; //для создания нового узла дерева begin //вначале очистим s s:= ''; //Получим в s имя нового раздела: if not InputQuery('Ввод имени раздела', 'Введите заголовок раздела:', s) then Exit; //снимаем возможное выделение у дерева: TreeView1.Selected:= nil; //создаем главный раздел (ветвь): NewRazd:= TreeView1.Items.Add(TreeView1.Selected, s); //Сразу же сохраняем его в базу: tRazdels.Append; //добавляем запись tRazdels['R_Parent']:= 0; //не имеет родителя //присваиваем значение созданного раздела:
74
tRazdels['R_Name']:= NewRazd.Text; //сохраняем изменения в базе: tRazdels.Post; end;
Разберем код. Переменная NewRazd имеет тип TTreeNode, к которому относятся все разделы и подразделы (узлы) дерева. В текстовую переменную s с помощью функции InputQuery() мы получаем имя нового главного узла. Функция имеет три строковых параметра: 1) Заголовок окна. 2) Пояснительная строка. 3) Переменная, куда будет записан введенный пользователем текст. Если переменная, передаваемая в качестве третьего параметра, пуста, то поле ввода будет пустым. Если же в ней содержался текст – он будет выведен как текст «по умолчанию». Функция возвращает True, если пользователь ввел (или изменил) текст, и False в противном случае. В результате работы функции для пользователя будет выведено простое окно с запросом:
Рис. 10.3. Окно функции InputQuery() Далее строкой TreeView1.Selected:= nil;
мы снимаем выделение, если какой либо раздел был выделен, ведь мы создаем главный раздел, не имеющий родителя. Свойство Selected компонента TreeView указывает на выделенный узел и позволяет производить с ним различные действия, например, получить текст узла: TreeView1.Selected.Text;
А присваиваемое значение nil (ничто) снимает всякое выделение, если таковое было. Далее мы создаем сам узел: NewRazd:= TreeView1.Items.Add(TreeView1.Selected, s);
Разберем эту строку подробней. Переменная NewRazd – это новый узел дерева. Каждый узел – объект, обладающий своими свойствами и методами. Все узлы хранятся в списке – свойстве Items дерева TreeView, а метод Add() этого свойства позволяет добавить новый узел. У метода два параметра – выделенный узел (у нас он равен nil) и строка текста, которая будет присвоена новому узлу. Таким образом, в дереве появляется новый главный узел. Затем мы сохраняем его в базу данных, предварительно добавив в таблицу новую запись: tRazdels.Append; //добавляем запись tRazdels['R_Parent']:= 0; //не имеет родителя //присваиваем значение созданного раздела: tRazdels['R_Name']:= NewRazd.Text; //сохраняем изменения в базе: tRazdels.Post;
75
Вы помните, что такие методы, как Append или Insert автоматически переводят таблицу в режим редактирования, поэтому вызывать метод Edit излишне? Обратите внимание на то, что мы сохраняем ноль в поле «R_Parent», так как это – главный раздел, не имеющий родителя. Свойство Text нового узла NewRazd содержит название нового узла, которое мы присваиваем полю «R_Name». Далее сгенерируем процедуру для команды меню «Добавить подраздел к выделенному»: {Добавить подраздел к выделенному разделу(подразделу)} procedure TfMain.N2Click(Sender: TObject); var s: String; //для получения имени раздела (подраздела) z: String; //для формирования заголовка окна NewRazd: TTreeNode; //для создания нового узла дерева begin //Проверим - есть ли выделенный раздел? //Если нет - выходим: if TreeView1.Selected = nil then Exit; //вначале очистим s s:= ''; //сформируем заголовок окна запроса: z:= 'Раздел "' + TreeView1.Selected.Text + '"'; //Получим в s имя нового раздела: if not InputQuery(PChar(z), 'Введите заголовок подраздела:', s) then Exit; //создаем подраздел: NewRazd:= TreeView1.Items.AddChild(TreeView1.Selected, s); //перед сохранением подраздела в базу, прежде получим //номер его родителя: Q1.SQL.Clear; Q1.SQL.Add('select * from Razdels where R_Name='''+ NewRazd.Parent.Text+''''); Q1.Open; //Теперь сохраняем его в базу: tRazdels.Append; //добавляем запись //присваиваем № родителя: tRazdels['R_Parent']:= Q1['R_Num']; //присваиваем название узла: tRazdels['R_Name']:= NewRazd.Text; //сохраняем изменения в базе: tRazdels.Post; end;
Код этой процедуры очень похож на код предыдущей, но есть и отличия. Прежде всего, мы проверяем – а имеется ли выделенный раздел? Ведь фокус ввода мог быть и на сетке DBGrid, когда пользователь щелкнул правой кнопкой по дереву, и выбрал эту команду. В этом случае, если не делать проверки, мы получим ошибку, пытаясь добавить дочерний узел к пустоте. Далее, мы ввели строковую переменную z, чтобы сформировать запрос. Ведь пользователю будет удобней, если в окне InputQuery() он сразу увидит, к какому именно разделу он добавляет подраздел. Затем, при добавлении дочернего узла вместо метода Add() мы используем метод AddChild(). Ну и, наконец, при сохранении узла в таблицу мы записываем не только созданный узел, но и номер его родителя, получив его с помощью запроса Q1.SQL.Add('select * from Razdels where R_Name='''+ NewRazd.Parent.Text+'''');
Запрос формирует набор данных с единственной строкой – записью родителя добавляемого элемента. Поле Q1[‘R_Num’], как вы понимаете, хранит номер этого родителя в запросе. Код процедуры переименования выделенного раздела выглядит так: 76
{Переименовать выделенный раздел (подраздел)} procedure TfMain.N3Click(Sender: TObject); var s: String; //для получения имени раздела (подраздела) z: String; //для формирования заголовка окна begin //Проверим - есть ли выделенный раздел? //Если нет - выходим: if TreeView1.Selected = nil then Exit; //получаем текущий текст: s:= TreeView1.Selected.Text; //формируем заголовок: z:= 'Редактирование "' + s + '"'; //если не изменили, выходим: if not InputQuery(PChar(z), 'Введите новый заголовок:', s) then Exit; //находим эту запись в таблице, учитывая, что ее по каким то //причинам может и не быть: if not tRazdels.Locate('R_Name', TreeView1.Selected.Text, []) then begin ShowMessage('Ошибка! Указанный раздел не существует в таблице.'); Exit; end; //if //если до сих пор не вышли из процедуры, значит запись найдена, //и является текущей. изменяем ее: tRazdels.Edit; tRazdels['R_Name']:= s; tRazdels.Post; //теперь меняем текст выделенного узла: TreeView1.Selected.Text := s; end;
Здесь комментарии достаточно подробны, чтобы вы разобрались с кодом. Следует обратить внимание на то, что вначале мы исправляем запись в таблице, и только потом – в узле. Если бы мы сначала исправили текст узла, как бы затем нашли старую запись в таблице? Пришлось бы вводить дополнительную переменную для хранения старого текста. Удаляется выделенный узел еще проще: {Удалить выделенный раздел (подраздел)} procedure TfMain.N4Click(Sender: TObject); var s: String; //для строки запроса begin //Проверим - есть ли выделенный раздел? //Если нет - выходим: if TreeView1.Selected = nil then Exit; //иначе формируем строку запроса: s:= 'Удалить "' + TreeView1.Selected.Text + '"?'; //запросим подтверждение у пользователя: if Application.MessageBox(PChar(s), 'Внимание!', MB_YESNOCANCEL+MB_ICONQUESTION) <> IDYES then Exit; //если не вышли - пользователь желает удалить раздел. //найдем и удалим его вначале из таблицы: if tRazdels.Locate('R_Name', TreeView1.Selected.Text, []) then tRazdels.Delete; //теперь удаляем раздел из дерева: TreeView1.Items.Delete(TreeView1.Selected); end;
Далее нам осталось сгенерировать процедуры для сворачивания и разворачивания дерева. Делается это одной строкой: {свернуть дерево} TreeView1.FullCollapse;
77
{развернуть дерево} TreeView1.FullExpand;
Итак, метод FullCollapse дерева TreeView сворачивает его узлы, а метод FullExpand разворачивает. Теперь сохраните проект и скомпилируйте его. Попробуйте заполнить дерево разделами и подразделами, убедитесь, что параллельно данные сохраняются и в таблице.
Чтение древовидной структуры из таблицы Прежде всего, создадим пункты для второго всплывающего меню, которое «привязано» к сетке DBGrid. Пункты будут такими: Очистить дерево Заполнить дерево Для очищения дерева нам требуется просто очистить его свойство Items, делается это одной строкой: TreeView1.Items.Clear; Займемся заполнением дерева. Прежде разберемся с алгоритмом. Вначале нам потребуется считать из таблицы в дерево все узлы, не имеющие родителя (главные). Затем мы сделаем запрос, в котором получим пару «Родительский узел – Дочерний узел» всех подразделов. То есть, главные узлы будут отфильтрованы этим запросом. После чего нам останется пройти от первой до последней записи этого набора данных, добавляя дочерний узел к его родителю. Создайте обработчик команды «Заполнить дерево». Код обработчика будет таким: {Заполнить дерево} procedure TfMain.N10Click(Sender: TObject); begin //если таблица пуста, сразу выходим: if tRazdels.IsEmpty then Exit; //если в старом дереве есть узлы, очистим их: TreeView1.Items.Clear; //вначале запросим все главные узлы: Q1.SQL.Clear; Q1.SQL.Add('select * from Razdels where R_Parent=0'); Q1.Open; if Q1.IsEmpty then Exit; //если НД пуст, выходим. //теперь занесем их в дерево: while not Q1.Eof do begin TreeView1.Selected := nil; TreeView1.Items.Add(TreeView1.Selected, Q1.FieldByName('R_Name').AsString); Q1.Next; end; //while //делаем запрос, выводящий пару: Родительский узел - Дочерний узел //и поочередно прописываем их в дерево процедурой TreeViewAddChild: Q1.SQL.Clear; Q1.SQL.Append('select r.R_Name, d.R_Name '+ 'from Razdels r, Razdels d '+ 'where r.R_Num=d.R_Parent'); Q1.Open; if Q1.IsEmpty then Exit; //если нет вложенных узлов, выходим Q1.First;
78
while not Q1.Eof do begin TreeViewAddChild(Q1.Fields[0].AsString, Q1.Fields[1].AsString); Q1.Next; end; //while //распахиваем дерево: TreeView1.FullExpand; end;
Разберем этот код. В самом начале мы проверяем – не пуста ли таблица, и если это так, то выходим из процедуры, ничего не делая. Затем мы очищаем старые данные из дерева. Конечно, у нас предусмотрена очистка дерева во втором всплывающем меню, но ведь пользователь может вызвать команду «Заполнить дерево» дважды, и тогда у нас могут возникнуть проблемы. Далее мы создаем запрос: //вначале запросим все главные узлы: Q1.SQL.Clear; Q1.SQL.Add('select * from Razdels where R_Parent=0'); Q1.Open; if Q1.IsEmpty then Exit; //если НД пуст, выходим.
Здесь после выполнения метода Open мы получаем все разделы, не имеющие родителя. Иначе говоря, главные ветви дерева. Потом мы проверяем – а есть ли главные узлы в таблице? Ведь таблица может быть пуста или испорчена, и тогда дальнейшее выполнение программы не имеет смысла. Если таблица не пуста и главные разделы в ней есть, то мы обходим полученный запросом набор данных от первой до последней записи, сразу же добавляя эти главные узлы в дерево: while not Q1.Eof do begin TreeView1.Selected := nil; TreeView1.Items.Add(TreeView1.Selected, Q1.FieldByName('R_Name').AsString); Q1.Next; end; //while
В результате, в наше дерево пропишутся все главные разделы. После этого нам нужно будет сделать еще один запрос, который выведет все записи, имеющие родителя, в виде «Раздел - подраздел». Запрос формируется следующим образом: Q1.SQL.Clear; Q1.SQL.Append('select r.R_Name, d.R_Name '+ 'from Razdels r, Razdels d '+ 'where r.R_Num=d.R_Parent'); Q1.Open; if Q1.IsEmpty then Exit; //если нет вложенных узлов, выходим
Обратите внимание, в запросе мы используем две копии одной и той же таблицы! Подробнее о псевдонимах таблиц в запросах смотрите лекцию № 8. В результате этого запроса мы получим примерно такой набор данных:
79
Рис. 10.4. Полученный набор данных Далее мы обрабатываем полученный НД от первой до последней записи: Q1.First; while not Q1.Eof do begin TreeViewAddChild(Q1.Fields[0].AsString, Q1.Fields[1].AsString); Q1.Next; end; //while
Здесь мы использовали обращение к полю не по имени, а по индексу, то есть, Q1.Fields[0] – это первое поле. Как видно из рисунка, дважды обращаясь в запросе к одному и тому же полю, мы получим разные названия этих полей (R_Name и R_Name1). Поэтому обращаться к полю по его имени не получится. В цикле мы двигаемся от первой записи к последней, вызывая процедуру TreeViewAddChild, которой у нас еще нет. И в конце процедуры мы распахиваем все узлы полученного дерева. Теперь сделаем процедуру, которой будем передавать все полученные подразделы. В начале модуля, в разделе private, объявите следующую процедуру: private { Private declarations } procedure TreeViewAddChild(rod, doch: String);
Здесь, в параметре rod мы будем передавать название родительского раздела, а в doch – название подраздела. Не убирая курсор с названия процедуры, нажмите . Эта комбинация клавиш автоматически генерирует тело объявленной процедуры. Код процедуры следующий: procedure TfMain.TreeViewAddChild(rod, doch: String); var i : Integer; //счетчик begin //ищем родительский узел в дереве и выделяем его: for i := 0 to TreeView1.Items.Count-1 do begin //если родитель найден, выделяем его и прерываем цикл: if TreeView1.Items[i].Text = rod then begin TreeView1.Items[i].Selected := True; Break; end; //if end; //for //теперь родитель имеет выделение и мы можем добавить к нему //наш узел: TreeView1.Items.AddChild(TreeView1.Selected, doch); end;
Здесь мы вначале циклом for обходим дерево, ища родительский узел. Если узел найден, мы выделяем его в дереве и прерываем цикл. Теперь к выделенному родительскому узлу мы добавляем подраздел: 80
Вот и все. Сохраните проект, скомпилируйте и попробуйте программу в работе. Все должно работать безошибочно. Единственное замечание: вам придется следить за правильностью данных в базе (реализовать бизнес-правила). Если пользователь удалит какой-нибудь родительский узел, нужно будет удалить и все его вложенные узлы. Реализовать это несложно, сделайте такие правила самостоятельно. Данный прием работы с древовидными структурами можно использовать в любой СУБД для самых разных целей.
81
Лекция 11. Отчеты. Quick Report. Отчеты – это один из основных результатов работы проекта с базами данных. Для чего и создаются базы данных, как не для получения отчетов? Отчет подразумевает, что программа выбирает необходимые данные из НД (TTable, TQuery и т.п.) и выводит их на экран в удобном виде. Сам отчет можно не только просматривать, но и выводить его на печать. Лист профессионального отчета представляет собой сгенерированное графическое изображение, картинку, другими словами, данные в отчете редактировать уже нельзя. Не получится также выделить и скопировать в буфер обмена текст отчета. Готовый отчет выводит информацию, разбитую на страницы, и подготовленную к печати. Отчеты создаются специальными наборами компонентов. Имеется очень много отчетов сторонних разработчиков, как платных, так и бесплатных, которые можно найти в Internet. В этой лекции мы разберем стандартный набор компонентов Quick Report, который поставляется вместе с Delphi.
Установка Quick Report. Quick Report представляет собой стандартный набор компонентов для создания отчетов. Он поставляется вместе с Delphi, но не устанавливается в палитру компонентов автоматически. Нам придется установить его самостоятельно. Если пакет Quick Report у вас еще не установлен (на палитре компонентов отсутствует вкладка QReport), то загрузите Delphi и закройте все открытые проекты (File -> Close All). Выбрерите пункт меню «Component -> Install Packages». Нажмите кнопку «Add» и выберите пакет «dclqrt70.bpl», который по умолчанию устанавливается по адресу: c:\Program Files\Borland\Delphi7\bin\dclqrt70.bpl и нажмите кнопку «Открыть». Далее, нажмите кнопку «ОК» - пакет компонентов Quick Report установится, и его вкладка будет самой последней на Палитре компонентов. При желании можно перетащить ее мышью на другое место, поближе к началу.
Простой отчет Создадим простой отчет на основе программы для отдела кадров из лекции №4. Чтобы не менять старый проект, скопируйте его целиком в новую папку и откройте. Кнопок на панели инструментов у нас здесь достаточно, для работы с отчетами создадим главное меню. Добавьте компонент MainMenu и создайте разделы: Таблица 11.1 Разделы главного меню Раздел Подразделы Файл Выход Отчеты Кадры По телефонам По адресам Для самого отчета нам потребуется новая форма. Создайте ее, свойству Name присвойте значение fRepKadr, а модуль сохраните под именем RepKadr. Сразу же командой «File -> Use Unit» подключим к этой форме модуль данных DM, а к главной форме – только что созданный новый модуль. В палитре компонентов перейдем на вкладку QReport. Самым первым компонентом на вкладке является QuickRep – основа всех отчетов. Установите его на новую форму, и он примет вид разлинованного листа. Это своего рода холст, на котором мы будем собирать различные части нашего отчета:
82
Рис. 11.1 Пустой «холст» QuickRep Выделите QuickRep и обратите внимание на его свойства. В самом верху находится свойство Bands (Ленты, полосы – англ.). Это раскрывающееся свойство, оно содержит шесть параметров. Щелкните по плюсу слева от свойства, чтобы раскрыть его. По умолчанию, все параметры имеют значение False, то есть, не установлены. Если какой-либо параметр перевести в значение True, на холсте появится соответствующая полоса. Попробуйте установить все параметры. Разберемся с их назначением. HasColumnHeader – Заголовки колонок. Здесь мы будем вписывать названия колонок таблицы. HasDetail – Детальная информация. На этой ленте формируются строки таблицы. HasPageFooter – Подвал (нижний колонтитул). Здесь можно установить информацию, которая будет появляться в нижней части каждой страницы. HasPageHeader – Шапка (верхний колонтитул). Здесь можно установить информацию, которая будет появляться в верхней части каждой страницы. HasSummary – Суммарная информация. Содержимое этой полосы печатается один раз в самом конце отчета. HasTitle – Заголовок отчета. Переведите в True полосы HasPageHeader, HasTitle, HasColumnHeader, HasDetail и HasPageFooter. Не установленной останется только полоса HasSummary. Если вы дважды щелкните мышью по свободному месту холста, появится настроечное окно:
83
Рис. 11.2. Окно настроек компонента QuickRep В этом окне можно выполнить большинство настроек, причем в Инспекторе объектов соответствующие свойства будут изменены автоматически. Как видите, установленные нами полосы отмечены «галочкой» в разделе Bands окна. Выше располагается раздел Page frame, в котором можно задать обрамление для верхней (Top), нижней (Bottom), левой (Left) и правой (Right) границ холста, а также изменить цвет и ширину обрамления. Те же действия можно выполнить в Инспекторе объектов с помощью параметров раскрывающегося свойства Frame (пока открыто окно настроек, менять свойства в Инспекторе Объектов не получится). Еще выше располагается раздел Other, где можно установить общие данные для холста – шрифт, размер шрифта и единицы измерения (по умолчанию mm - миллиметры). В Инспекторе объектов за это отвечают свойства Font и Units. Далее находится раздел Margins (Границы, края), где можно задать расстояния от краев листа до рабочей части холста. На самом верху окна располагается раздел Paper size (Размер бумаги), где задаются тип листа и его размеры. Данные этих двух разделов можно изменить в Инспекторе объектов в раскрывающем свойстве Page. Еще следует обратить внимание на свойство Options, которое имеет три параметра: FirstPageHeader – Разрешает печать заголовков (шапку) первой страницы, если равно True. LastPageFooter – Разрешает печать подвала последней страницы, если равно True. Compression – Разрешает сжатие отчета при формировании из него метафайла (отчет представляет собой изображение), если равно True. Свойство PrintIfEmpty разрешает (True) или запрещает (False) печатать отчет, если в нем нет никаких данных. Свойство ShowProgress разрешает или запрещает показывать индикатор процесса печати отчета. По умолчанию индикатор разрешен. Свойство SnapToGrid разрешает или запрещает привязывание компонентов к сетке. По умолчанию привязка разрешена. 84
Свойство Zoom имеет тип Integer и позволяет изменить масштаб отображения отчета при его разработке. Значение 100 указывает, что отчет показывается в 100% от листа бумаги. Изменение этого свойства не влияет на масштаб печати отчета или его предварительного просмотра. Теперь приступим к формированию отчета. На холсте у нас уже должны быть расположены пять полос. Теперь мы можем на эти полосы устанавливать другие компоненты. На самом верху холста находится полоса Page Header, которая, как мы уже знаем, является верхним колонтитулом. Установите в левой части этой полосы компонент QRSysData – компонент с различного рода системной информацией. Нас интересует свойство Data этого компонента. Data содержит несколько свойств, формирующих отображаемую информацию. Разберем эти свойства.
Установим для этого свойства значение qrsDateTime, чтобы пользователь мог видеть, когда был сформирован отчет. Далее выделим всю полосу Page Header и в свойстве Frame переведем в True параметр DrawBottom. Это свойство позволяет задать обрамление выделенной полосе, а параметр DrawBottom рисует линию в нижней части полосы. То есть, мы визуально отделили колонтитул от данных листа. В нижней части холста располагается полоса Page Footer (подвал). Здесь желательно установить верхнюю линию в свойстве Frame, отделив от данных нижний колонтитул. А по центру полосы установить еще один компонент QRSysData, установив свойство Data в значение qrsPageNumber. Этот компонент будет выводить номер текущей страницы в нижней части листа. Примечание: для того, чтобы увидеть отчет в окне предварительного просмотра, не обязательно компилировать программу. Достаточно щелкнуть правой кнопкой мыши по свободному месту листа QuickRep, и в контекстном меню выбрать команду Preview. При этом следует иметь в виду, что такие данные, как значения вычисляемых полей, например, видны не будут. Эти данные станут доступны только в режиме выполнения программы. Колонтитулы мы установили, займемся данными. Прежде всего, напишем заголовок отчета. Для этого установите компонент QRLabel в центре полосы Title. QRLabel похож на обычный Label и служит тем же целям: выводит на лист какой то текст. Выделите его, и в свойстве Caption напишите «Отчет по кадрам». Чтобы заголовок был красивым, щелкните дважды по свойству Font, чтобы открылось окно шрифта. Здесь установите шрифт Times New Roman, начертание выберите жирное, а размер шрифта пусть будет 18 (вы можете использовать настройки по собственному выбору). Можно изменять и цвет шрифта, но при этом имейте в виду, что чаще всего отчеты печатают на черно-белых принтерах, так что злоупотреблять разными цветами не рекомендуется. Далее займемся полосой Column Header (Заголовки колонок). Здесь установите рядом пять компонентов QRLabel, в свойстве Caption которых напишите Фамилия Имя Отчество Дата рождения Образование Это будут названия колонок таблицы. Шрифт этих компонентов также желательно сделать крупнее, но не больше заголовка. 85
Далее займемся полосой Detail, на которой, собственно, и будет формироваться таблица. Здесь нам нужно будет в самом крайнем положении слева установить компонент QRSysData, в свойстве Data которого выбрать qrsDetailNo – перед каждой строкой будет выходить ее номер. Далее установите пять компонентов QRDBText, в которых будут отражаться данные из соответствующих полей таблицы. Эти компоненты соответствуют обычному DBText, с которым мы неоднократно сталкивались. Расположите их точно под названиями столбцов, чтобы таблица была красивой. При этом может оказаться, что компонент QRSysData «наплывает» на QRDBText – ничего страшного, данные все равно не будут мешать друг другу. Выделите все QRDBText, и в их свойстве DataSet выберите нашу таблицу fDM.TLichData, затем поочередно в свойстве DataField этих компонентов выберите соответствующие поля таблицы: Фамилия Имя Отчество Дата_Рожд Образование Кроме того, сам компонент QuickRep1, который является «холстом» отчета, также должен знать, из какой таблицы ему нужно брать данные. Поэтому выделите его, и в свойстве DataSet также выберите нашу таблицу fDM.TLichData. Если этого не сделать, то в отчете будет выходить лишь текущая запись таблицы, а не все ее записи. Собственно, отчет уже готов:
Рис. 11.3. Отчет Вернитесь в главное окно проекта и сгенерируйте обработку команды меню «Отчеты - Кадры». В созданной процедуре напишите такую строку (вы ведь добавили к главному окну командой File – Use Unit наш отчет?): {Отчет Кадры} procedure TfMain.N5Click(Sender: TObject); begin fRepKadr.QuickRep1.PreviewModal; end;
86
После того, как вы сохраните проект, скомпилируете его и выполните команду меню, появится подобное окно с отчетом:
Рис. 11.4. Окно отчета Как видите, окно уже содержит весь необходимый инструментарий в панели инструментов – позволяет настроить вид отчета, листать его страницы, сделать установку принтера и распечатать отчет, а также сохранить отчет в специальный файл с расширением *.qrp с возможностью последующей его загрузки. К сожалению, интерфейс этого окна доступен только на английском языке. Наш отчет был бы красивей, если бы полученная таблица была очерчена рамкой. Исправим этот недостаток. На вкладке QReport имеется компонент QRShape, который позволяет рисовать простейшие линии и фигуры. Он имеет свойство Shape, в котором можно задать нужную фигуру. Возможные значения этого свойства:
qrsCircle (Круг) qrsHorLine (Горизонтальная линия) qrsRectangle (Прямоугольник) qrsRightAndLeft (Прямоугольник с очерченными левым и правым краями) qrsTopAndBottom (Прямоугольник с очерченными верхним и нижним краями) qrsVertLine (Вертикальная линия)
Этот компонент можно использовать по-разному. Например, можно установить по одной горизонтальной линии сверху и снизу полосы Detail, а затем вертикальными линиями отделить каждый столбце страницы. Я сделал проще: каждый столбец заключил в прямоугольник (шесть компонентов QRShape), а чтобы QRShape не перекрывал текст, щелкнул по ним правой кнопкой и выбрал команду Control -> Send to Back (поместить на задний план):
Рис. 11.5. Компоненты QRShape играют роль границ таблицы 87
В результате таблица отчета приняла вид:
Рис. 11.6. Таблица отчета с границами Результат может не сразу получиться таким – придется поэкспериментировать с расположением и размерами прямоугольников QRShape.
Отчет из связанных таблиц Часто бывает, когда недостаточно получить отчет по данным только из одной таблицы. И здесь можно поступить двумя способами: 1. Воспользоваться набором данных ADOQuery и с помощью SQL-запроса получить нужные данные из двух таблиц. 2. Воспользоваться компонентом QRSubDetail, который специально предназначен для получения данных из связанной таблицы. Этим компонентом мы и воспользуемся. Итак, QRSubDetail – это полоса, которая импортирует в отчет данные из подчиненной таблицы. Создайте в проекте новую форму, назовите ее fRepTelephons, а модуль сохраните как RepTelephons. Сразу же к этому окну командой File -> Use Unit подключите модуль с наборами данных DM, а к главному модулю подключите только что созданный RepTelephons. Установите на новую форму основу отчета QuickRep. Проверьте, чтобы он не был смещен по отношению к листу (свойства Left и Top равны 0). В свойстве DataSet компонента-основы выберите fDM.TLichData, то есть, таблицу с личными данными. Теперь создайте на основе полосы Page Header, Title, Column Header и Detail. На верхнюю полосу Page Header установите по краям два компонента QRSysData, в свойстве Data у первого выберите qrsDateTime, а у второго qrsPageNumber. Кроме того, переведите в True параметр DrawBottom у свойства Frame полосы Page Header, чтобы отделить линией верхний колонтитул. Далее, на полосу Title установите один компонент QRLabel, на котором напишите заголовок «Отчет по телефонам сотрудников». Измените шрифт, начертание и размер, как в прошлом примере, и отцентрируйте заголовок по полосе. Ниже идет полоса Column Header с заголовками таблицы. Как и в прошлом примере, требуется из компонентов QRLabel сформировать заголовки столбцов, но в этот раз ограничимся только тремя заголовками: «Фамилия», «Имя» и «Отчество». К слову сказать, чтобы не делать работу дважды, вы можете открыть форму fRepKadr из прошлого примера, выделить нужные компоненты и командой всплывающего меню Edit -> Copy скопировать их. Затем перейти в новую форму, выделить нужную полосу, и командой Edit -> Paste вставить эти компоненты. Далее у нас идет полоса Detail, на которой нам нужно разместить три компонента QRDBText, которые привязать к соответствующим полям (не забудьте про свойства DataSet и DataField этих 88
компонентов). Еще в свойстве Frame полосы Detail желательно перевести параметр DrawTop в True, чтобы каждая запись отчета отделялась линией. Пока что все, что мы делали, было практически таким же, как в прошлом отчете. Теперь добавим в отчет связанные данные из другой таблицы. Установите компонент QRSubDetail – эта полоса должна быть самой нижней. Сначала нужно выбрать главный по отношению к этому компонент: в свойстве Master выберите QuickRep1. Кроме того, полоса QRSubDetail должна знать, откуда листать данные, поэтому в свойстве DataSet полосы выберите fDM.TTelephones. Далее установим на полосу один компонент QRLabel, напишем на нем «Телефон:». Слева от него установите два компонента QRDBText, в свойстве Dataset которых выберите fDM.TTelephones, а в свойстве DataField выберите соответственно, поля «Телефон» и «Примечание». Теперь мышью немного перетащите нижний край полосы, чтобы сделать ее поуже. В результате у вас должна получиться форма, подобная этой:
Рис. 11.7. Отчет по телефонам Наш отчет готов. Создайте процедуру вызова этого окна в команде главного меню Отчеты -> По телефонам, и пропишите там вызов fRepTelephons.QuickRep1.PreviewModal;
Сохраните проект, скомпилируйте и запустите его. В результате выбора этой команды мы получим подробный отчет по телефонам сотрудников:
89
Рис. 11.8. Окно отчета по телефонам Отчет по адресам сотрудников создается точно таким же образом, и теперь вы сможете сделать его самостоятельно.
Экспорт отчета в другие форматы Отчет можно не только распечатать. Его также можно сохранить в специальном формате *.qrp, а затем загрузить в окно предварительного просмотра. Для этого соответственно служат кнопки «Save Report» и «Load Report» на панели инструментов окна предварительного просмотра:
Рис. 11.9. Кнопки «Save Report» и «Load Report» Однако бывают случаи, когда отчет желательно сохранить в каком-нибудь общем формате, например, в текстовом, или html (web-страница). Тогда отчет можно было бы просмотреть в стандартном Блокноте или web-броузере, переслать сотруднику, у которого ваша программа не установлена. На вкладке QReport имеются компоненты, которые позволяют это сделать. QRTextFilter – позволяет сохранить отчет в виде текстового файла. QRCSVFilter – позволяет сохранить отчет в специальном формате CSV (Comma Separated). QRHTMLFilter – позволяет сохранить отчет в формате web-страницы. Это не визуальные компоненты, на отчете они не отобразятся. Достаточно установить один из них (или все вместе) на основу отчета QuickRep, и при сохранении отчета пользователю станут доступны соответствующие форматы:
90
Рис.11.10. Выбор формата сохранения отчета Причем если у вас в проекте имеется несколько окон с отчетами, компоненты добавляются только в один из них, в остальных отчетах эти форматы также станут доступны.
91
Лекция 12. Работа с сеткой DBGrid Мы с вами уже неоднократно применяли этот компонент для вывода на экран информации из наборов данных в виде таблицы. Однако этот компонент способен на большее. Профессиональные программы отличаются большим набором дополнительных возможностей для пользователя. В этой лекции мы и поговорим о дополнительных возможностях сетки DBGrid. Как мы уже знаем, строки сетки DBGrid соответствуют записям подключенного набора данных, а столбцы – полям. Свойство DataSource содержит ссылку на выбранный набор данных. Изменяя эту ссылку во время работы программы, можно изменять выводимые в сетке данные, отображая то одну, то другую таблицу (или запрос) в одной сетке DBGrid.
Столбцы DBGrid Столбцы содержат значения полей подключенного к сетке набора данных. Этими значениями можно манипулировать, показывая или скрывая поля НД, меняя их местами или добавляя новые столбцы. Нам уже приходилось это делать в редакторе полей набора данных, однако, это не всегда оправдано – один набор данных может использоваться в различных местах приложения, в различных формах и на различных сетках. Изменение свойств полей набора данных в этом случае коснется и всех сеток DBGrid, которые подключены к нему, а это требуется далеко не всегда. Более разумным вариантом будет добавление всех полей в редактор полей набора данных, а изменение их свойств можно сделать в каждой сетке по-своему. Если не пользоваться редактором столбцов самой сетки, DBGrid будет выводить значения по умолчанию – будут выведены все поля набора данных, а заголовки столбцов будут соответствовать именам полей. Но стоит только добавить в редактор столбцов хоть один столбец, и сетка DBGrid будет отображать только его. Таким образом, мы можем показывать только те столбцы, которые действительно необходимы. Создайте новое приложение. Свойству Name формы, как всегда, присвойте значение fMain, свойству Caption – «Изучение свойств DBGrid». Проект сохраните в отдельную папку, модулю дайте имя Main, а проекту в целом – MyDBGrid. В эту же папку скопируйте базу данных ok.mdb из прошлой лекции. На форме нам понадобятся сетка DBGrid с вкладки Data Controls, с вкладки ADO компоненты ADOConnection и ADOTable, с вкладки Data Access – компонент DataSource. Также для красоты и удобства можно добавить компонент DBNavigator. Из прошлых лекций вы знаете, как подключить к базе данных компонент ADOConnection, а затем подключить к нему таблицу ADOTable. В свойстве TableName таблицы выберите таблицу LichData, и откройте ее. Компонент DataSource подключите к нашей таблице, а сетку DBGrid и навигатор DBNavigator – к DataSource. В результате у вас должна получиться простая форма с сеткой и навигатором по ней, в которой отображаются все поля таблицы LichData:
92
Рис. 12.1. Форма проекта Допустим, в нашем проекте нам нужны не все поля таблицы, а только некоторые из них. Значит, придется поработать с редактором столбцов сетки DBGrid. Вызвать редактор можно тремя способами: дважды щелкнуть по сетке; щелкнуть правой кнопкой по сетке и в контекстном меню выбрать команду Columns Editor и, наконец, щелкнув дважды по свойству сетки Columns в Инспекторе Объектов:
Рис. 12.2. Редактор столбцов сетки DBGrid Работа с этим редактором очень похожа на редактор полей набора данных, но есть и отличия. В верхней части окна вы видите четыре кнопки, слева – направо: 1. Add New (Добавить новый столбец). 2. Delete Selected (Удалить выделенный столбец). 3. Add All Fields (Добавить все столбцы из набора данных). 4. Restore Defaults (Восстановить значения по умолчанию для выделенного столбца). Если столбцов в редакторе нет, то сетка отображает все поля НД. Добавим один столбец. Для этого нажмем первую кнопку. Сразу же все поля НД исчезли, а сетка отображает пустой столбец. Выделим его в редакторе столбцов, а в Инспекторе объектов в свойстве FieldName выберем поле «Фамилия». Сразу же столбец отобразит это поле. Заголовок столбца будет соответствовать названию поля. В нашей БД имена полей мы задавали русскими буквами, однако это бывает не всегда, особенно если вы работаете с таблицами Paradox или клиент-серверными БД. В этом случае названия полей будут 93
выводиться латиницей, а это не удобно для пользователя. Изменить параметры заголовка столбца можно в раскрывающемся свойстве Title, которое имеет ряд собственных свойств: Alignment – свойство устанавливает выравнивание заголовка и может быть taCenter (по центру), taLeftJustify (по левому краю) и taRightJustify (по правому краю). По умолчанию, заголовок выровнен по левому краю. Caption – свойство содержит текст, который отображается в заголовке столбца. Если поле НД имеет имя латинскими буквами, именно здесь можно отобразить его кириллицей. Color – цвет заголовка, по умолчанию это свойство равно clBtnFace, что обеспечивает стандартный серый цвет. Если вы желаете украсить программу, можете выбрать другой цвет. Font – шрифт заголовка. Если дважды щелкнуть по этому свойству, откроется диалоговое окно, в котором можно изменить шрифт, начертание, размер и цвет шрифта. То же самое можно сделать, если раскрыть это свойство и непосредственно изменять нужные свойства. Надо заметить, что свойства Font, Alignment и Color внутри свойства Title меняют шрифт, выравнивание и цвет фона только заголовка столбца, а не его содержимого. Но у столбца имеются эти же свойства, они меняют шрифт, выравнивание и цвет фона выводимых в столбце данных. Свойство Visible разрешает или запрещает отображение столбца, а свойство Width позволяет изменить его ширину. О других свойствах поговорим чуть позже. Добавьте в сетку другие поля НД: «Имя», «Отчество», «Пол» и «Военнообязанный». Обратите внимание, что перетаскивая мышью столбцы в редакторе столбцов, вы можете менять их порядок в сетке. Пользователь же имеет возможность менять их порядок, перетаскивая мышью заголовки столбцов. У сетки DBGrid имеется свойство Columns, которое содержит столбцы. Щелчок по этому свойству как раз и откроет редактор столбцов сетки. Столбцы хранятся в свойстве Items в виде массива и имеют индексы от 0 до DBGrid1.Columns.Items.Count-1. Заметим, что свойство Items используется по умолчанию, и его можно не указывать: DBGrid1.Columns[0] = DBGrid1.Columns.Items[0]
Шрифт и цвет можно менять не только в Инспекторе объектов, но и программно. Добавим на форму две кнопки, на которых напишем «Шрифт» и «Цвет». А также два диалога: FontDialog и ColorDialog. Создадим процедуру нажатия на первую кнопку и впишем в нее следующее: {Меняем шрифт} procedure TfMain.Button1Click(Sender: TObject); begin //считаем в диалог шрифта установленный шрифт FontDialog1.Font := DBGrid1.Columns.Items[DBGrid1.SelectedIndex].Font; //установим выбранный шрифт: if FontDialog1.Execute then DBGrid1.Columns.Items[DBGrid1.SelectedIndex].Font := FontDialog1.Font; end;
Здесь вначале мы свойству Font диалога FontDialog присвоили тот же шрифт, который был в текущем столбце сетки. Затем вызвали выбор диалога, и если пользователь выбрал другой шрифт (название, начертание, размер, цвет шрифта), то изменяем шрифт всего столбца на выбранный пользователем. Аналогичным образом меняем и цвет столбца. Создайте процедуру обработки второй кнопки, и впишите код:
94
{Меняем цвет} procedure TfMain.Button2Click(Sender: TObject); begin //считаем в диалог цвета установленный цвет: ColorDialog1.Color := DBGrid1.Columns.Items[DBGrid1.SelectedIndex].Color; //установим выбранный цвет: if ColorDialog1.Execute then DBGrid1.Columns.Items[DBGrid1.SelectedIndex].Color := ColorDialog1.Color; end;
Сохраните проект, скомпилируйте его и проверьте работу кнопок. И шрифт, и цвет текущего столбца будут меняться:
Рис. 12.3. Изменение шрифта и цвета текущего столбца Следует заметить, что таким образом можно менять параметры не только содержимого текущего столбца, но и его заголовка, если, например, вместо DBGrid1.Columns.Items[DBGrid1.SelectedIndex].Color
использовать DBGrid1.Columns.Items[DBGrid1.SelectedIndex].Title.Color
А если применить цикл, то можно изменить параметры всех столбцов: for i:= 0 to DBGrid1.Columns.Count-1 do DBGrid1.Columns.Items[i].Color :=
Пустые столбцы Если добавить в редактор столбцов сетки DBGrid новый столбец, но в свойстве FieldName не выбирать поле БД, а оставить его пустым, мы получим пустой столбец. Для чего нужны пустые столбцы в сетке? Во-первых, в них можно выводить обработанные данные из других столбцов. К примеру, пользователю неудобно просматривать три столбца «Фамилия», «Имя» и «Отчество». Ему было бы удобней просмотреть один сборный столбец в формате «Фамилия И.О.». Во-вторых, пустое поле может выводить информацию по требованию. Рассмотрим эти случаи. Создайте новый столбец, но не назначайте ему поле из НД. Выделите этот столбец, и в его свойстве Title.Caption впишите «Фамилия И.О.», а в свойстве Width укажите ширину в 150 пикселей. 95
Эти сборные данные не будут видны в момент проектирования таблицы, они выйдут только во время работы программы. Столбцы «Фамилия», «Имя» и «Отчество» нам уже не нужны, скройте их, установив их свойство Visible в False. А новый столбец перетащите мышью наверх, его индекс будет равен 0. Нам придется написать код, который нужно вписать в событие OnDrawColumnCell сетки. Это событие наступает при прорисовке каждой ячейки столбца. Также имеется событие OnDrawDataCell, которое выполняет схожие функции, но оно оставлено для поддержки старых версий, и использовать его не желательно. Итак, выделяем сетку, генерируем событие OnDrawColumnCell и вписываем код: {Прорисовка таблицы} procedure TfMain.DBGrid1DrawColumnCell(Sender: TObject; const Rect: TRect; DataCol: Integer; Column: TColumn; State: TGridDrawState); var s: String; begin //если это пустой столбец if Column.Index = 0 then begin if ADOTable1['Фамилия'] <> Null then s:= ADOTable1['Фамилия'] + ' '; if ADOTable1['Имя'] <> Null then s:= s + Copy(ADOTable1['Имя'], 1, 1) + '.'; if ADOTable1['Отчество'] <> Null then s:= s + Copy(ADOTable1['Отчество'], 1, 1)+ '.'; DBGrid1.Canvas.TextOut(Rect.Left, Rect.Top, s); end; //if end;
Здесь мы вначале проверяем – наш ли это столбец (равен ли индекс нулю)? Если наш, то в переменную s начинаем собирать нужный текст. При этом имеем в виду, что пользователь мог и не заполнить некоторые поля. Чтобы у нас не произошло ошибки, вначале убеждаемся, что поле не равно Null (то есть, текст есть). Если текст есть, добавляем его в переменную s. Причем если это имя или отчество, с помощью функции Copy() получаем только первую букву и добавляем к ней точку. Когда s сформирована, добавляем этот текст в наш столбец с помощью метода TextOut() свойства Canvas сетки. В метод передаются три параметра: координаты левой позиции, верхней позиции и сам текст. Эти координаты мы берем из параметра события OnDrawColumnCell – Rect, который имеет такие свойства, как Left и Top, показывающие, соответственно, левую и верхнюю позиции текущей ячейки. В результате программа будет иметь вид:
Рис. 12.4. Заполнение пустого столбца Теперь о том, как использовать пустой столбец для вывода информации по требованию пользователя. Допустим, в сетке DBGrid нам нужна кнопка, нажатие на которую привело бы к выводу сообщения об образовании сотрудника. Создайте новый пустой столбец. Перетаскивать его не нужно, пусть будет последним. Свойство Width (ширина) установите в 20 пикселей. Название столбца (Title.Caption) вписывать не нужно, пусть 96
будет пустым. В свойстве ButtonStyle выберите значение cbsEllipsis. Это приведет к тому, что при попытке редактировать этот столбец образуется кнопка с тремя точками:
Рис. 12.5. Кнопка в пустом столбце Нужный код пишется в событии OnEditButtonClick() сетки DBGrid, которое происходит всякий раз, когда пользователь нажимает на кнопку «…». Сгенерируйте это событие и впишите только одну строку: ShowMessage(ADOTable1['Образование']);
Теперь, когда пользователь нажмет на эту кнопку, ему будет выведено сообщение с текстом об образовании текущего сотрудника.
Список выбора в столбце Для организации списков выбора служит компонент ComboBox. Однако сетка DBGrid позволяет устроить такой же список в одном из своих столбцов без использования каких-либо других компонентов. В нашем примере есть поле «Пол». Это текстовое поле из трех символов. Во время конструирования базы данных ok.mdb с помощью программы MS Access мы указывали, что это поле может хранить значения либо «муж», либо «жен». То же самое можно было сделать с помощью сетки. Откройте редактор столбцов сетки и выделите столбец «Пол». Обратите внимание на свойство PickList в Инспекторе объектов. Это свойство имеет тип TStrings, то есть представляет собой набор строк, так же как и свойство Items у компонента ComboBox. Щелкните дважды по PickList, чтобы открыть редактор, и впишите туда муж жен именно так, каждое значение на своей строке. Сохраните проект, скомпилируйте его и попробуйте редактировать этот столбец. При попытке редактирования в ячейке покажется похожий на ComboBox список, в котором можно будет выбрать одно из указанных значений:
Рис. 12.6. Список выбора в сетке Следует иметь в виду, что наличие такого списка не препятствует пользователю ввести какое-то иное значение. Этот список нужен не для контроля, а только для облегчения пользователю ввода данных. Если же вы не желаете, чтобы пользователь имел возможность вводить другие данные, контроль следует организовать иным способом. Еще нужно заметить, что в практике программирования список чаще формируется во время работы программы, а строки списка берутся, как правило, из другой связанной таблицы. Добавить в список новую строку очень просто: DBGrid1.Columns.Items[4].PickList.Add('абв');
97
Выделение отдельных строк Очень часто в практике приходится выделять какие-то строки, изменяя их фон или цвет шрифта. Например, в бухгалтерии обычно выделяют строки, в которых значение меньше нуля. Допустим, ваша программа показывает клиентов, а какой-то столбец содержит их баланс на счету вашей компании. Если этот баланс меньше 0, значит, клиент имеет задолженность перед вашей фирмой. Бухгалтеру будет очень удобно, если дебиторы (должники) будут выделяться в общем списке красным цветом. Способ прорисовки данных в сетке DBGrid зависит от значения ее свойства DefaultDrawing. По умолчанию свойство равно True, то есть данные прорисовываются автоматически. Если свойство содержит False, то прорисовку придется кодировать самостоятельно в свойствах OnDrawColumnCell или OnDrawDataCell, о которых уже упоминалось в этой лекции. Если мы написали алгоритм прорисовки, но свойство DefaultDrawing содержит True, то вначале сетка заполнится данными автоматически, а затем будет выполнен наш алгоритм. Другими словами, прорисовка некоторых частей сетки будет выполнена дважды. Это не очень хорошо для быстродействия программы, однако нам придется поступать именно так: ведь мы не все строки и столбцы собираемся выводить другим способом, а только некоторые. Остальные будут заполнены данными по умолчанию. Разберем этот метод подробней. Если найти его в справке Delphi, то увидим: property OnDrawColumnCell: TDrawColumnCellEvent;
То есть, этот метод имеет тип TDrawColumnCellEvent. Описание типа такое: type TDrawColumnCellEvent = procedure (Sender:TObject; const Rect:TRect; DataCol:Integer; Column:TColumn; State:TGridDrawState) of object;
Разберемся с параметрами. Rect – координаты прорисовки. DataCol – порядковый номер текущего столбца (начиная с 0). Column – данные текущего столбца. State – состояние ячейки. Может быть: gdSelected – ячейка выделена gdFocused – фокус ввода в ячейке gdFixed – ячейка – заголовок столбца. Допустим, в нашем примере требуется, чтобы строки с военнообязанными сотрудниками выделялись красным цветом:
Рис. 12.7. Выделение строк Заметим сразу, что наличие пустых столбцов создает дополнительные, но решаемые проблемы. Код события OnDrawColumnCell придется переделать, он будет таким: 98
{Прорисовка таблицы} procedure TfMain.DBGrid1DrawColumnCell(Sender: TObject; const Rect: TRect; DataCol: Integer; Column: TColumn; State: TGridDrawState); var s: String; begin with DBGrid1.Canvas do begin //поле "Военнообязанный" содержит истину? if (ADOTable1['Военнообязанный'])= True and not (gdSelected in State) then begin //выводим все ячейки строки белым по красному: Font.Color:= clRed; FillRect(Rect); end; //if //если это пустой сборный столбец if Column.Index = 0 then begin if ADOTable1['Фамилия'] <> Null then s:= ADOTable1['Фамилия'] + ' '; if ADOTable1['Имя'] <> Null then s:= s + Copy(ADOTable1['Имя'], 1, 1) + '.'; if ADOTable1['Отчество'] <> Null then s:= s + Copy(ADOTable1['Отчество'], 1, 1)+ '.'; DBGrid1.Canvas.TextOut(Rect.Left, Rect.Top, s); end //if //если это пустой столбец с кнопкой "..." else if Column.Index = 6 then begin DBGrid1.DefaultDrawColumnCell(Rect, DataCol, Column, State); Exit; end //if //все остальные столбцы else TextOut(Rect.Left+2, Rect.Top+2, Column.Field.Text); end; //with end;
Разберемся с кодом. Вначале с помощью with мы указываем, что будем работать непосредственно со свойством DBGrid1.Canvas, которое отвечает за стиль прорисовки ячейки. Далее мы смотрим, содержится ли True в поле «Военнообязанный» текущей записи. Если да, то указываем, что цвет шрифта должен быть красным, а затем функцией FillRect(Rect) мы стираем стандартный вывод. Далее мы определяем, прорисовывается ли в данный момент пустой сборный столбец с «Фамилия И.О.». Если это он, то мы формируем переменную s с нужными данными и выводим их, как в прошлый раз. Если же это пустой столбец с кнопкой «…», то мы делаем стандартный вывод и выходим из процедуры. Если мы этого не сделаем, то получим ошибку программы. Все остальные столбцы мы выводим строкой TextOut(Rect.Left+2, Rect.Top+2, Column.Field.Text);
Обратите внимание, что мы добавили по два пикселя к крайней левой и крайней верхней координате ячейки. Если этого не сделать, то новая прорисовка не будет целиком закрашивать старую:
Рис. 12.8. Некорректная прорисовка Количество добавляемых пикселей зависит от формата данных и размера шрифта. Обычно это определяется путем проб. Например, ячейка может содержать цифры, которые обычно прижимаются к правому краю, и тут двумя пикселями не обойтись. 99
Сделать проверку на тип данных можно, например, так: //если текст, сдвинем только на 2 пикселя if Column.Field.DataType = ftString then TextOut(Rect.Left, Rect.Top+2, Column.Field.Text) //если цифры, сдвинем их вправо на 28 пикселей else TextOut(Rect.Left+28, Rect.Top+2, Column.Field.Text);
Выделение строки красным текстом может оказаться недостаточным для заказчика. Что, если он потребует, чтобы эти строки выделялись красной строкой с белым шрифтом? Тогда вместо Font.Color:= clRed; FillRect(Rect);
вам придется написать //выводим все ячейки строки белым текстом по красному фону: Brush.Color:= clRed; Font.Color:= clWhite; FillRect(Rect);
Как видите, подсвойство DBGrid1.Canvas.Brush.Color
отвечает за цвет заливки ячейки, а DBGrid1.Canvas.Font.Color
за цвет выводимого в ней шрифта. Список доступных цветов вы можете открыть в любом свойстве Color любого компонента. Теперь вы сможете создать сетку со сложными элементами прорисовки.
100
Лекция 13. DBChart. Графики и диаграммы. Для построения графиков и диаграмм используются компоненты Chart с вкладки Additional, и DBChart с вкладки Data Controls палитры компонентов. Это равноценные компоненты, отличие состоит в том, что DBChart принимает данные из указанного набора данных – таблицы или запроса, а при использовании Chart данные приходится вносить самостоятельно. Это довольно сложные компоненты, они имеют множество свойств, которые в свою очередь сами являются сложными объектами. Если описывать компоненты графиков и диаграмм подробно, то получится небольшая книга, поэтому мы рассмотрим лишь основные приемы работы с ними.
Простое приложение с графиком Графики применяются там, где нужно показать динамику подъема или спада одного или нескольких объектов. Хорошим примером является график с кривыми, демонстрирующими динамику изменений курсов доллара и евро к рублю. Диаграммы же применяются для демонстрации сравнительных показателей разных объектов. Так, например, во время предвыборной кампании часто демонстрируют диаграмму, где столбики различного роста показывают, кто из кандидатов набрал больше голосов. Круговые диаграммы показывают сравнительное отношение каждого объекта к целому, например круговая диаграмма, показывающая процент депутатов каждой партии от общего количества депутатов. Познакомимся с работой компонента DBChart на практике. Для примера нам понадобится база данных с одной таблицей, в которую мы запишем курсы доллара и евро к рублю. Таблицу вы можете сделать какой угодно – Paradox или MS Access, в таблице создайте три поля: CDate, CDollar и CEvro. Саму таблицу назовите Curs. Заполните таблицу произвольными данными, делая по записи на каждый день, пусть в таблице будет не менее 10 записей. Данные не обязательно должны соответствовать действительности, но постарайтесь сделать их похожими на реальный курс доллара США и евро к рублю, например: CDate: 10.03.2010 CDollar: 29,21 CEvro: 40,16 Заполнив таблицу, подключите ее к новому проекту, используя технологию BDE или ADO, и сразу же переведите в True свойство Active компонента TTable (TADOTable). Далее на форму установите пустую панель со свойством Align, равным alTop (она нам потребуется позже). На свободное место формы установите компонент DBChart с вкладки Data Controls Палитры компонентов. Свойство Align компонента установите в alClient, чтобы график занял все оставшееся место формы. Теперь дважды щелкните по графику, чтобы открыть редактор серий:
101
Рис. 13.1. Редактор серий графика Все отображаемые на графике данные построены с помощью серий – объектов Series типа TChartSeries, которые являются отображением данных отдельного реального объекта. Например, если мы используем график динамики курса доллара и евро к рублю, то серия доллара будет содержать ряд точек на графике, которые соответствуют стоимости доллара на каждый день. Для евро будет создана собственная серия. Все настройки серий можно делать как с помощью этого редактора, так и программно изменяя свойства графика. В этом примере нам потребуется сделать две серии: для доллара и для евро. Ось X будет содержать дату, а ось Y – значение. Нажмите кнопку Add (добавить серию). У вас появится окно выбора графика:
Рис. 13.2. Окно выбора графика. Помимо выбора графика мы так же можем оставить или снять «галочку» 3D, которая включает или выключает объемность. Объемный график смотрится наряднее и больше подходит для всякого рода докладов или презентаций. Если же вам придется строить график в строгой деловой программе, то 102
объемность будет разумней не использовать. Выбор типа графика или диаграммы зависит от типа отображаемых данных. В нашем примере мы собрались чертить кривую, так что нам больше подойдут типы Line или Fast Line. Выберем первый из них. Как только мы это сделали, на компоненте DBChart отобразился график со случайными данными. Это происходит по умолчанию, чтобы легче было производить настройки серии. В редакторе серий появилась серия Series1. Выделите ее и щелкните по кнопке Title (заголовок). Измените заголовок на «Доллар»:
Рис. 13.3. Новая серия Теперь перейдите на вкладку Series на самом верху окна редактора, а уже на этой вкладке откройте внутреннюю вкладку Data Source. В выпадающем списке вы видите Random Values (случайные значения), которые и обеспечили показ серии на графике. Нам нужно выбрать Dataset, а в окне Dataset – наш набор данных:
Рис. 13.4. Подключение серии к таблице
103
В списке Labels можно выбрать поле с данными по доллару, а можно оставить его пустым, этот список используется для отображения меток, если они установлены. В списке X выберите поле с датой, автоматически должна установиться «галочка» DateTime. Эти даты будут отображаться по оси X. А по оси Y отобразим поле с курсом доллара. Как только вы закроете редактор кнопкой Close, на форме появится график курса доллара. Далее перейдите на вкладку Chart и добавьте еще одну серию. Сделайте все точно также, только отобразите курс евро. Мы получили не очень впечатляющий график:
Рис. 13.5. График курсов доллара и евро Займемся настройкой графика. Вновь откройте редактор графика и перейдите на вкладку Chart, а в ней на вкладку Titles. В выпадающем списке мы видим «Title» (заголовок графика), а в текстовом окне отображается название графика «TDBChart». Впишите вместо него «Курсы доллара и евро». Кнопка Font позволяет изменить шрифт заголовка, кнопка Border откроет окно, в котором можно настроить обрамление. Кнопка Back Color открывает диалог выбора цвета для фона заголовка, кнопка Pattern также позволяет настроить фон, придав ему цвет «родителя» - самого графика. Если вы откроете выпадающий список, то увидите, что помимо «Title» (заголовка) доступен еще и «Foot» (подвал) – надпись, которая будет выведена внизу. Напишите там «Пример простого графика». Смотрим, какие вкладки здесь еще есть. Самой последней имеется вкладка 3D, на которой можно включить или выключить объемность графика, а также отрегулировать вращение, наклон или масштаб. На вкладке Walls (стены) можно отрегулировать «стены» осей, на рисунке 13.5 они выделяются желтым и белым цветами. Вкладка Paging позволяет настроить многостраничные графики, а вкладка Panel – задать параметры фона. Интересны здесь параметры панели Gradient, позволяющие задать градиентную заливку. При этом фон будет плавно переходить из одного цвета в другой. Вкладка Legend позволяет настроить легенду графика, на рисунке вы видите ее в правой части графика с надписями «Доллар» и «Евро». Перейдем на вкладку Axis (оси). Здесь мы можем сделать множество настроек осей. Вначале в левой части окна в разделе Axis нужно выбрать ось. Мы выберем Left, то есть, ось Y. Правее находится дополнительное окно со своими собственными вкладками, причем открыта вкладка Scales (шкалы):
104
Рис. 13.6. Настройка осей графика Здесь мы снимем «галочку» Automatic, которая автоматически устанавливает размер шкалы. В большинстве случаев этого не требуется, но в нашем примере мы получили относительно ровные линии, причем одна из них в нижней, а другая – в верхней части, что не делает график красивее. Итак, снимите эту галочку, а затем с помощью кнопок Change немного увеличьте максимальное значение, и немного уменьшите минимальное. В результате кривые графика сдвинутся к середине. Далее можете перейти на внутреннюю вкладку Title, где напишите «Курс к рублю». Эта надпись является заголовком оси Y. Больше, пожалуй, с этой осью делать ничего не нужно. Зато ось X у нас вместо дат показывает значения. Исправим это. В группе радиокнопок Axis выберем Bottom (нижняя ось), и перейдем на внутреннюю вкладку Labels. В разделе Style вместо Auto выберем Value, что изменит надписи к точкам оси X: вместо назначаемых автоматически, мы четко указали, что нужно взять значение поля, то есть, дату. В результате этих манипуляций мы получим уже достаточно привлекательный график:
105
Рис. 13.7. График курсов валют Сохраните проект, скомпилируйте и загрузите полученную программу. Посмотрим, что умеет делать этот график в рабочем приложении. Прежде всего, если вы выделите какой то участок графика левой кнопкой мыши слева-направо и сверху-вниз, выделенный фрагмент увеличится во все окно графика. Ухватившись за график правой кнопкой мыши, его можно будет перемещать. Затем вы можете сделать обратное выделение левой кнопкой любого участка графика снизу-вверх и справа-налево, и масштаб графика восстановится. Изменить масштаб программно можно с помощью свойства Zoom объекта View3DOptions графика: DBChart1.View3DOptions.Zoom:= 100;
Это целое число, содержащее процент масштаба. Значение 100 соответствует нормальному масштабу. Попробуйте изменять масштаб от 1 до 500.
Печать графика Посмотреть, как реализована печать графика с предварительным просмотром можно уже на этапе конструирования, в редакторе серий. Для этого на вкладке Chart редактора серий перейдите на внутреннюю вкладку General и нажмите кнопку Print Preview. Вы получите такое окно:
106
Рис. 13.8. Окно предварительного просмотра перед печатью В этом окне можно указать используемый принтер, если он установлен на вашем компьютере, направление печати (книжный или альбомный вариант), установить поля, произвести дополнительную настройку и непосредственно дать команду на печать. Но конечный пользователь не имеет доступа к редактору серий, поэтому нам нужно вывести это окно программным путем. Для этого установите кнопку на верхнюю панель, которую мы оставили специально для этого. На кнопке напишите «Печать». Щелкните по ней дважды, чтобы сгенерировать обработчик нажатия. Однако, прежде чем вписывать в обработчик код, нам необходимо подключить модуль Teeprevi в верхний раздел uses, потому что именно в этом модуле описана процедура ChartPreview(), вызывающая данное окно. Напомню, что для этого нужно после последнего подключенного модуля поставить запятую, после чего вписать Teeprevi
а уже после этого модуля поставить точку с запятой. Код процедуры нажатия на кнопку будет содержать следующую строку: ChartPreview(Form1, DBChart1);
Как видим, процедура ChartPreview() имеет два параметра: форму, содержащую график, и компонент DBChart (если вы изменили имена формы или графика, установленные по умолчанию, то и здесь нужно будет указать их). Сохраните проект, скомпилируйте его и попробуйте нажать на кнопку. У вас должно выйти окно печати.
107
Основные методы и свойства DBChar На вкладке General, где мы вызывали окно печати, под кнопкой Print Preview имеется кнопка Export. Эта кнопка выводит следующее диалоговое окно:
Рис. 13.9. Окно экспорта графика Как мы видим, график (диаграмму) можно экспортировать в буфер обмена или графический файл одного из четырех форматов. Формат BMP наиболее универсален, но файл получается большого размера. WMF – формат метафайлов Windows, который обеспечивает хорошее качество изображения при сравнительно небольшом размере файла. EMF – такой же формат, как и WMF, но более новый, используемый в 32-разрядных Windows. Последний TEE формат – специализированный для TDBChart(TChart) формат, который сохраняет не только сам график, но и все его настройки, что в дальнейшем позволяет загрузить ранее сохраненный график в компонент TDBChart. Сохранение графика программным путем осуществляется следующими методами: Procedure SaveToBitmapFile(const Filename: String); Здесь график сохраняется в указанный в параметре файл формата BMP. Procedure SaveToMetafile(const Filename: String); График сохраняется в WMF формат. Procedure SaveToMetafileEnh(const Filename: String); График сохраняется в EMF формат. Procedure SaveChartToFile(AChart:TCustomChart; const AName: String); График сохраняется в специализированный формат. Первым параметром указывается сохраняемый график. Если вы будете использовать эту процедуру, не забудьте в раздел uses добавить модуль Teestore, где описан этот метод. Если вы сохранили график или диаграмму таким образом, то в дальнейшем вы можете и загрузить его методом Procedure LoadChartFromFile(AChart:TCustomChart; const AName: String); Следующая группа свойств позволяет вращать трехмерный график, изменять его масштаб и угол наклона, что позволяет нам в программе установить богатый инструментарий для пользователя, с помощью которого он сможет изменять вид графика. При разработке программы эти же инструменты доступны вам на вкладке Chart и на внутренней вкладке 3D. Многие возможности управления графиком зависят от того, включено ли свойство Orthogonal. При включенной ортогональности многие свойства становятся недоступными. Изменить состояние этого свойства можно просто: DBChart1.View3DOptions.Orthogonal:= False;
Изменить масштаб можно, присвоив свойству Zoom целое число: DBChart1.View3DOptions.Zoom:= 300;
108
Напомню, что нормальным масштабом является число 100. Пользователю можно дать возможность изменять масштаб от 1 до 500, еще больший масштаб будет уже неудобным. Свойство Rotation отвечает за горизонтальное вращение графика, и может быть целым числом от 0 до 360, это число указывает количество градусов: DBChart1.View3DOptions.Rotation:= 100;
Свойство Tilt отвечает за вертикальное вращение и также содержит целое число от 0 до 360 (градусов): DBChart1.View3DOptions.Tilt:= 120;
Свойство Elevation содержит целое число, указывающее наклон графика. Число может быть также от 0 до 360: DBChart1.View3DOptions.Elevation:= 50;
Свойство Perspective указывает соответственно, на перспективу графика. Это число удобней делать от 0 до 100: DBChart1.View3DOptions.Perspective:= 30;
Попробуйте сделать в проекте новое окно для настройки графика, и с помощью события onChange ползунков TrackBar изменять соответствующие свойства:
Рис. Окно настройки графика Напомню, что компонент TrackBar хранит значение в свойстве Position, а минимальное и максимальное значение можно установить при проектировании в свойствах Min и Max. Чтобы из главного окна можно было вызывать окно с настройками, а из окна настроек менять параметры графика, оба модуля придется подключить друг к другу командой File ->Use Unit. Еще раз напомню, что если ортогональность будет включена, пользователь сможет менять только масштаб графика, попытки изменить параметры вида графика ни к чему не приведут. 109
Лекция 14. Введение в клиент-серверные БД. InterBase. Как мы уже знаем, базы данных могут быть не только локальными или файл-серверными, но и клиент-серверными. При использовании архитектуры клиент-сервер, сами базы данных находятся на ПК, который выполняет роль сервера. При этом сервером называют не только сам компьютер, но и программу, которая обеспечивает работу с базами данных: позволяет подключаться к ним зарегистрированным пользователям, следит за целостностью и непротиворечивостью данных, имеет удобные средства для архивации и восстановления баз данных. В архитектуре клиент-сервер вся работа с данными распределяется между сервером, поставляемым независимыми разработчиками, и клиентским приложением, которое разрабатывается программистом для того или иного предприятия. Взгляните на рисунок:
Рис. 14.1. Организация архитектуры клиент-сервер На рисунке вы можете видеть, что сервер InterBase (или аналогичный) располагается на отдельном компьютере, вместе с самими данными. При этом не используется никаких открытых ресурсов (дисков, папок или файлов), обмен данными происходит только по специально выделенному порту. Давайте представим себе работу файл-серверной базы данных по сети. Компьютер, выполняющий роль сервера, не делает ничего, кроме обеспечения общего доступа к папке, в которой находится база данных. Пользовательский компьютер, обращаясь к какой-нибудь таблице из этой БД, вначале получает по сети всю таблицу, какой бы большой она ни была, и лишь затем получает возможность работать с загруженной копией. А если база данных имеет много таблиц? А если таблицы содержат сотни тысяч записей? А если клиентских компьютеров несколько десятков, а то и сотен? В этом случае сеть подвергается огромным перегрузкам, так как каждый клиент должен получить собственную копию таблицы, и не один раз за сеанс. Кроме того, работая с файл-серверной базой данных, клиентское приложение принимает на себя всю тяжесть обеспечения правильной работы с этой БД: данные должны быть полными и непротиворечивыми, удаляя какую то запись, следует удалять и все связанные с ней записи, другими словами, бизнес-правила осуществляет клиентский ПК. Все это приводит к тому, что не только сетевые каналы должны быть скоростными, но и компьютеры пользователей должны быть как можно мощнее. Следует помнить и об отсутствии безопасности файл-серверной архитектуры, ведь для обеспечения совместной работы нужно открыть общий доступ к данным, которые из-за этого могут быть кем-нибудь испорчены, намеренно или случайно. Работа клиент-серверной БД выглядит совершенно иначе. Компьютер, «выделенный под сервер», не только выполняет все необходимые работы по обслуживанию БД, он еще и обрабатывает запросы от клиентских ПК, и пересылает им не всю таблицу или связанные таблицы, а лишь те сведения, которые были запрошены. В результате многократно снижается нагрузка на сеть, а безопасность работы увеличивается: в файл-серверных БД очень сложно реализовать непротиворечивость данных, если несколько клиентов обращаются к одной записи. Клиент-серверная архитектура же пользуется транзакциями – пакетом запросов, который последовательно производит изменения БД и либо принимается, если все изменения записи подтверждены, либо отвергается, если хоть один запрос завершился неуспешно. Мощным можно оставить лишь один компьютер – сервер. Пользовательские же ПК могут быть неприхотливыми и недорогими. Таким образом, происходит разделение всей работы с базой данных на две части: обслуживание БД, и обслуживание клиентов. Первая часть возлагается на SQL-сервер, вторая – на клиентскую программу.
110
На рынке имеется немало SQL-серверов самых разных разработчиков. Какой из них выбрать – дело вкуса, но клиентскую часть нам придется создавать самим. Клиентское приложение, работающее с серверным процессом, может выполнять различные действия с базой данных:
Поиск в БД по заданному условию. Сравнение, сортировка и вывод данных в виде таблиц. Редактирование данных (изменение, добавление и удаление). Создание новой базы данных и ее структуры. Выполнение программного кода на стороне сервера. Обмен сообщениями с другими клиентами, которые в данный момент также подключены к серверу.
Программист, используя архитектуру клиент-сервер, должен быть также и неплохим администратором БД, то есть, он должен уметь устанавливать серверное программное обеспечение и обслуживать саму базу данных: делать резервные копии, удалять накопившийся «мусор», регистрировать новых пользователей и т.п.
InterBase InterBase представляет собой полнофункциональный SQL-сервер. Сервер баз данных – это программа или служба, которая выполняется на сетевом компьютере (сервере), где физически расположена сама база данных. На этом курсе мы изучим установку сервера InterBase версии 6.5, который входит в поставку Delphi 7. InterBase – очень надежный сервер БД, при этом он не требователен к ресурсам ПК, благодаря чему является одним из самых популярных SQL-серверов на рынке программного обеспечения. Благодаря тому, что InterBase обеспечивает автоматическое восстановление и готовность к работе после сбоев системы (пользователи часто даже не замечают, что у сервера были проблемы), он используется во многих военных проектах США. Во многом из-за этого InterBase так поздно появился на нашем рынке. InterBase выгодно отличается от многих других серверов следующими качествами:
Высокая производительность и надежность при минимальных требованиях к ПК. Поддержка стандарта SQL-92, что позволяет обеспечить переносимость программ. Относительно низкая стоимость продукта (с Delphi поставляется сервер InterBase с бесплатной лицензией на 5 клиентов, этого достаточно для разработки БД и приложения, но обычно недостаточно для развертывания сервера в организации). Простота управления и поддержки сервера. InterBase имеет простой и удобный механизм администрирования БД, не требующий специальных знаний.
В 1985 году сервер носил название GDS (Groton Database System), но вскоре был переименован в InterBase. В 1991 году сервер был перекуплен фирмой Aston Tate, но уже в 1992 году вместе с фирмой сервер перешел во владение корпорацией Borland. Начиная со второй версии Delphi, дистрибутив включает в себя бесплатную локальную версию сервера InterBase. Поскольку InterBase является «родным» для Delphi сервером БД и не требует для своей работы установки дополнительных драйверов, а также, принимая во внимание все вышесказанное, мы остановимся именно на нем. Средств самой Delphi вполне достаточно для программирования приложений, работающих с InterBase, однако имеются разработки и сторонних производителей – компоненты, программы для облегчения администрирования БД и т.д. Предполагается, что при установке Delphi вы также установили и InterBase Server. Впрочем, если это не так, то вставьте дистрибутивный диск и установите InterBase 6.5 Server:
111
Рис. 14.2. Выбор установки сервера в поставляемом дистрибутиве Delphi Если же вы не знаете, установлен ли у вас уже InterBase, достаточно посмотреть в список меню «Программы», где он должен присутствовать отдельной папкой. Тут следует сделать одно замечание: если вы используете ОС Windows NT, 2000 или XP, то InterBase может запускаться как служба (по умолчанию) или как приложение. В случае Windows 95, 98 или ME InterBase запускается только как приложение. Вне зависимости от того, какая ОС у вас установлена, если сервер запущен как приложение, в правом нижнем углу (в трее) вы увидите значок InterBase Guardian:
Рис. 14.3. Значок InterBase Guardian InterBase Guardian – утилита, которая устанавливается вместе с сервером. Эта утилита осуществляет начальный запуск сервера, и его перезапуск, если по каким то причинам сервер «рухнул». Если же у вас установлена Windows NT, 2000 или XP, то загрузите Панель управления (Пуск -> Настройки -> Панель управления). Среди прочих имеющихся служб вы увидите и InterBase Manager:
Рис. 14.4. Панель управления в Windows XP SP-2 Щелкните дважды по этой службе, чтобы открыть ее. Вы увидите следующее окно:
112
Рис. 14.5. Окно службы InterBase Manager В группе Startup Mode этого окна вы можете выбрать одну из радиокнопок: Automatic (Сервер запускается автоматически) и Manual (Сервер запускается вручную). Если вы установили InterBase на ПК, который действительно будет сервером, то лучше оставить включенной кнопку Automatic. Но если же это ваш рабочий ПК, на котором вы лишь разрабатываете приложение, используя локальный сервер, то запускать его лучше вручную. Дело в том, что запущенный сервер пусть немного, но отнимает оперативную память. Кроме того, сервер постоянно «прослушивает» свой порт, по которому к нему может обращаться клиентское приложение, что также незначительно снижает производительность ПК. Данные между компьютерами передаются «пакетами», которые в служебной части содержат и номер порта. Порт – это целое число, которое используется при приеме и передаче данных для идентификации процесса (программы), которая этими данными обменивается. Например, протокол HTTP использует порт 80. Сервер InterBase использует порт 3050. (Все установленные порты описаны в файле SERVICES, расположенном в одном из папок Windows. Для Windows XP это адрес C:\WINDOWS\SYSTEM32\DRIVERS\ETC). Ниже расположен раздел Root Directory (корневая папка сервера). В этом разделе указан адрес, по которому была произведена установка InterBase. Еще ниже расположен раздел Status. Если сервер находится в рабочем состоянии, то зеленым цветом выводится Running (выполняется), а кнопка справа имеет название Stop (остановить). Если же сервер не работает, то красным цветом выводится надпись Stopped (остановлено), а кнопка справа содержит надпись Start (запустить). Вы можете безбоязненно попробовать нажимать на эту кнопку, запуская или останавливая сервер. «Галочка» Run the InterBase server as a service on Windows NT (Загружать сервер InterBase как службу Windows NT) позволяет вам указать способ загрузки сервера: как службу Windows (при отмеченном состоянии) или как простое приложение. Рекомендуется запускать сервер, как службу. В самом низу расположен раздел Properties (Свойства), где вы можете посмотреть или изменить текущие свойства сервера или служебной программы InterBase Guardian.
Регистрация сервера При использовании старых версий InterBase (до 5.5 включительно), для администрации сервера использовалась утилита InterBase Windows ISQL. Мы не будем рассматривать работу с устаревшим ПО, ведь в новых версиях InterBase (начиная с 6.0) используется утилита IBConsole, и маловероятно, что кто то еще использует старые версии. IBConsole – это графическая оконная утилита, с помощью которой можно выполнять множество операций: регистрировать и конфигурировать серверы, создавать и администрировать базы данных, добавлять и удалять зарегистрированных пользователей, имеющих доступ к базам данных, а также запускать запросы SQL в интерактивном режиме. Существует 113
множество утилит для работы с сервером InterBase, выполненных сторонними разработчиками, которые более удобны для работы с базами данных, например InterBase Expert, IBManager, IBAdmin. Но IBConsole – «родная» утилита InterBase, ее не нужно искать и устанавливать специально, поэтому мы будем использовать именно ее. IBConsole устанавливается вместе с сервером и запускается из папки InterBase (Пуск -> Программы -> InterBase). При первом запуске окно IBConsole выглядит так:
Рис. 14.6. Утилита IBConsole Дерево серверов в левой части окна содержит только корневую ветвь «InterBase Servers» (Серверы InterBase), но не содержит пока ни одного сервера. Для начала работы требуется зарегистрировать хотя бы один сервер (служба InterBase должна быть запущена). Выберите команду меню Server -> Register или нажмите в панели инструментов первую кнопку «Register a new InterBase Server». Появится окно регистрации нового сервера:
Рис. 14.7. Окно регистрации нового сервера 114
В верхней части окна необходимо выбрать расположение сервера: Local (локальный, установленный на этой же машине) или Remote (удаленный, расположенный на другом ПК в сети). В случае использования локального сервера все просто: достаточно будет указать только User Name (имя пользователя) и Password (пароль). При выборе удаленного сервера придется также указать имя компьютера, на котором находится сервер и используемый сетевой протокол. Данный курс не ставит целью изучение сетевой архитектуры, однако необходимо коротко прояснить некоторые вопросы. Прежде всего, в сетях обычно используют протокол TCP/IP (Transmission Control Protocol / Internet Protocol – Протокол контроля передачи данных / Интернет протокол). Каждый компьютер в сети имеет собственный уникальный IP-адрес, состоящий из четырех цифр от 0 до 255, разделенных точками, например: 120.0.0.5 В строке Server Name можно указать имя компьютера в сети либо его сетевой адрес. Вы должны быть уверены, что удаленный компьютер, который вы регистрируете, работает, подключен к сети и на нем имеется запущенный сервер InterBase. Помимо этого, при регистрации удаленного сервера, потребуется указать Alias Name – псевдоним, под которым будет зарегистрирован удаленный сервер в дереве серверов на IBConsole (при регистрации локального сервера этого делать не нужно). Однако в качестве удаленного сервера можно указать и локальный, расположенный на этом же ПК. Дело в том, что во всех операционных системах принято, чтобы IP-адрес 127.0.0.1 был зарезервирован для служебных нужд и указывал на локальный компьютер. Другими словами, если вы обращаетесь на адрес 127.0.0.1, то обращаетесь сами к себе. А зарезервированное имя, по которому ПК может обращаться к самому себе, как к сетевому компьютеруlocalhost В Windows эти данные прописаны в файле hosts, который содержит сопоставления IP-адресов именам узлов. Если у вас Windows XP, то этот файл находится по адресу C:\WINDOWS\system32\drivers\etc Вы также самостоятельно можете найти этот файл, с помощью команды меню Пуск -> Найти. Сейчас мы рассмотрим регистрацию локального сервера. Для этого в верхней части окна оставим выделенным Local Server, а в строках User Name и Password напишем: SYSDBA masterkey «SYSDBA» – это имя администратора БД по умолчанию, регистр символов здесь не имеет значения. А «masterkey» – пароль администратора, здесь регистр букв имеет значение: пароль нужно вводить маленькими буквами. Вообще-то, хотя пароль может содержать до 32 символов, значимыми являются только первые 8, так что можно ввести просто «masterke». Для реального сервера рекомендуется изменить пароль администратора сразу после установки InterBase (в примерах последующих лекций пароль «masterkey» не меняется). В любом случае, чтобы зарегистрировать нового пользователя, нужно будет войти в систему под именем SYSDBA. Как только вы нажмете кнопку OK, то зарегистрируете локальный сервер. В дереве серверов IBConsole появится вложенная ветвь Local Server:
115
Рис. 14.8. Зарегистрированный локальный сервер При выборе команды «Logout» во всплывающем меню, или двойном щелчке по разделу «Logout» в колонке «Action» правого окна, вы завершаете текущий сеанс работы. Ветвь «Local Server» закроется, но сам сервер будет продолжать свою работу:
Рис. 14.9. Сеанс подключения к серверу завершен. Чтобы вновь подключиться к серверу, следует выбрать команду «Login» (можно просто дважды щелкнуть по «Local Server»), вписать имя пользователя и пароль, нажать кнопку «Login»:
Рис. 14.10. Подключение к серверу. 116
Подключаться к серверу придется и при каждой загрузке утилиты IBConsole. Следует отметить один момент: локальный сервер может быть только один! Другими словами, если вы попытаетесь зарегистрировать еще один сервер, то пункт «Local Server» будет уже недоступен:
Рис. 14.11. Недоступность локального сервера (локальный сервер уже зарегистрирован) Если вам нужно зарегистрировать локальный сервер заново, то предыдущую регистрацию следует вначале удалить. Для этого нужно щелкнуть правой кнопкой по строке «Local Server» (рис. 14.7) и отключить его командой «Logout», а затем удалить регистрацию командой «Un-Register». После этих действий вы вновь получите возможность зарегистрировать новый локальный сервер. Удаленных же серверов может быть множество.
Регистрация нового пользователя Если у вас в окне с деревом выбран Local Server, как на рисунке 14.7, то в правой части в колонке Action вы видите список доступных команд. Выполнить любую команду можно, дважды щелкнув по ней мышью. Щелкните дважды по команде User Security или выполните команду меню Server -> User Security. Выйдет окно диалога безопасности:
117
Рис. 14.12. Окно диалога безопасности Здесь вы можете изменить пароль у выбранного пользователя или зарегистрировать нового пользователя, нажав на кнопку New. Нажмите на эту кнопку, затем в строке User Name впишите PUPKIN а в строках Password (пароль) и Conform Password (подтверждение пароля) напишите qwerty Сразу заметим, что выбор простых паролей недопустим с точки зрения безопасности, хотя на практике нередко встречаются такие пароли, как «1», «111» и т.п.; также нередко пароль пользователя совпадает с его именем. Конечно, такие пароли проще запомнить конечному пользователю, но имейте в виду, что при этом вы не сможете ручаться за безопасность данных! Так что в реальных серверах используйте более сложные пароли. Нажмите кнопку Apply (применить), которая стала доступна. Как только регистрация нового пользователя закончилась, нажмите кнопку Close. А в дереве серверов выделите раздел Users (пользователи):
Рис. 14.13. Вывод сведений о зарегистрированных пользователях Как видно в правой части окна, мы получили нового пользователя PUPKIN. 118
Лекция 15. Технические характеристики. Создание и перенос базы данных. Основные технические характеристики InterBase – неприхотливый сервер. Он может работать на платформах различных операционных систем (Windows, Unix, Solaris и др.), программы для работы с ним можно разрабатывать на различных компиляторах (Delphi, Borland C++ Builder, Microsoft Visual C++ и др.). Системные требования к ПК на платформе Windows следующие: Память минимум 16 Мб (для сервера рекомендуется 64 Мб). Процессор 486 DX2 66 MHz минимум (для сервера рекомендуется Pentium 100 MHz или выше). Примерно 30 Мб на диске, не считая самой базы данных Таким образом, на современных компьютерах InterBase не будет испытывать никаких недостатков, для комфортной работы с ним достаточно даже слабого Pentium-III. Основные технические характеристики самого сервера указаны в таблице 15.1: Таблица 15.1. Технические характеристики InterBase Характеристика Значение Максимальный размер одной БД Рекомендуется не более 10 ГБ Максимальное количество таблиц в одной БД 65 536 Максимальное количество полей в таблице 1 000 Максимальное количество записей в таблице Не ограничено Максимальная длина записи 64 Кб (кроме BLOB-полей) Максимальная длина поля 32 Кб (кроме BLOB-полей) Максимальная длина BLOB-поля Не ограничено Максимальное количество индексов 65 536 Максимальное количество полей в индексе 16 Максимальный уровень вложенности SQL-запроса 16 Максимальный размер триггера или хранимой 48 Кб процедуры Из приведенных выше характеристик видно, что InterBase способен удовлетворить требованиям практически любой базы данных. Ну а если этих характеристик все-таки будет недостаточно, на серверном ПК можно хранить множество баз данных, и сервер InterBase сможет обслуживать их!
Создание базы данных Каждый зарегистрированный сервер, как локальный, так и удаленный, может содержать и обслуживать множество баз данных. База данных представляет собой единый файл, который имеет расширение *.gdb. Все таблицы, индексы, генераторы, триггеры и т.д. хранятся в этом файле, что облегчает процесс резервного копирования БД. Перед созданием новой базы данных необходимо создать папку, в которой она будет храниться, утилита IBConsole не создает папки на диске, так что для этого придется использовать средства Windows или файловый менеджер. Создайте папку C:\DataBases Далее откройте утилиту IBConsole, подключитесь к локальному серверу (команда «Login»). Выберите команду «Database -> Create Database». Откроется окно такого вида:
119
Рис. 15.1. Создание новой базы данных В выделенной строке Filename(s) следует вписать путь и имя создаваемого файла. Впишите C:\DataBases\first.gdb В строку Size (Pages) ничего вписывать не нужно. Далее следует раздел Options, ниже следует описание пунктов этого раздела.
Размер страницы Пункт Page Size указывает размер страницы в базе данных, по умолчанию он равен 4096 байт. InterBase позволяет использовать следующие размеры страниц (в байтах): 1024 2048 4096 8192 Дело в том, что вся база данных в InterBase разбивается на страницы фиксированного размера, а при работе с БД данные считываются постранично. Размер страницы указывается в байтах. Если выбрать слишком маленький размер страницы, то записи большой длины могут занимать более одной страницы, и серверу придется делать больше операций чтения, что плохо скажется на производительности сервера. С другой стороны, выбирать слишком большой размер страницы также не рекомендуется, так как в этом случае сервер при запросе клиента будет считывать много лишних данных, которые размещаются на той же странице. Локальный или удаленный сервера могут содержать множество баз данных, и у этих БД может быть установлен различный размер страниц. Существуют следующие рекомендации по выбору размера страниц: 120
Для дисков с файловой системой NTFS можно оставить размер по умолчанию: 4096, или установить 8192. Для дисков с файловой системой FAT32 следует выбрать больший размер страницы: 8192. Для примера мы оставим размер 4096.
Кодировка по умолчанию InterBase имеет множество кодировок – наборов символов того или иного языка. Можно указывать нужную кодировку при создании каждого текстового поля отдельно, а можно указать ее в поле Default Character Set раздела Options при создании новой базы данных, или при регистрации существующей. В последнем случае вы определяете кодировку «по умолчанию» для всей базы данных: в дальнейшем при создании любого текстового поля эта кодировка будет использована автоматически. Впрочем, можно указать одну кодировку «по умолчанию», и другую – при создании текстового поля. В этом случае приоритет будет за кодировкой, указанной явно при создании текстового поля. Если вы планируете использовать символы только русского и английского языков, при создании базы данных выбирайте кодировку WIN1251. Если же вы не знаете заранее, какую кодировку будете использовать, можете оставить NONE, то есть, неопределенная кодировка. Затем нужные кодировки можно прописывать вручную для каждого поля. В нашей базе данных мы выберем кодировку WIN1251.
Диалект В поле SQL Dialect можно выбрать либо первый, либо третий диалект SQL. Второй диалект является промежуточным и в списке диалектов отсутствует. Третий диалект отличается от первого более строгими правилами и расширенным набором типов данных, таких как типы для работы с большими целыми числами, типы Date и Time. Кроме того, в третьем диалекте различается регистр символов идентификатора, если последний заключен в двойные кавычки. То есть, ‘MyTable’ и ‘MYTABLE’ в обоих диалектах равны, а вот “MyTable” и “MYTABLE” в третьем диалекте различаются, а в первом – нет. Наконец, третий диалект не поддерживает неявное преобразование типов: в первом диалекте выражение: ‘10’ + 2 будет корректным и вернет значение 12, а в третьем диалекте мы получим ошибку несоответствия типов. Рекомендации здесь следующие: для более корректной работы базы данных желательно выбрать третий диалект, но если вы собираетесь использовать совместимость со старыми механизмами доступа к данным, такими как BDE, то выбирать желательно первый диалект. Впрочем, это только рекомендации. Выберите для нашей БД третий диалект. Оставьте галочку Register database (Регистрация базы данных), а в поле Alias впишите псевдоним нашей базы: first. Нажмите кнопку «OK», и в результате будет создан файл C:\DataBases\first.gdb который и является базой данных. Псевдоним базы данных появится в дереве серверов:
121
Рис. 15.2. Новая база данных в локальном сервере
Регистрация базы данных Может случиться, что вам придется не создавать новую базу данных, а зарегистрировать уже имеющуюся, например, при переустановке InterBase. Другими словами, файл *.gdb с какой-то базой данных у вас уже есть, требуется лишь зарегистрировать его в локальном или в удаленном сервере. Для демонстрации этой возможности придется вначале убрать регистрацию нашей БД first.gdb. Щелкните правой кнопкой мыши по псевдониму first в дереве серверов и выберите команду Disconnect для закрытия БД (с открытой БД нельзя снять регистрацию) и подтвердите закрытие БД. Значок псевдонима вместо зеленой галочки отметится красным крестиком, сообщая, что БД закрыта. Снова щелкните правой кнопкой по этому псевдониму и выберите команду Unregister (Удаление регистрации), подтвердите команду. После этого вы увидите, что локальный сервер больше не содержит никакой БД:
Рис. 15.3. Отсутствуют зарегистрированные базы данных 122
Между тем, файл first.gdb на диске остался. Теперь нам нужно снова зарегистрировать его. Делается это командой меню Database -> Register. Вы увидите окно регистрации БД:
Рис. 15.4. Окно регистрации БД В разделе Database в первом поле File щелкните по кнопке справа и найдите наш файл, при этом имя файла, включая расширение, автоматически появится во втором поле Alias Name. Чтобы воссоздать предыдущие настройки, уберите точку и расширение из имени алиаса, оставив только имя FIRST. Здесь же следует заметить, что если вы регистрируете базу данных на удаленном сервере, то в поле File вам потребуется указать реальный адрес и имя файла БД, который находится на удаленном компьютере. Предположим, что на ПК «А» установлен и запущен сервер InterBase, причем база данных там находится по адресу C:\DATABASES\SomeBD.gdb Вы регистрируете эту БД на компьютере пользователя «Б». Удаленный сервер, как говорилось в прошлой лекции, у вас уже зарегистрирован, там вы указывали IP-адрес компьютера «А». Теперь вам нужно зарегистрировать базу данных. Когда вы даете команду на регистрацию базы данных, в дереве серверов на IBConsole у вас должен быть выделен этот удаленный сервер. В поле File вы указываете строку C:\DATABASES\SomeBD.gdb причем делать это нужно вручную, так как кнопка справа от поля, открывающая диалог выбора файла, будет недоступна. Это и неудивительно: как правило, на серверах БД в целях безопасности не открывают прямой доступ к диску или папке с базой данных (в отличие от файл-серверных БД). Указав адрес и имя файла, вы можете оставить полученный псевдоним базы данных без изменений, или же можете изменить его. 123
Далее переходим в раздел Login Information. Здесь первая строка должна содержать имя пользователя, а вторая – его пароль. Вписываем SYSDBA masterkey (если вы еще не меняли пароль администратора) В нижнем поле Default Character Set выбираем кодировку WIN1251 и нажимаем кнопку «OK». Все, база данных вновь зарегистрирована и открыта.
Перенос базы данных Под переносом базы данных здесь подразумевается преобразование БД из локальных технологий (dBase, Paradox, MS Access или из файлов MS Excel) в клиент-серверную БД InterBase. Вообще-то, во избежание ошибок, делать этого не рекомендуется: если вы сразу создадите БД в InterBase, то не получите возможных проблем несовпадения типов данных. Однако как быть, если БД уже существует и содержит не одну тысячу записей? Придется такую базу переносить. Делается это утилитой Datapump, которая входит в поставку Delphi, устанавливается вместе с ней и загружается из того же раздела меню, что и Delphi. Для переноса необходимо, чтобы приемник данных имел зарегистрированный в системе псевдоним. Желательно, чтобы и источник данных имел псевдоним, но без этого можно обойтись. Мы будем переносить базу данных Menu, которую разрабатывали в 5-6 лекциях. Сама база данных должна находиться в папке C:\Menu и иметь псевдоним MenuParadox. Если у вас этого нет, то вернитесь на 5 лекцию, создайте и зарегистрируйте базу данных, как там описано. Далее нам нужно будет зарегистрировать псевдоним нашей базы данных First.gdb. Это можно сделать в утилите SQL Explorer, которая устанавливается вместе с Delphi и запускается из меню Пуск -> Программы -> Borland Delphi 7 -> SQL Explorer
Рис. 15.5. Утилита SQL Explorer Щелкните правой кнопкой по свободному месту окна с базами данных и выберите команду New. В поле Database Driver Name нужно будет выбрать «INTERBASE» и нажать кнопку «OK». Будет создан 124
новый псевдоним с названием по умолчанию INTERBASE1, который мы изменим на FirstIB. В правом окне вы увидите параметры псевдонима, некоторые из них нужно будет изменить. Вы помните, что в базе данных MenuParadox мы изменили кодировку на Pdox ANSI Cyrillic? Эту же кодировку нужно будет выбрать в строке LANGDRIVER. Затем в строке SERVER NAME нужно будет выбрать файл с БД: C:\DATABASES\FIRST.GDB И, наконец, в строке USER NAME нужно будет вписать логин администратора SYSDBA:
Рис. 15.6. Регистрация псевдонима Далее щелкните правой кнопкой по окну и выберите команду Apply (Применить). Псевдоним будет создан. Можете закрывать SQL Explorer, он больше не нужен. Теперь переходим к утилите DataPump. После запуска утилита выводит поочередно несколько окон. В первом окне вам предлагается выбрать источник данных по псевдониму (Select by alias name) или по месту размещения (Select by directory). Выберем последний вариант. Получим такое окно:
125
Рис. 15.7. Data Pump – утилита переноса БД Найдем и выберем папку C:\Menu и нажмем кнопку «Next». В следующем окне предлагается выбрать псевдоним приемника данных. Выберем FirstIB и нажмем «Next». Сервер InterBase защищает данные от несанкционированного доступа, поэтому вам придется ввести пароль (masterkey). В следующем окне нужно выбрать таблицы, подлежащие переносу. Щелкнем по кнопке с двумя знаками «>>», чтобы перенести все таблицы:
Рис. 15.8. Выбор таблиц для переноса.
126
Снова нажмем на кнопку «Next». Следующее окно сообщает об изменениях, которые будут сделаны при переносе. Изменения могут касаться типов полей, индексов и ссылочной целостности данных:
Рис. 15.9. Изменения, которые произойдут при переносе Осталось только нажать на кнопку «Upsize», чтобы завершить перенос. Последним выйдет окно с отчетом о переносе, закройте его кнопкой «Done». Данные перенесены. Убедиться в этом можно, открыв утилиту IBConsole. Войдите в локальный сервер, откройте БД FIRST и выделите пункт Tables:
Рис. 15.10. Перенесенные таблицы 127
В правой части окна вы видите, что база данных теперь содержит две таблицы: FOOD и TIPS. Щелкните по любой из них правой кнопкой и выберите команду «Properties». Затем перейдите на вкладку «Data» и вы увидите данные, которые хранились в выбранной таблице. Разумеется, эти базы данных не идентичны, в InterBase, например, нет таких типов, как «Счетчик» и «Логический», есть и другие отличия. Но об этом речь пойдет в следующих лекциях. Базы данных MS Access переносятся примерно также, однако если у вас названия полей и таблиц были выполнены кириллицей, вы получите массу ошибок, ведь InterBase не допускает русских идентификаторов.
128
Лекция 16. Типы данных. Домены. Типы данных Большинство SQL-серверов имеют схожие типы данных, хотя имеются и различия. После того обилия типов, к которому мы привыкли в базах данных Paradox или MS Access, может показаться, что SQL-серверы имеют очень ограниченный набор типов данных. Связано это в первую очередь, с ограничениями стандартов языка запросов SQL. Например, InterBase не имеет: Тип автоинкремент (автоматически увеличивающееся числовое поле). Нехватку такого важного типа можно компенсировать специальными числовыми генераторами. Тип Boolean. Вместо него предлагается использовать символьный тип данных Char(1). Тип Currency. Денежные типы данных придется хранить в обычных столбцах с вещественным типом данных. В InterBase все настолько взаимосвязано, что очень сложно говорить о какой-то одной теме, не затрагивая при этом другие. На этой лекции мы коротко затронем вопрос создания таблиц, хотя полностью развернем эту тему позже. Таблицы создаются с помощью специального SQL-запроса, и для этого проще всего использовать утилиту для работы с базами данных. Если InterBase сервер у вас не запущен, запустите его. Также откройте утилиту IBConsole, войдите в локальный сервер и откройте базу данных FIRST. На панели задач находится кнопка Interactive SQL, которая запускает встроенную утилиту для редактирования выделенной базы данных:
Рис. 16.1. Значок утилиты «Interactive SQL» Нажав на эту кнопку, вы увидите окно Интерактивного SQL:
129
Рис. 16.2. Interactive SQL На рисунке указаны основные кнопки, с которыми вам придется работать, и два окна: для ввода запроса, и для вывода результатов запроса. Строка состояния содержит адрес и имя текущей базы данных, в нашем случае это C:\DataBases\first.gdb. Все запросы, которые мы будем вводить, будут относиться к этой БД. Еще следует заметить, что в синтаксисе запросов не обязательно использовать заглавные буквы для ключевых слов, однако мы будем придерживаться рекомендаций SQL по этому вопросу.
Целые числа InterBase поддерживает два типа целых чисел: SMALLINT – Короткое целое число (2 байта) со знаком. Диапазон значений от -32768 до 32767. INTEGER (INT) – Длинное целое число (4 байта) со знаком. Диапазон значений от – 2147483648 до 2147483647. Здесь рекомендации обычные: экономить на дисковом пространстве для чисел не стоит, поэтому используйте SMALLINT только в тех полях, которые никогда не будут содержать больших чисел. Во всех остальных случаях рекомендуется использовать длинное целое INTEGER, которое также можно обозначать сокращенным синтаксисом INT. Заметим здесь же, что длинное целое INTEGER также используется для ключевых автоинкрементных полей, идентифицирующих запись, а роль счетчика при этом играют генераторы. В окне для запросов впишите следующий запрос:
и нажмите кнопку Execute Query (Выполнить запрос). Этим запросом мы дали команду создать таблицу с именем «Table_Cel», которая содержит два поля: «Korotkoe», имеющее тип SMALLINT, и «Dlinnoe» с типом INTEGER. Если вы все сделали правильно, запрос исчезнет. Далее можно закрыть Interactive SQL или просто перейти на основное окно IBConsole. В дереве серверов выделите раздел Tables базы данных FIRST, и в правом окне увидите таблицу TABLE_CEL:
Рис. 16.3. Новая таблица создана. Чтобы окончательно убедиться, что все получилось правильно, щелкните правой кнопкой по получившейся таблице и выберите команду Properties. Выйдет окно со списком полей на вкладке Properties. Вкладка Metadata содержит запрос, по которому была создана таблица, а на вкладке Data можно увидеть записи таблицы:
Рис. 16.4. Отображение вкладки Metadata свойств таблицы TABLE_CEL Таким же образом вы сможете создавать следующие таблицы и контролировать правильность их параметров. Выполняйте все приводимые примеры создания таблиц, так как некоторые из них понадобятся нам в следующих лекциях.
131
Вещественные числа К вещественным типам относятся: FLOAT – Число с плавающей точкой одинарной точности (4 байта). Диапазон от 3,4*10-38 до 3,4*10+38. Значащих цифр 7. DOUBLE PRECISION – Число с плавающей точкой двойной точности (8 байт). Диапазон от 1,7*10-308 до 1,7*10+308. Значащих цифр 15. Типа данных REAL в InterBase не существует, однако попытка создать поле такого типа не приведет к ошибке, вместо этого InterBase создаст поле типа FLOAT. Попытки создать поле других вещественных типов, которые имеются в Delphi, приведут к ошибке. Тип FLOAT не рекомендуется использовать там, где нужна точность расчетов дробных значений, особенно в денежных полях. Вместо привычного типа CURRENCY, которого нет в InterBase, лучше подойдет тип DOUBLE PRECISION. Пример: CREATE TABLE Table_Vesh( Kol_Float FLOAT, Kol_Double DOUBLE PRECISION) Здесь первое поле может содержать вещественные числа, имеющие не более 4 знаков после запятой. При попытке сохранить число «1,234567» реально будет записано число «1,2345». Второе поле обеспечивает большую точность сохраняемых данных. В вещественных полях нельзя управлять количеством цифр после запятой, поэтому если вам потребуется, выводить только две цифры, в программе придется использовать маску ввода, например «#,###.##».
Числа с фиксированной точкой Таких типов два: DECIMAL и NUMERIC, оба типа данных практически равнозначны. Не самые удобные в использовании типы. Они призваны задавать фиксированное количество чисел после запятой. Как известно, вещественные числа определяются двумя значениями: Размер – общее количество цифр. Точность – количество цифр после запятой. DECIMAL / NUMERIC (Размер, точность) – Числа с плавающей точкой переменной точности. Оба типа равнозначны и могут использоваться с одинаковым результатом. Размер (от 1 до 15) указывает число значащих цифр. Точность (от 0 до 15) указывает число знаков после запятой. Точность должна быть меньше или равна размеру. Например, Поскольку специальных типов DECIMAL и NUMERIC на самом деле не существует, вместо них используются другие типы столбцов. Правила таковы:
При указании размера числа от 1 до 4 будет использован столбец SMALLINT. При указании размера числа от 5 до 9 будет использован столбец INTEGER. При указании размера числа от 10 до 15 будет использован столбец DOUBLE PRECISION.
Как видите, в первых двух случаях используются целые типы столбцов, и если даже вы укажете точность, числа после запятой будут утеряны. Следовательно, чтобы сохранить вещественное число в полях DECIMAL и NUMERIC, требуется указать размер не менее 10 чисел. Пример запроса: CREATE TABLE Table_Fiks( Kol_Dec DECIMAL(15,2), Kol_Dec2 NUMERIC(8,2)) 132
При, казалось бы, похожем объявлении в первом случае мы получили вещественное поле, в котором может храниться число типа 9999999999999,99, зато второе поле сможет хранить только целую часть числа. То есть, при попытке сохранить число 123456,78, в таблице останется число 123456, а поле будет иметь тип INTEGER.
Даты и время InterBase поддерживает три типа данных, работающие с датой и временем, причем применение этих типов напрямую зависит от используемого в базе данных диалекта SQL. TIMESTAMP – тип данных, совместимый с типом TTimeStamp в Delphi. Содержит одновременно и дату, и время в виде двух целых 32-битовых чисел. Первое число содержит порядковый номер дня, прошедший с 01.01.0001. Второе число указывает количество миллисекунд, прошедших с полуночи. Этот тип может применяться как в первом, так и в третьем диалектах SQL. DATE – тип данных для хранения значений только дат. Диапазон от 1 января 0001 г. до 31 декабря 9999 г. Поддерживается только в базах данных, использующих третий диалект SQL. Попытка создать поле типа DATE в базах данных с первым диалектом ошибки не вызовет, однако реально будет создано поле типа TIMESTAMP. TIME – тип данных для хранения значений только времени. Диапазон времени от 00:00 до 23:59:59:9999. Поддерживается только в базах данных, использующих третий диалект SQL. Попытка создать поле типа TIME в базах данных с первым диалектом приведет к ошибке. Обычно данные этих типов заполняются на компьютере клиента, а там все зависит от настроек операционной системы. Не следует забывать, что даты на компьютере пользователя могут выводиться по-разному, в зависимости от настроек ОС. Например, в английской вариации дат вначале указывается месяц, затем день, и год, тогда как у нас первым идет день. Кроме того, английская (американская) неделя начинается не с понедельника, а с воскресенья. А разделителем значений могут быть и точка «.», и слеш «/», и тире «-». Чтобы избежать конфликтов при работе вашей программы (клиентской части), полезно сразу программно переустановить формат даты и времени на компьютере клиента в привычный российский формат (пример ниже приводится для программ Delphi, а не для Interactive SQL): DateSeparator := '.'; ShortDateFormat := 'dd.mm.yyyy'; ShortTimeFormat := 'hh:mm:ss'; После этого в клиентской части программы дату и время спокойно можно вводить в привычном для нас формате. Пример создания таблицы с полями даты и времени (пример корректен только для 3 диалекта SQL): CREATE TABLE Table_DateTime( Kol_Date DATE, Kol_Time TIME, Kol_TimeStamp TIMESTAMP)
Текстовые типы Существует два текстовых типа данных: CHARACTER(n) (в сокращенном варианте CHAR(n)) и CHARACTER VARYING(n) (в сокращенном варианте VARCHAR(n)). n – указывает количество символов в поле. В обоих типах n может быть от 1 до 32767 символов (32 Кбайт). Если n не указать, по умолчанию будет присвоено число 1. Пример создания полей: 133
CREATE TABLE Table_Text( Kol_Char CHAR, Kol_Varchar VARCHAR(255)) В первом случае будет создано поле размером 1 символ, во втором случае в поле можно сохранить текст размером до 255 символов. Между этими типами данных есть отличия. Тип CHAR предназначен для хранения текста фиксированной длины. Другими словами, если мы определим поле в 10 символов, а запишем только 5, то текст будет дополнен завершающими пробелами до полной длины. Тип VARCHAR хранит текст переменной длины, и возвращает сохраненный текст без завершающих пробелов. Тем самым, использование типа VARCHAR в большинстве случаев является предпочтительным. Еще важными факторами при работе с текстовыми полями является кодировка символов и порядок их сортировки. Как мы знаем из прошлой лекции, при создании базы данных можно указать кодировку «по умолчанию», для кириллицы предпочтительней будет кодировка WIN1251. При этом все создаваемые текстовые поля будут иметь эту кодировку, в чем несложно убедиться, выполнив описанный выше запрос, и посмотрев вкладку Metadata созданной таблицы:
Рис. 16.5. Установки кодировки «по умолчанию» Как видно из рисунка, с помощью оператора CHARACTER SET кодировку можно указать явно при описании поля: CREATE TABLE Table_Text2( Kol_Char CHAR CHARACTER SET WIN1251, Kol_Varchar VARCHAR(255) CHARACTER SET ASCII) Если кодировка указана явно, именно она и будет использована при создании столбца таблицы. Для символьных полей также возможно указывать порядок сортировки COLLATE. Этот порядок определяет способ, по которому будут сортироваться и сравниваться текстовые данные при выводе их оператором SELECT. Для кодировки WIN1251 это может быть сортировка WIN1251 или PXW_CYRL. В первом случае русские символы сортируются как АБВ….эюя что не очень удобно. Сортировка PXW_CYRL предпочтительней, она выводит русские символы в таком порядке:
134
аАбБ…яЯ Пример: CREATE TABLE Table_Text3( Kol_Char CHAR CHARACTER SET WIN1251 COLLATE WIN1251, Kol_Varchar VARCHAR(255) CHARACTER SET WIN1251 COLLATE PXW_CYRL)
Тип данных BLOB BLOB (Binary Large Object – Большой двоичный объект) – Поле неограниченного размера, может содержать любой тип двоичных данных, например, файл с фотографией, мелодией или цифровой книгой, или просто большой текст (аналог MEMO). Пример определения поля: CREATE TABLE Table_BLOB( Kol_BLOB BLOB) Поля BLOB несколько отличаются по способу хранения данных от других типов данных. Все остальные поля записи хранятся на одной странице рядом, одно за другим. Поле BLOB сохраняет на этой странице только идентификатор записи, а сама запись хранится на отдельной странице (страницах). Таким образом, оказалось возможным хранить данные большого размера, не ухудшая в целом работу всей БД. У этого типа есть возможность указывать явно подтип: целое число, определяющее тип хранимых в поле данных. По умолчанию, 0 указывает на двоичные данные и 1 указывает на ASCII – текст. Программист может указывать собственные типы, используя отрицательные значения. Подтип указывается оператором SUB_TYPE: CREATE TABLE Table_BLOB2( Kol_BLOB BLOB SUB_TYPE -1) Однако заметим, что эта возможность InterBase практически не используется программистами.
Столбцы – массивы В InterBase столбец любого типа, кроме BLOB, может быть объявлен, как массив. Такие столбцы, вместо единичного значения записи, содержат массив значений одинакового типа. Доступ к каждому значению возможен с помощью указания индекса. По умолчанию, нумерация элементов массива начинается с 1, однако программист может явно указать границы элементов: CREATE TABLE ARRAY_TABLE( ARRAY1 INTEGER [10], ARRAY2 FLOAT [0:10, 5:10]); В этом примере была объявлена таблица из двух столбцов-массивов. В первом случае объявлен массив целых чисел с диапазоном от 1 до 10 элементов, во втором случае объявлен двухмерный массив вещественных чисел с указанными программистом границами элементов. Это эквивалентно объявлению в Delphi двух переменных-массивов: Var Array1 [1..10] of Integer; Array2 [0..10, 5..10] of Float;
Однако стандарт SQL-92, поддерживаемый InterBase, не поддерживает столбцов-массивов, поэтому доступ к отдельным значениям возможен только с помощью низкоуровневых функций API InterBase. Практически же такие столбцы используют крайне редко, ведь проще объявить, например, 135
три поля с целыми числами, чем одно целое поле-массив, имеющее три элемента. В этом курсе мы не будем рассматривать работу полей-массивов, однако знать о существовании такого типа нужно. Любителям выбирать путь посложней, рекомендуем обратиться к справочнику и технической документации InterBase.
Логический тип Как уже говорилось выше, в InterBase не поддерживаются типы Boolean. Вместо этого предлагается использовать тип CHAR(1), который создает односимвольный столбец, и вводить значения типа T/F, Y/N, Д/Н, 1/0, +/- и т.п. Проверка логики и правильности ввода значения возлагается на клиентское приложение.
Домены Доменом в InterBase называется заранее созданное описание столбца, которое хранится отдельно от таблиц. Зачем это нужно? Предположим, планируемая база данных будет содержать десятки таблиц. Предположим далее, что во многих из этих таблиц потребуются поля с фамилией, именем и отчеством каких то людей. Можно было бы описать эти поля обычным образом, например (следующий запрос вводить в Interactive SQL не нужно): CREATE TABLE Table_Firma( Familiya VARCHAR(20) CHARACTER SET WIN1251 COLLATE PXW_CYRL, Imya VARCHAR(20) CHARACTER SET WIN1251 COLLATE PXW_CYRL, Otchestvo VARCHAR(20) CHARACTER SET WIN1251 COLLATE PXW_CYRL) Но обратите внимание на то, что нам пришлось много раз повторить одно и то же описание типа поля. А если такой же тип будет содержаться и в других таблицах? Гораздо предпочтительней создать домен, который затем и указывать при описании поля. Домен создается таким образом: CREATE DOMAIN FIO AS VARCHAR(20) CHARACTER SET WIN1251 COLLATE PXW_CYRL Выполните последний запрос в Interactive SQL, затем в дереве серверов утилиты IBConsole выделите раздел Domains и вы увидите созданный нами домен. Теперь создание таблицы Table_Firma существенно упростится: CREATE TABLE Table_Firma( Familiya FIO, Imya FIO, Otchestvo FIO) Этот же домен можно использовать в описании полей любой таблицы в пределах текущей базы данных.
136
Лекция 17. Создание, модификация и удаление таблиц и представлений. Перед созданием таблиц программисту приходится выполнить ряд действий: спроектировать на бумаге саму базу данных, определить, какие таблицы в ней должны быть, нормализовать их, решить, как они будут называться, и какие столбцы в ней будут. Следует определиться со списком доменов и генераторов. Только после этого можно приступать к физическому созданию базы данных и доменов, после чего можно создавать таблицы, генераторы, индексы и т.д.
CREATE TABLE Как мы уже знаем из прошлой лекции, создание таблиц осуществляется запросом CREATE TABLE, который можно выполнить с помощью утилиты IBConsole. Полный синтаксис запроса такой: CTRATE TABLE Имя_таблицы [EXTERNAL [FILE] Имя_файла] (<описание_столбца_1> [, …, <описание_столбца_n>] | <ограничение_таблицы> …); Имя_таблицы – уникальный внутри базы данных идентификатор (имя) таблицы. Является обязательным. Нельзя допускать, чтобы такой же идентификатор был у других таблиц, представлений или процедур текущей базы данных. EXTERNAL [FILE] Имя_файла – необязательное определение того, что создается внешняя по отношению к базе данных таблица. “Имя_файла” при этом указывает адрес и имя файла создаваемой таблицы. Файла с таким именем не должно существовать на момент создания таблицы. В результате применения этого оператора будет создана внешняя по отношению к базе данных таблица, в текстовом формате ASCII. Обычно поля в таких файлах разделяются символом табуляции, а в конце записи ставится символ перевода строки. При этом созданная во внешнем файле таблица будет доступна в списке таблиц базы данных в утилите IBConsole. В основном, такие таблицы могут использоваться для обмена данными между разными БД, для сбора и обработки статистических данных, для сложной сортировки и т.п. Пример создания таблицы во внешнем файле (не забывайте, что служба InterBase должна быть запущена, утилита IBConsole загружена, и база данных First открыта): CREATE TABLE VneshTable EXTERNAL FILE 'C:\DataBases\VneshFile.tbl'( ID INTEGER, NAME VARCHAR(30))
<описание_столбца> - Описание столбца таблицы, которое может иметь как простой, так и достаточно сложный формат. Описание столбца имеет свой синтаксис: <описание_столбца> = имя_столбца {<тип_данных> | COMPUTED [BY] <домен>} [DEFAULT {<литерал> | NULL | USER}] [NOT NULL] [<ограничение_столбца>] [COLLATE collation]
<выражение> |
Давайте по порядку разберемся со всеми этими определениями. Как вы, вероятно, знаете, в синтаксисе различных языков программирования в фигурные скобки принято помещать список возможных параметров, из которого нужно выбрать. То есть, мы должны выбрать либо <тип_данных>, либо COMPUTED [BY], либо заранее созданный домен. О типах данных и их описаниях, равно как и о доменах, мы говорили на прошлой лекции. Теперь рассмотрим создание вычисляемого столбца.
137
Вычисляемые столбцы Вычисляемые столбцы в InterBase мало отличаются от вычисляемых столбцов в других базах данных, разве что вычисления производятся не на машине клиента, а на стороне сервера. Предположим, у нас имеется таблица сделок с названием товара, его стоимостью и количеством единиц этого товара, купленных каким то клиентом: Таблица 17.1. Описание таблицы сделок ID Идентификатор сделки (длинное целое) TOVAR Наименование товара (текст длиной 20 символов) ED_IZM Единица измерения (кг, штука, банка и т.п. – текст длиной 7 символов) STOIMOST Стоимость товара (вещественное число) KOLVO Количество проданных единиц товара (короткое целое) В данном случае хорошо бы также иметь сумму сделки, то есть умножить поле STOIMOST на KOLVO. В этом нам поможет вычисляемый столбец. Вот как можно реализовать данную таблицу: CREATE TABLE Sdelki ( ID INTEGER, TOVAR VARCHAR(20) CHARACTER SET WIN1251 COLLATE PXW_CYRL, ED_IZM VARCHAR(7) CHARACTER SET WIN1251, STOIMOST DOUBLE PRECISION, KOLVO SMALLINT, SUMMA COMPUTED BY (STOIMOST * KOLVO))
Значения по умолчанию Значения столбцов по умолчанию задаются необязательным параметром DEFAULT: [DEFAULT {<литерал> | NULL | USER}] Если вы задали этот параметр, то указанное значение будет подставляться в столбец автоматически, если пользователь не введет в него другого значения. Здесь: <литерал> - заданный по умолчанию символ или текст, целое или вещественное число, дата и (или) время, в зависимости от типа столбца. Применяется соответственно с текстовыми, числовыми полями и полями с датами. Значение даты указывается в кавычках. В примере ниже мы создаем логическое поле, которое должно содержать один из двух символов: Y (истина) или N (ложь). По умолчанию, в поле должен помещаться символ N. Кроме того, мы создаем числовое поле, которое по умолчанию «обнуляем», а также поле дат, которое по умолчанию будет содержать дату 1 Января 2010 года. Вот как можно этого добиться: CREATE TABLE MyDefault( Bool_col CHAR(1) DEFAULT ‘N’, Int_col INTEGER DEFAULT 0, Date_col DATE DEFAULT ’01.01.2010’ ) NULL – пустое значение в столбце. Когда вы добавляете в таблицу запись, все ее поля автоматически принимают значение Null, поэтому данный атрибут можно и не использовать. USER – очень интересный и в высшей степени полезный атрибут, который хранит имя текущего пользователя. В многопользовательских базах данных нередко возникают ситуации, когда кто-то из пользователей ввел неправильные значения, в результате чего вся база стала содержать ошибочные данные. Ошибку, конечно, можно найти и исправить, но как найти виновника? Здесь очень пригодится 138
атрибут USER. Вот как можно реализовать таблицу, которая автоматически сохраняет в каждой записи имя пользователя, который эту запись редактирует: CREATE TABLE UsersTable( ID INTEGER, ZAPIS VARCHAR(50), LOG_USERS VARCHAR(10) DEFAULT USER) В данном примере не имеет значения, какие столбцы используются в записи. Самое главное, что в последний столбец автоматически будет вводиться ответственное за редактирование записи лицо, так что в случае ошибки долго искать виновника не придется. Такие поля, как правило, предназначены только для программиста или администратора базы данных, так что в клиентских приложениях их скрывают.
Параметр NOT NULL Параметр NOT NULL указывает, что столбец не должен быть пустым. В случае если вы попытаетесь сохранить запись с таким столбцом, не введя в него какого-нибудь значения, произойдет ошибка, и InterBase не позволит сохранить такую запись. Если вы описываете поле, которое будет ключевым, указание такого параметра является обязательным. Пример: CREATE TABLE Key_col( ID INTEGER NOT NULL)
Ограничения столбцов Ограничение на значение столбцов используется для проверки достоверности вводимых данных и подразумевает, что пользователь не сможет ввести в столбец значение, не удовлетворяющее указанному условию. При попытке редактирования столбца с ограничением, InterBase будет автоматически отслеживать вводимое значение, и отвергать те из них, которые нарушают заданное ограничение. Ограничения накладываются оператором CHECK, которое имеет следующий формат:
CHECK (<условие_поиска>); где <условие_поиска> = <значение> <оператор> {<значение1> | <выбор_одного>} | <значение> [NOT] BEETWEN <значение1> AND <значение2> | <значение> [NOT] LIKE <маска> [ESCAPE <символ>] | <значение> [NOT] IN (<значение1> [, <значение2> ...]) | <список_выбора> | <значение> IS [NOT] NULL | <значение> {[NOT] {= | < | >} | >= | <=} {ALL | SOME | ANY} {список_выбора} | EXISTS (<выражение_выбора>) | SINGULAR (<выражение_выбора>) | <значение> [NOT] CONSTAINING <значение1> | <значение> [NOT] STARTING [WITH] <строка> | NOT <условие_поиска> | <условие_поиска> OR <условие_поиска> | <условие_поиска> AND <условие_поиска>
139
Напомним, что символом «|» в описании синтаксиса языков программирования принято разделять альтернативные значения. То есть, «|» смело можно заменить на «или». А в квадратные скобки заключаются необязательные параметры. Диапазон возможностей оператора CHECK весьма широк – теоретически в нем можно указать почти любое условие поиска. В ограничениях можно сравнивать вводимое значение с другим указанным значением, со значением другого столбца или даже другого столбца другой таблицы. Кроме того, в качестве сравниваемого значения можно использовать выборку оператором SELECT из другой таблицы. Практически все параметры отвечают стандартам языка SQL и могут встречаться в операторах SELECT. Приведем несколько примеров: CREATE TABLE Check_table( /*Столбец должен содержать только положительные числа или ноль:*/ Col_1 INT CHECK (Col_1 >= 0), /*Столбец должен содержать значение в диапазоне от 10 до 50:*/ Col_2 INT CHECK (Col_2 BETWEEN 10 AND 50), /*Столбец должен оканчиваться символами «руб.»*/ Col_3 VARCHAR(20) CHECK (Col_3 LIKE ‘% руб.’), /*Столбец должен содержать либо «муж», либо «жен».*/ Col_4 VARCHAR(3) CHECK(Col_4 IN ('муж','жен')), /*Столбец не может быть пустым.*/ Col_5 VARCHAR(5) CHECK (Col_5 IS NOT NULL), /*Столбец 6 не может иметь такое же значение, как столбец 2*/ Col_6 INT CHECK (NOT Col_6 = Col_2), /* Значение столбца 7 должно совпадать с одним или несколькими из */ /* значений столбца DLINNOE таблицы Table_cel*/ Col_7 INT CHECK (EXISTS(SELECT DLINNOE FROM Table_cel WHERE Table_cel.DLINNOE = Check_table.Col_7)), /* Значение столбца 8 должно совпадать лишь с одним из */ /* значений столбца DLINNOE таблицы Table_cel*/ Col_8 INT CHECK (SINGULAR(SELECT DLINNOE FROM Table_cel WHERE Table_cel.DLINNOE = Check_table.Col_8)), /*Столбец обязательно должен содержать подстроку «мир».*/ /*Например, «Мы за мир!», «мировая экономика», «эмират»*/ Col_9 VARCHAR(30) CHECK ( Col_9 CONTAINING ‘мир’), /*Столбец обязательно должен начинаться с подстроки «мир».*/ /*Например, «мировая экономика», «мираж», «мир в объективе»*/ Col_10 VARCHAR(30) CHECK ( Col_10 STARTING WITH ‘мир’) ) Комментарии достаточно подробны, чтобы вы смогли разобраться с параметрами ограничений. В приведенном примере присутствуют практически все ограничения, которые могут понадобиться в реальном программировании. Ограничения могут присутствовать не только в столбцах, но и в таблицах. Позднее мы подробно их разберем.
140
Ограничения CHECK в доменах Указанные выше ограничения на значения столбцов справедливы и для доменов, с небольшим изменением. Поскольку мы заранее не знаем, какой столбец (столбцы) какой таблицы (таблиц) будут использовать описание этого домена, вместо имени столбца указывается ключевое слово VALUE. VALUE является заменой для любого имени поля, константы, значения переменной или результата выражения, которые могут быть подставлены в синтаксисе SQL для сохранения данных в столбце. Пример: CREATE DOMAIN Poloj_Cel AS INT CHECK(VALUE >= 0)
Порядок сортировки COLLATE Эту тему мы рассматривали в прошлой лекции и знаем, что данный параметр применяется с текстовыми столбцами и определяет способ, по которому будут сортироваться и сравниваться текстовые данные при выводе их оператором SELECT. Для кодировки WIN1251 это может быть сортировка WIN1251 или PXW_CYRL.
Удаление таблиц Нередко возникает необходимость удалить из базы данных созданную ранее таблицу. Делается это оператором DROP. Пример: DROP TABLE ARRAY_TABLE Этот же оператор используется для удаления доменов, представлений, триггеров и т.д.
Модификация таблицы Иногда встречаются случаи, когда структуру таблицы нужно изменить. Проще всего удалить ее оператором DROP и создать новую таблицу с этим же именем, и с новой структурой. Но таблица может уже содержать какие-то данные, или изменения должны быть небольшими: добавить новый столбец (столбцы) или удалить парочку существующих столбцов. Изменить структуру таблицы можно оператором ALTER, который может иметь дополнительные параметры ADD (добавить столбец) или DROP (удалить столбец). Примеры: /* Добавляем столбец */ ALTER TABLE TABLE_CEL ADD New_String VARCHAR(30) или /* Удаляем столбец */ ALTER TABLE TABLE_CEL DROP Korotkoe Примечание: после выполнения операторов ALTER в утилите IBConsole, транзакция может считаться незавершенной, и попытка ввести новую команду или закрыть окно Interactive SQL вызовет ошибку. В этом случае нужно просто ввести и выполнить команду завершения транзакции COMMIT, после которой можно вводить новые запросы. О транзакциях мы будем говорить позднее. Иногда бывает необходимо не удалить столбец, а только изменить его. Например, вместо VARCHAR(30) указать VARCHAR(50). При этом нужно сохранить данные, которые хранились в старом столбце. Сделать это одним оператором невозможно, придется изменять столбец в несколько этапов. Вначале создается новый временный столбец, повторяющий все атрибуты изменяемого, и в него 141
копируются все данные из старого столбца. Копирование данных осуществляется оператором UPDATE … SET: /* Добавляем новый временный столбец: */ ALTER TABLE TABLE_CEL ADD Temp_String VARCHAR(30); /* Копируем в него данные из столбца New_String: */ UPDATE TABLE_CEL SET Temp_String = New_String Далее нужно удалить старый столбец и создать новый с этим же именем, но уже с новыми параметрами: /* Удаляем старый столбец: */ ALTER TABLE TABLE_CEL DROP New_String; /* Добавляем новый, с другими параметрами: */ ALTER TABLE TABLE_CEL ADD New_String VARCHAR(50) Далее, с помощью оператора UPDATE … SET нужно скопировать данные из временного столбца в только что созданный, после чего удалить временный: /* Копируем данные: */ UPDATE TABLE_CEL SET New_String = Temp_String; /* Удаляем временный столбец: */ ALTER TABLE TABLE_CEL DROP Temp_String
Представления Представление (VIEW) – это виртуальная таблица, созданная SQL-запросом для выборки данных из одной или нескольких таблиц БД, или даже из других представлений. Такая таблица не содержит данных, а лишь ссылается на другие таблицы или представления. Для пользователя представление ничем не отличается от обычной таблицы. Для работы с представлением можно использовать обычные наборы данных: TTable или TQuery. Само представление является SQL-запросом, хранящемся на сервере и выполняющимся всякий раз, когда происходит обращение к нему. Во время запроса к представлению, сервер InterBase оптимизирует и компилирует этот запрос, что значительно сокращает время его выполнения. Представление, в отличие от таблиц, не может иметь ключей или индексов. При упорядочивании записей используются ключи и индексы таблиц, которые лежат в основе представления. Представления обычно применяют для изоляции реально хранимых данных от пользователя, что увеличивает безопасность базы данных. Представления удобны, когда например, программист или администратор БД принимает решение разделить одну таблицу на две. При этом описание представления также изменяется, но для пользователя это по-прежнему одна таблица, так что изменять клиентское приложение не придется. Разработчик также получает возможность изменять представление, дополняя его новыми возможностями. Еще представления помогут, если данному пользователю нежелательно предоставлять доступ ко всем полям таблицы (таблиц). Ему можно сделать доступ к представлению, в котором использовать нужные столбцы как с возможностью их редактирования, так и «Только для чтения». Представление создается следующим образом: CREATE VIEW <Имя_представления> [(<Имя_столбца_представления> [, < Имя_столбца_представления > …])] AS <Запрос_SELECT> [WITH CHECK OPTION] Здесь <Имя_представления> является идентификатором представления, который не должен совпадать с идентификаторами других представлений, таблиц или хранимых процедур.
142
[(<Имя_столбца_представления> [, < Имя_столбца_представления > …])] - необязательный список имен столбцов создаваемого представления. Если этот список не указывать, имена столбцов будут такими же, как и имена столбцов таблицы (таблиц), указанных в запросе SELECT. Однако при использовании нескольких таблиц, могут возникнуть случаи дублирования имен столбцов, то есть две таблицы могут иметь столбцы с одинаковым именем. В этом случае указать список имен столбцов для представления необходимо, соответствующие столбцы будут переименованы в представлении. Имена столбцов в списке представления должны соответствовать количеству и порядку столбцов, указанных в операторе SELECT. <Запрос_SELECT> представляет собой обычный SQL-запрос выборки данных из одной или нескольких таблиц или просмотров. Однако в запросе нельзя указывать условия упорядоченности, такие как ORDER BY. Необязательный параметр [WITH CHECK OPTION] запрещает добавлять записи, значения столбцов которых не удовлетворяют условиям выборки запроса представления. Предположим, что в запросе SELECT представления имеется условие WHERE. Это условие делает выборку записей таблицы по полю целого типа, указывая, что значения поля должны быть в диапазоне от 0 до 100. Если в изменяемом представлении пользователь модифицирует запись, выйдя за рамки указанного диапазона, запись перестанет соответствовать условию WHERE. Параметр [WITH CHECK OPTION] гарантирует, что такие изменения будут невозможны. Пример создания представления: CREATE VIEW View_Firma AS SELECT FAMILIYA, IMYA FROM Table_Firma Данное представление создает виртуальную таблицу из двух столбцов FAMILIYA и IMYA, которые физически хранятся в таблице Table_Firma. В утилите IBConsole созданные представления можно увидеть в дереве серверов, в выбранной базе данных в разделе Views. Обратиться к этому представлению можно, как к обычной таблице, с помощью запроса SELECT, выполненного в окне запросов Interactive SQL: SELECT * FROM View_Firma В окне вывода результатов будут отображены столбцы представления. Представления могут иметь и более сложный формат, содержать в запросе несколько таблиц и даже других представлений. Ниже приведен пример создания двух таблиц и представления, соединяющего некоторые значения этих таблиц: CREATE TABLE TOVAR( ID INTEGER NOT NULL, NAZVANIE VARCHAR(20) NOT NULL COLLATE PXW_CYRL, STOIMOST DOUBLE PRECISION NOT NULL); COMMIT; CREATE TABLE SKLAD( ID INTEGER NOT NULL, ID_TOVAR INTEGER NOT NULL, KOLVO INTEGER NOT NULL); COMMIT; CREATE VIEW TOVARY20(NAZ, KOL, CENA) AS SELECT NAZVANIE, KOLVO, STOIMOST FROM TOVAR, SKLAD WHERE (SKLAD.ID_TOVAR = TOVAR.ID) AND (TOVAR.STOIMOST <= 20); 143
Данное представление создает три столбца: название товара, количество этого товара на складе и его стоимость. Причем выводятся только те товары, стоимость которых не превышает 20. Параметр [WITH CHECK OPTION] здесь не указывается, так как данное представление по определению является «только для чтения». После создания каждой таблицы указывается оператор COMMIT, который, как говорилось выше, подтверждает и завершает предыдущую транзакцию.
Изменяемые представления Изменяемые представления позволяют пользователям не только просматривать, но и редактировать данные. Специально указывать, что представление является изменяемым, не нужно. Представление автоматически создается изменяемым, если оно удовлетворяет следующим требованиям:
Представление состоит только из одной таблицы. Столбцы представления содержат все столбцы таблицы, определенные с параметром NOT NULL. В представлении не используются агрегатные функции, параметры DISTINCT и HAVING, хранимые процедуры и пользовательские функции.
Если представление удовлетворяет всем этим требованиям, к нему можно применять операторы INSERT, UPDATE и DELETE (то есть, редактировать). Если в представлении указаны не все столбцы таблицы, при добавлении новой записи неуказанные столбцы таблицы помечаются значением NULL. Если в представлении указаны не все NOT NULL – столбцы таблицы, то нельзя добавлять новые записи, можно только редактировать или удалять имеющиеся.
Модификация представления Представление, как и таблицу, можно удалить командой DROP <Имя_представления> Однако модифицировать представление командой ALTER нельзя. Если все же возникнет такая необходимость, то единственной возможностью является удаление старого представления, и создание нового, с таким же именем, но с новыми параметрами. Поскольку физически данные в представлении не хранятся, такая операция не приведет к их потере.
144
Лекция 18. Ключи и индексы. Помимо ограничений столбцов и доменов, о которых говорилось в прошлой лекции, существуют еще ограничения базы данных. Ограничения БД – это правила, которые определяют взаимосвязи между таблицами, могут проверять и изменять данные в таблицах по этим правилам. На ограничениях БД основана значительная часть бизнес-логики приложений. Базы данных InterBase могут использовать следующие виды ограничений:
PRIMARY KEY – первичный ключ таблицы. UNIQUE – уникальный ключ таблицы. FOREIGN KEY – внешний ключ, обеспечивает ссылку на другую таблицу и гарантирует ссылочную целостность между родительской и дочерней таблицами.
Примечание о терминологии Если вы похожи на автора данного курса в том, что любите искать ответы на интересующий вас вопрос комплексно, в разных трудах разных авторов, то вы не могли не заметить некоторую путаницу в определениях главная (master) -> подчиненная (detail) таблицы. Напомним, что главную таблицу часто называют родительской, а подчиненную – дочерней. Связано это, вероятно, с тем, как интерпретируются эти определения в локальных и SQLсерверных СУБД. В локальных СУБД главной называется та таблица, которая содержит основные данные, а подчиненной – дополнительные. Возьмем, к примеру, три связанные таблицы. Первая содержит данные о продажах, вторая – о товарах и третья – о покупателях:
Рис. 18.1. Связи главная-подчиненная Здесь основные сведения хранятся в таблице продаж, следовательно, она главная (родительская). Дополнительные сведения хранятся в таблицах товаров и покупателей, значит они дочерние. Это и понятно: одна дочь не может иметь двух биологических матерей, зато одна мать вполне способна родить двух дочерей. Но в SQL-серверах баз данных имеется другое определение связей: когда одно поле в таблице ссылается на поле другой таблицы, оно называется внешним ключом. А поле, на которое оно ссылается, называется родительским или первичным ключом. Таблицу, которая имеет внешний ключ (ссылку на запись другой таблицы) нередко называют дочерней, а таблицу с родительским ключом – родительской. Еще в определении связей говорят, что родитель может иметь только одну уникальную запись, на которую могут ссылаться несколько записей дочерней таблицы. Так что в приведенном выше примере таблица продаж имеет два внешних ключа: идентификатор товара, и идентификатор покупателя. А обе таблицы в правой части рисунка имеют родительский ключ 145
«Идентификатор». Поскольку один покупатель или товар могут неоднократно встречаться в таблице продаж, то получается, что обе таблицы в правой части рисунка – родители, а таблица слева – дочерняя. Поскольку сейчас мы изучаем InterBase – SQL сервер БД, этими определениями мы и будем руководствоваться в последующих лекциях. Чтобы далее не ломать голову над этой путаницей, сразу договоримся: дочерняя таблица имеет внешний ключ (FOREIGN KEY) на другую таблицу.
PRIMARY KEY PRIMARY KEY – первичный ключ, является одним из основных видов ограничений в базе данных. Первичный ключ предназначен для однозначной идентификации записи в таблице, и должен быть уникальным. Первичные ключи PRIMARY KEY находятся в таблицах, которые принято называть родительскими (Parent). Не стоит путать первичный ключ с первичными индексами локальных баз данных, первичный ключ является не индексом, а именно ограничением. При создании первичного ключа InterBase автоматически создает для него уникальный индекс. Однако если мы создадим уникальный индекс, это не приведет к созданию ограничения первичного ключа. Таблица может иметь только один первичный ключ PRIMARY KEY. Предположим, имеется таблица со списком сотрудников. Поле «Фамилия» может содержать одинаковые значения (однофамильцы), поэтому его нельзя использовать в качестве первичного ключа. Редко, но встречаются однофамильцы, которые вдобавок имеют и одинаковые имена. Еще реже, но встречаются полные тезки, поэтому даже все три поля «Фамилия» + «Имя» + «Отчество» не могут гарантировать уникальности записи, и не могут быть первичным ключом. В данном случае выход, как и прежде, в том, чтобы добавить поле – идентификатор, которое содержит порядковый номер данного лица. Такие поля обычно делают автоинкрементными (об организации автоинкрементных полей поговорим на следующих лекциях). Итак, первичный ключ – это одно или несколько полей в таблице, сочетание которых уникально для каждой записи. Если в первичный ключ входит единственный столбец (как чаще всего и бывает), спецификатор PRIMARY KEY ставится при определении столбца: CREATE TABLE Prim_1( Stolbec1 INT NOT NULL PRIMARY KEY, Stolbec2 VARCHAR(50)) Если первичный ключ строится по нескольким столбцам, то спецификатор ставится после определения всех полей: CREATE TABLE Prim_2( Stolbec1 INT NOT NULL, Stolbec2 VARCHAR(50) NOT NULL, PRIMARY KEY (Stolbec1, Stolbec2)) Как видно из примеров, первичный ключ обязательно должен иметь ограничение столбца (столбцов) NOT NULL.
UNIQUE UNIQUE – уникальный ключ. Спецификатор UNIQUE указывает, что все значения данного поля должны быть уникальными, в связи с этим такие поля также не могут содержать значения NULL. Можно сказать, что уникальный ключ UNIQUE является альтернативным вариантом первичного ключа, однако имеются различия. Главное различие в том, что первичный ключ должен быть только один, тогда как уникальных ключей может быть несколько. Кроме того, ограничение UNIQUE не может быть построено по тому же набору столбцов, который был использован для ограничения PRIMARY KEY или 146
другого UNIQUE. Уникальные ключи, как и первичные, находятся в таблицах, которые являются родительскими по отношению к другим таблицам. Столбец, объявленный с ограничением UNIQUE, как и первичный ключ, может применяться для обеспечения ссылочной целостности между родительской и дочерней таблицами. При этом внешний ключ дочерней таблицы будет ссылаться на это поле (поля). Как и в случае первичного ключа, при создании уникального ключа, для него автоматически будет создан уникальный индекс. Но не наоборот. Пример создания таблицы с одним первичным и двумя уникальными ключами: CREATE TABLE Prim_3( Stolbec1 INT NOT NULL PRIMARY KEY, Stolbec2 VARCHAR(50) NOT NULL UNIQUE, Stolbec3 FLOAT NOT NULL UNIQUE)
FOREIGN KEY FOREIGN KEY – внешний ключ. Это очень мощное средство для обеспечения ссылочной целостности между таблицами, которое позволяет не только следить за наличиями правильных ссылок, но и автоматически управлять ими. Внешние ключи содержатся в таблицах, которые являются дочерними (Child) по отношению к другим таблицам. Ссылочная целостность обеспечивается именно внешним ключом, который ссылается на первичный или уникальный ключ родительской таблицы. Вернемся к рисунку 18.1. Если мы удалим сведения о каком-то покупателе в таблице покупателей, таблица продаж станет недостоверной – она будет содержать ссылки, которые на самом деле никуда не ссылаются. Чтобы обеспечить достоверность данных, нужно воспрепятствовать удалению записи с покупателем, если на эту запись есть ссылки в таблице продаж. Либо же при удалении записи с покупателем нужно автоматически удалить и все записи таблицы продаж, ссылающиеся на этого покупателя. Если же меняется значение идентификатора в таблице покупателей, значит нужно также изменить это значение во всех записях таблицы продаж, которые ссылаются на данного покупателя. Для обеспечения достоверности данных и применяют внешний ключ. Внешний ключ – это столбец или набор столбцов в дочерней таблице, который в точности соответствует столбцу или набору столбцов, определенных в родительской таблице как первичный (или уникальный) ключ, и ссылается на них. В отличие от первичного ключа, ключ FOREIGN KEY может содержать пустое значение, для него не обязателен атрибут NOT NULL. Строки с пустым внешним ключом не ссылаются ни на какую запись родительской таблицы, и называются «зависшими». Чтобы продемонстрировать работу с внешним ключом, создадим две таблицы – родительскую и дочернюю: CREATE TABLE Roditel( R_ID VARCHAR(20) NOT NULL PRIMARY KEY, R_Other INT); COMMIT; CREATE TABLE Doch( D_ID VARCHAR(20), D_Other INT, FOREIGN KEY (D_ID) REFERENCES Roditel ON UPDATE CASCADE ON DELETE NO ACTION); COMMIT;
147
Рис. 18.2. Результат совместной работы родительской и дочерней таблиц. Что мы получили в итоге? Родительская таблица имеет первичный ключ – поле текстового типа, и ни на кого не ссылается. Дочерняя таблица имеет такое же текстовое поле, которое может иметь значение NULL и является внешним ключом, ссылающимся на первичный ключ родительской таблицы. При совместной работе этих таблиц справедливы следующие замечания:
Вначале вводится значение первичного ключа (R_ID) в родительскую таблицу. Затем вводится такое же значение во внешний ключ (D_ID) дочерней таблицы. Попытка ввести значение, которого нет в поле R_ID родительской таблицы, потерпит неудачу. Зато можно не вводить это значение, оставив в поле NULL, или ввести несколько записей с одинаковым значением. Изменение текста в поле R_ID родительской таблицы приведет к автоматическому изменению такого же текста во всех записях дочерней таблицы, где этот текст встречается (об этом чуть ниже):
Попытка удалить дочернюю таблицу командой DROP приведет к ошибке: она ссылается на родительскую таблицу. Попытка удалить родительскую таблицу приведет к ошибке: для сохранения целостности данных InterBase не даст удалить первичный ключ. Для удаления этих таблиц вначале придется удалить ограничения, наложенные на них (об этом чуть ниже). 148
Механизмы управления ссылками внешних ключей Внешний ключ имеет такой синтаксис: FOREIGN KEY (список_столбцов_дочерней_таблицы) REFERENCES <имя_родительской_таблицы> [<список_столбцов_родительской_таблицы>] [ON DELETE {NO ACTION | CASCADE | SET DEFAULT | SET NULL}] [ON UPDATE {NO ACTION | CASCADE | SET DEFAULT | SET NULL}] Разберем этот синтаксис. список_столбцов_дочерней_таблицы – это один или несколько столбцов, которые являются внешним ключом. <имя_родительской_таблицы> - имя родительской таблицы, на которую ссылается внешний ключ дочерней таблицы. [<список_столбцов_родительской_таблицы>] – один или несколько столбцов, являющихся ключевыми для связи таблиц. Это необязательный параметр, его можно не указывать, если связь строится по первичному ключу родительской таблицы, но обязательный, если связываемся с ключом UNIQUE. Необязательные параметры ON DELETE и ON UPDATE указывают, что должен делать InterBase соответственно, при удалении или изменении записи первичного ключа. Фактически, для реализации этих механизмов создается системный триггер, который выполняет эти действия. Действия могут быть следующими: NO ACTION – При удалении или изменении первичного ключа родительской таблицы, ничего не делать с записями дочерней таблицы, которые ссылаются на этот ключ. Это действие является действием по умолчанию. CASCADE – При удалении или изменении первичного ключа родительской таблицы, автоматически удалить или изменить все записи дочерней таблицы, которые ссылаются на этот ключ. SET DEFAULT – При удалении или изменении первичного ключа родительской таблицы, установить все записи дочерней таблицы, которые ссылаются на этот ключ, в значение по умолчанию. SET NULL – При удалении или изменении первичного ключа родительской таблицы, установить все записи дочерней таблицы, которые ссылаются на этот ключ, в значение NULL. В приведенном выше примере с родительской и дочерней таблицами мы указали, что при изменении значения первичного ключа родительской таблицы, следует изменить это же значение во всех записях внешнего ключа дочерней таблицы (см. рис.18.3). А при удалении значения первичного ключа ничего делать с дочерней таблицей не нужно. Вообще то, с параметром ON DELETE CASCADE следует быть очень осторожным: случайная ошибка пользователя может привести к потере большого количества связанных данных, имейте это в виду. Также следует быть внимательными при использовании атрибута NO ACTION. При удалении или изменении записи в родительской таблице, связанные с ней записи в дочерней таблице не изменятся. А это означает, что база данных станет недостоверной.
Именование ссылочной целостности Ссылочную целостность, объявленную внешним ключом, можно именовать. Делается это для более удобного управления этим ограничением: если ссылочная целостность имеет имя, ее можно удалить командой DROP, сославшись на ее имя. Для именования используется оператор 149
CONSTRAINT <Имя_ссылочной_целостности> Создадим еще две таблицы, одна из которых ссылается на другую: CREATE TABLE Roditel2( R_ID VARCHAR(20) NOT NULL PRIMARY KEY, R_Celoe INT); COMMIT; CREATE TABLE Doch2( D_ID VARCHAR(20), D_Celoe INT, CONSTRAINT Cons_Doch2 FOREIGN KEY (D_ID) REFERENCES Roditel2 ON UPDATE CASCADE ON DELETE NO ACTION); COMMIT; Чтобы удалить эти таблицы, нужно вначале удалить ссылочную целостность: ALTER TABLE Doch2 DROP CONSTRAINT Cons_Doch2 Внимание! При удалении ограничения вы можете получить ошибку «object is in use» (объект находится в использовании). Это говорит о том, что на какую-то из таблиц имеется незавершенная транзакция. Просто завершите работу IBConsole, и снова загрузите ее, тогда все получится. После удаления ссылочной целостности можно удалить и таблицы: DROP TABLE Doch2; DROP TABLE Roditel2; Еще одно важное замечание: в InterBase нет ссылочных целостностей без идентификатора! Если вы не дали имени ссылочной целостности, InterBase делает это автоматически. Выделите в IBConsole пункт Tables, чтобы в правой части окна появился список таблиц базы данных. Затем щелкните правой кнопкой по таблице DOCH из первого примера (именно в ней мы создавали внешний ключ без имени), и выберите команду Properties. Откроется знакомое вам окно, в котором нужно щелкнуть по кнопке Show Check Constraints:
150
Рис. 18.4. Кнопка Show Check Constraints показывает ограничения таблицы В окне вы увидите имя ограничения, которое автоматически было дано InterBase, у меня это INTEG_31, у вас оно может быть другим. Теперь, зная имя ограничения, самостоятельно удалите его, после чего удалите таблицы DOCH и RODITEL.
Индексы С индексами вы уже знакомы по локальным базам данных, в InterBase они используются для тех же целей: для ускорения поиска и сортировки нужных записей. Индекс – это упорядоченный указатель на записи в таблице. Индексы в InterBase хранятся отдельно от таблицы, и фактически представляют собой упорядоченные пары «значение поля» -> «физическое расположение этого значения в таблице». В одной таблице может быть до 64 индексов, причем сортировку в них можно указывать как в возрастающем, так и в убывающем порядке. Синтаксис создания индекса следующий: CREATE [UNIQUE] {[ASC[ENDING] | DESC[ENDING]]} INDEX ON (
[,
… ]); Как вы уже знаете, в квадратные скобки заключены необязательные параметры команды. То есть, минимальным выражением создания индекса может быть: CREATE INDEX Sklad_Index ON SKLAD(ID_TOVAR) Выделив в IBConsole раздел Indexes, в правой части окна вы увидите список индексов БД. Как вы заметили, помимо только что созданного индекса имеются и другие, которые построены по столбцам, указанным в первичных и уникальных ключах. Дело в том, что индексы используют такой же механизм упорядочивания записей, как и ключи, так что разница между ними в основном, логического характера. Необязательный параметр UNIQUE указывает, что запись должна быть уникальной (сравните с уникальным ключом). Необязательный параметр ASC или ASCENDING указывает, что индекс должен сортироваться в возрастающем порядке, а DESC (DESCENDING) – в убывающем. 151
Как и ключ, индекс может быть построен не по одному столбцу, а по нескольким, однако этим увлекаться не стоит – использование составного индекса иногда даже замедляет работу с БД. Еще одно замечание: в отличие от локальных БД, в InterBase нельзя указать индекс, используемый при сортировке. Когда вы делаете запрос, InterBase автоматически применяет наиболее подходящий индекс и использует его для поиска записи. Удаляется индекс обычным способом: DROP INDEX Интенсивная работа с базой данных может привести к тому, что индексы становятся разбалансированными, значения в них располагаются, как попало, и использование индекса не ускоряет, а даже замедляет поиск данных. В этом случае поможет перестройка индексов: ALTER INDEX INACTIVE; ALTER INDEX ACTIVE; Первая команда отключает индекс, вторая подключает его вновь. Имеется ряд ограничений на эти действия:
Нельзя отключать индекс, если он используется в данный момент в каком либо запросе. Нельзя перестроить индекс, если он использован в первичном, уникальном или внешнем ключе. Для перестройки индекса необходимо иметь права администратора БД (SYSDBA) или быть создателем данного индекса.
Обычно администратор дожидается, пока все уйдут на обед, подключается к базе данных в монопольном режиме и перестраивает индексы. Однако это только полумера. В идеале, для оптимизации работы БД, время от времени индексы нужно удалять, а затем снова их создавать. Само собой, для этих действий также нужен монопольный режим и права администратора БД.
152
Лекция 19. Хранимые процедуры. Хранимые процедуры и триггеры, о которых уже неоднократно упоминалось в предыдущих лекциях, являются одними из самых мощных средств InterBase для реализации бизнес-логики на стороне сервера. Использование этих инструментов приводит к:
ускорению выполнения запросов и снижению нагрузки на сеть; повышению безопасности БД, снижению риска ошибок; централизации обработки данных (дополнить или исправить правила нужно только на сервере); уменьшению и упрощению кода клиентских приложений, работающих с сервером InterBase.
Хранимые процедуры, как и триггеры, типичны только для клиент-серверных баз данных. И те, и другие используют специальный алгоритмический язык. О триггерах мы поговорим на следующей лекции, а сейчас изучим хранимые процедуры и алгоритмический язык, общий и для триггеров, и для хранимых процедур.
Хранимые процедуры (Stored Procedures) Каждая хранимая процедура является самостоятельной программой, скомпилированной во внутренний двоичный язык InterBase, и является частью метаданных (данные о данных) базы данных. Другими словами, хранимые процедуры являются частью базы данных и хранятся вместе с таблицами, индексами и другими объектами БД. Хранимую процедуру можно вызвать из клиентского приложения, из другой хранимой процедуры или триггера. Хранимые процедуры могут быть двух типов:
выполняемые процедуры, которые либо вообще не возвращают результатов, а только выполняют какие-то действия, либо возвращают только один набор выходных параметров. Такие процедуры вызываются командой EXECUTE PROCEDURE. процедуры выборки, которые предназначены для создания многострочных выходных данных, такие процедуры вызываются командой SELECT и используются, как виртуальные таблицы.
Алгоритмический язык хранимых процедур и триггеров содержит в своей основе обычный SQL, дополненный переменными, входными и выходными параметрами, условными операторами, операторами циклов и некоторыми другими средствами. Синтаксис создания хранимой процедуры следующий: SET TERM <новый_терминатор><старый_терминатор> CREATE PROCEDURE Имя_Процедуры [(<входной_параметр> <тип_данных> [,<входной_параметр> <тип_данных> […]])] [RETURNS (<выходной_параметр> <тип_данных> [,<выходной_параметр> <тип_данных> […]])] AS <тело_процедуры> <тело_процедуры> = [DECLARE [VARIABLE] <переменная><тип_данных>; […]] BEGIN <составной оператор> END<терминатор> SET TERM <старый_терминатор><новый_терминатор> 153
Если синтаксис представляется вам слишком сложным, не пугайтесь заранее, на самом деле все не так страшно, как кажется с первого взгляда. Разберем его подробней по частям.
Терминаторы Терминаторами называются символы окончания SQL оператора. Установка терминаторов не относится напрямую к синтаксису хранимых процедур или триггеров, однако попытка создания процедуры без переопределения терминатора, скорее всего, приведет к ошибке. Дело в том, что внутри создаваемой процедуры неоднократно может встречаться символ «;», который по умолчанию является символом конца оператора в языке SQL. В этом случае утилита IBConsole решит, что оператор закончен, и попытается его выполнить. Но процедура еще не будет прочитана до конца, что и приведет к ошибке. Выход: переопределить терминатор. Делается это довольно просто: SET TERM <новый_терминатор> <старый_терминатор>. В качестве нового символа окончания вы можете использовать любой редкий символ, например «^» или «&». Затем в теле процедуры может сколько угодно раз встречаться символ «;», SQL при этом не воспримет его как окончание оператора. Завершающую END процедуры следует закрыть установленным вами терминатором, в этом случае процедура будет прочитана IBConsole до конца и выполнена без ошибок. А напоследок вы вновь переопределяете терминатор, устанавливая стандартный символ «;». Например: SET TERM ^; CREATE PROCEDURE ……^ SET TERM ;^ Здесь, и далее в лекции показаны примеры различных фрагментов процедуры, а не всей процедуры в целом. Поэтому выполнять данные примеры в Interactive SQL не нужно, это все равно приведет к ошибке. В конце лекции будут представлены три примера рабочих процедур. Совет: выберите для себя какой-то один символ для переопределения терминатора, и всегда используйте только его, чтобы не путаться в коде процедур или триггеров.
Заголовок Заголовок процедуры состоит из следующих разделов: Имя процедуры – обязательный элемент. Имя должно быть уникальным во всей базе данных. Пример: CREATE PROCEDURE Proc1
Входные параметры – необязательный элемент. Входные параметры, как и в процедурах Delphi, служат для передачи в процедуру каких-то значений из внешнего приложения, другой процедуры или триггера. При этом типы данных этих параметров могут быть любыми, определенными в SQL, кроме массивов. Параметры объявляются в виде списка «параметр тип», несколько параметров разделяются запятой. Имена входных параметров процедуры не обязаны соответствовать именам параметров вызывающего приложения, но типы данных должны совпадать. Пример:
Выходные параметры – необязательный элемент. Выходные параметры служат для возврата в вызывающее приложение списка результирующих значений. Объявление выходных параметров (если они есть), начинается ключевым словом RETURNS, после которого в скобках параметры перечисляются в виде списка «параметр тип». Пример:
Ключевое слово AS – обязательный элемент, указывающий на окончание заголовка процедуры. Пример:
CREATE PROCEDURE Proc4 RETURNS (param char(50)) AS
Тело процедуры Хранимые процедуры, как и процедуры в Delphi, могут иметь локальные переменные, или не иметь их. Если локальных переменных нет, тело процедуры представляет собой только составной оператор, заключенный в скобки BEGIN … END. Причем эти скобки обязательны, даже если в процедуре всего только один оператор. Если же локальные переменные имеются, то вначале их нужно объявить после ключевых слов DECLARE VARIABLE, после чего следует составной оператор. При этом следует помнить, что объявление каждой переменной является отдельным оператором и должно завершаться точкой с запятой. Пример: SET TERM ^; CREATE PROCEDURE MyProc (param1 Integer) RETURNS (param2 Varchar(20), param3 Double Precision) AS DECLARE VARIABLE perem1 Varchar(10); DECLARE VARIABLE perem2 Date; DECLARE VARIABLE perem3 Integer; BEGIN … END^ SET TERM ;^ В приведенном примере мы вначале переопределяем терминатор, после чего приступаем непосредственно к описанию процедуры. В процедуре имеется один входящий, и два выходящих параметра, а также объявлены три локальные переменные. Заметим, что ключевое слово DECLARE обязательно, а вот слово VARIABLE можно опустить. Если вы желаете, чтобы ваша база данных была совместима с ранними версиями InterBase, то VARIABLE лучше указывать. Завершается процедура новым терминатором «^», после чего мы переопределяем его на стандартный символ «;».
Блок кода процедуры Блок кода процедуры начинается ключевым словом BEGIN, и оканчивается ключевым словом END. Блок кода может состоять из одного или нескольких операторов, а также содержать вложенные блоки кода BEGIN … END. В блоке кода процедуры могут встречаться: 155
операторы присваивания, которые присваивают значения локальным переменным, входным или выходным параметрам (в отличие от оператора «:=» в Delphi, в SQL это просто знак равно «=»); операторы SELECT для выборки данных из таблиц. Результаты выборки могут присваиваться переменным или параметрам; циклы, такие как FOR и WHILE; управляющие структуры IF; операторы EXECUTE PROCEDURE для вызова другой хранимой процедуры; комментарии, заключенные в скобки /* … */ ; символы сравнения >=, >, <=, <, <>, =, !< (не меньше), !> (не больше), != (не равно); команды модификации таблиц, такие как INSERT, UPDATE или DELETE; и др.
Важно! Если в блоке кода локальные переменные используются внутри SQL-оператора (например, SELECT), перед их именами следует ставить двоеточие. В других операторах этого делать не нужно.
Оператор присваивания Оператор присваивания имеет вид <переменная/выходной параметр> = <выражение> и служит для присвоения локальной переменной или выходному параметру какого-либо значения. Здесь есть несколько правил. Во-первых, переменная или выходной параметр должны иметь совместимый тип данных с выражением. Во-вторых, перед именем переменной или выходного параметра двоеточие не ставится. В-третьих, в InterBase выражение может быть либо строковым, либо арифметическим. В первом случае выражение может содержать оператор конкатенации (объединения) строк «||», во втором случае – четыре арифметических оператора +, -, * и /. Помимо этого, выражение может содержать значения однотипных столбцов таблиц, или результат работы другой процедуры.
Условный оператор IF… THEN … ELSE В отличие от Delphi, в InterBase условное выражение оператора IF обязательно нужно помещать в круглые скобки, кроме того, перед ELSE точка с запятой не опускается: IF (<условное_выражение>) THEN <оператор_1>; [ELSE <оператор_2>] Как обычно, если <условное_выражение> возвращает истину, то выполняется <оператор_1>, в противном случае выполняется <оператор_2>. Пример: IF (KOLVO>5 AND KOLVO<10) THEN …; Заметьте, что приоритет операций сравнения выше, чем логических операций AND, OR и NOT, поэтому при использовании более чем одного условия нет необходимости заключать каждое из них в отдельные скобки. Альтернативный вариант ELSE не является обязательным и может быть опущен.
Оператор SELECT Хранимая процедура может содержать оператор SELECT для вывода одного или нескольких значений и присвоения этих значений локальным переменным или выходным параметрам. Пример:
156
SELECT * FROM TABLE_FIRMA INTO :fam, :imya, :otch Таблица TABLE_FIRMA содержит три текстовых поля, содержащие фамилию, имя и отчество сотрудника. В примере берется первая запись таблицы, и значения ее полей присваиваются локальным переменным (или выходным параметрам) fam, imya и otch. Однако более типичным является применение этого оператора с условием выборки, возвращающим лишь одно значение: SELECT MAX(KOLVO) FROM SKLAD INTO :p_kolvo
Цикл FOR SELECT и SUSPEND Часто бывает недостаточно получения данных лишь одной записи. Чтобы получить множество значений (виртуальную таблицу), используется оператор FOR, имеющий следующий синтаксис: FOR SELECT <условие_выборки> INTO <список_переменных/параметров> DO <оператор> Здесь <условие_выборки> - любое условие оператора SELECT. <список_переменных/параметров> - Список локальных переменных или выходных параметров, чей тип данных соответствует типу данных, полученных командой SELECT. <оператор> - выполняемый оператор цикла. Обычно этим оператором бывает оператор SUSPEND, который помещает полученную запись в буфер (кэш), и требует получения следующей записи, и так до тех пор, пока не закончится цикл. Такая конструкция позволяет получать не одну запись, а набор записей, который возвращается в виде виртуальной таблицы. Такие процедуры называются процедурами выборки, и вызываются как обычные таблицы. Оператор SUSPEND применяется только в хранимых процедурах выборки, в триггерах он недопустим. В выполняемых процедурах пользоваться этим оператором синтаксически не запрещено, однако делать этого не стоит – все последующие после SUSPEND операторы не будут выполнены. Вместо этого в выполняемых процедурах обычно применяют явную команду досрочного выхода EXIT. Пример: FOR SELECT TOVAR, KOLVO FROM TABLE SDELKI INTO :param_st, :param_int DO SUSPEND; В данном примере выходным параметрам param_st и param_int присваиваются значения полей Tovar и Kolvo первой записи, после чего вызывается оператор SUSPEND и процедура приостанавливается. Данные передаются в вызывающую программу, после чего процедура таким же образом обрабатывает вторую запись. И так до конца таблицы. Для вызывающей программы все выглядит так, будто вызывалась таблица, а не хранимая процедура. Однако зачастую процедуры выборки выполняются намного быстрее, чем такой же запрос из клиентского приложения, ведь процедура – это скомпилированная подпрограмма, которая выполняется на стороне сервера. Следует отметить, что применение этого цикла не ограничивается только оператором SUSPEND. Вы можете установить там любой оператор, или несколько операторов, поместив их в скобки BEGIN … END. Например, в теле цикла вы можете проверять значения полей на какое-то условие, и если условие не верно, исправить запись.
Цикл WHILE … DO Этот цикл аналогичен тому, что вы используете в Delphi: WHILE (<условие_цикла>) DO <оператор> 157
Как видно из синтаксиса, условие цикла должно быть заключено в круглые скобки. Оператор может быть составным, помещенным между BEGIN и END. Кроме того, в теле оператора может встречаться команда EXIT, служащая для принудительного завершения работы процедуры. В триггере оператор EXIT не применяется.
Операторы INSERT, UPDATE, DELETE В хранимых процедурах и триггерах могут встречаться стандартные SQL-операторы модификации данных INSERT, UPDATE, и DELETE, которые соответственно, позволяют вставить новую запись, исправить или удалить существующую запись. В отличие от SQL, в качестве параметров в этих операторах вместо названия полей могут использоваться локальные переменные. Чтобы отличить названия полей от имен переменных, последние должны предваряться двоеточием. Подробнее операторы модификации данных мы будем изучать в лекции № 21. Пример процедуры с оператором INSERT смотрите в конце лекции.
Оператор EXECUTE PROCEDURE Из хранимой процедуры или триггера можно вызвать другую хранимую процедуру. Триггер вызвать нельзя. Синтаксис вызова хранимой процедуры: EXECUTE PROCEDURE <имя_процедуры> [<список_параметров>] [RETURNING_VALUES :<список_переменных>] Здесь <имя_процедуры> – имя вызываемой процедуры <список_параметров> – один или несколько передаваемых в процедуру параметров (необязательно, если процедура не требует параметров). Если параметров несколько, они разделяются запятыми. <список_переменных> – одна или несколько локальных переменных или выходных параметров, в которые помещаются результаты работы вызываемой процедуры (необязательно, если процедура не возвращает значения). Перед именем каждой переменной (выходного параметра) обязательно ставится двоеточие, переменные (выходные параметры) разделяются запятыми.
Исключения Исключения в InterBase во многом похожи на исключения в языках высокого уровня. Исключение – это сообщение об ошибке, которое имеет собственное уникальное имя и текст сообщения. Чтобы вызвать из хранимой процедуры или триггера исключение, вначале это исключение нужно создать. Делается это командой CREATE EXCEPTION <имя><’сообщение’>; Возьмем, к примеру, самое распространенное в языках программирования исключение – деление на ноль. Создадим исключение: CREATE EXCEPTION no_del_null ‘Cannot divide by zero!’ Тут следует заметить, что текст исключения вводится и хранится в кодировке символов NONE (то есть, какая то конкретная кодировка не используется). Поэтому если вы при создании базы данных указали кодировку по умолчанию WIN1251, то попытка создать исключение с русским текстом, скорее всего, вызовет ошибку. Выход: либо пишите текст исключения латиницей, либо при создании базы данных указывайте кодировку по умолчанию NONE. В последнем случае вы вполне сможете создать исключение с русским текстом: 158
CREATE EXCEPTION no_del_null ‘На ноль делить нельзя!’ Далее в блоке кода процедуры или триггера вы можете вызвать это исключение следующим образом: IF (delitel = 0) THEN BEGIN EXCEPTION no_del_null; Resultat = 0 END В данном примере Resultat – выходной параметр процедуры, вызвавшей исключение, а delitel – входной параметр, который мы проверяем на значение 0. Раз созданное исключение можно применять в любой хранимой процедуре или триггере. Исключения могут быть изменены командой ALTER EXCEPTION <имя_исключения> <”новый_текст”> При этом неважно, сколько процедур или триггеров используют его: само исключение никуда не делось, изменился лишь выводимый текст Исключения могут быть удалены командой DROP EXCEPTION <имя_исключения> Исключение не может быть удалено, если в настоящий момент оно используется какой-либо хранимой процедурой или триггером. Если вы удалили исключение, то ссылка на него в хранимых процедурах или функциях становится неразрешенной, вы получите исключение об отсутствие исключения, поэтому все же не забывайте удалять и ссылки на это исключение в ваших процедурах.
События и оператор POST_EVENT В хранимых процедурах и триггерах сервер InterBase позволяет посылать заинтересованным клиентам извещение о наступлении какого-либо события. Делается это командой POST_EVENT: POST_EVENT “Имя_события” Пример: POST_EVENT “Ups_Sorry” Имя события может быть строкой или текстовой переменной, содержащей имя события. Клиентская программа должна зарегистрировать на сервере те события, которые ее интересуют, чтобы получать их. Сделать это в клиентском приложении проще всего с помощью компонента TIBEventAlert, который находится на вкладке Samples Палитры компонентов, либо с помощью компонента TIBEvents, если вы для работы с БД пользуетесь компонентами с вкладки InterBase. Суть работы с этими компонентами проста: Вначале в свойстве Database вы выбираете компонент базы данных, например TDatabase или TIBDatabase, в зависимости от того, каким механизмом доступа к БД вы пользуетесь. Компонент базы данных должен быть подключен к серверу (подробнее об этом в следующих лекциях). Далее вы дважды щелкаете по свойству Events, которое имеет тип TStrings, и в открывшемся списке вписываете интересующие вас события. Затем вы переводите свойство Registered в True. 159
Потом требуется перейти на вкладку Events инспектора объектов и сгенерировать событие OnEventAlert, в котором можете написать какое-либо сообщение или действие. Параметр EventName будет содержать имя случившегося события. Например: If EventAlert = ‘Ups_Sorry’ then ShowMessage(‘Извините, но кто то удалил вашу запись!’); Параметр EventCount содержит количество событий, произошедших на сервере, а изменяемый параметр CancelAlerts позволяет отказаться от выдачи дальнейших сообщений, для этого нужно присвоить ему значение True.
Изменения и удаления хранимых процедур Изменение существующей процедуры делается командой ALTER PROCEDURE. Синтаксис этой команды ничем не отличается от синтаксиса команды CREATE PROCEDURE. Это «мягкий» способ изменения процедуры, который обычно применяют для добавления новых входных или выходных параметров. Более надежным способом считается удаление старой процедуры и создание новой, с таким же именем. Удаление процедуры производится командой DROP PROCEDURE <имя_процедуры> <имя_процедуры> - это просто имя существующей процедуры без всяких параметров. Пример: DROP PROCEDURE MyProc; Разумеется, изменять или удалять процедуру может только администратор SYSDBA или пользователь, создавший эту процедуру. Причем при изменении или удалении, процедура не должна находиться в использовании.
Примеры создания и вызова хранимых процедур Далее следуют примеры процедур, которые необходимо выполнять в Interactive SQL. Убедитесь, что сервис InterBase включен, загрузите IBConsole, войдите в базу данных First и вызовите окно Interactive SQL. Выполним следующий пример: /* Переопредилим терминатор: */ SET TERM ^; /* Создаем процедуру, которая будет добавлять новые записи в таблицу Table_Firma: */ CREATE PROCEDURE Firma_Insert(F VARCHAR(20), I VARCHAR(20), O VARCHAR(20)) AS BEGIN INSERT INTO TABLE_FIRMA(FAMILIYA, IMYA, OTCHESTVO) VALUES(:F, :I, :O); END^ SET TERM ;^ COMMIT; /* Сразу же применим полученную процедуру для ввода значений: */ EXECUTE PROCEDURE Firma_Insert('Иванов', 'Иван', 'Иванович'); EXECUTE PROCEDURE Firma_Insert('Петров', 'Петр', 'Петрович'); EXECUTE PROCEDURE Firma_Insert('Николаев', 'Николай', 'Николаевич'); COMMIT; 160
Данный пример создает выполняемую процедуру Firma_Insert, которая добавляет в таблицу Table_Firma указанные в параметре фамилию, имя и отчество. После создания процедуры мы трижды вызвали ее для добавления новых записей. Подобного рода процедуры нередко используют для предварительной проверки значений, переданных в качестве параметров. Эти значения можно изменить перед добавлением или исправлением записи, или отказаться добавлять запись, если какое-то значение не удовлетворяет нужному условию. Подобный подход позволяет организовать довольно гибкую систему бизнес-логики, которая будет выполняться на стороне сервера. Следующий пример создаст процедуру выборки: /* Переопределяем терминатор: */ SET TERM ^; /* Создаем процедуру: */ CREATE PROCEDURE Firma_Select RETURNS (F VARCHAR(20), I VARCHAR(20), O VARCHAR(20)) AS BEGIN /* С помощью цикла получаем все строки таблицы: */ FOR SELECT * FROM TABLE_FIRMA INTO :F, :I, :O DO SUSPEND; END^ SET TERM ;^ COMMIT; /* Вызовем полученную процедуру командой SELECT: */ SELECT * FROM Firma_Select; Эта процедура не изменяет данных, она только получает все записи из таблицы Table_Firma, и выводит их одна за другой. В результате, в нижнем окне Interactive SQL мы получим следующую картину:
Рис. 19.1. Результат действий процедуры выборки Firma_Select Как видите, и первая и вторая процедуры выполнили свою задачу. Мы получили такой же набор данных, как из обычной таблицы. В качестве полей здесь выступают выходные параметры F, I и O. Иногда хранимые процедуры применяют для получения набора из данных, которые вообще не хранятся в базе данных. Вот пример такой процедуры: SET TERM ^; CREATE PROCEDURE PrimerProc(I INTEGER) RETURNS (K INTEGER, V VARCHAR(25)) AS BEGIN K = 0; 161
WHILE (K < I) DO BEGIN K = K + 1; V = 'Строка № ' || K; SUSPEND; END END^ SET TERM ;^ COMMIT; /* Вызываем полученную процедуру: */ SELECT * FROM PrimerProc(5); Обратите внимание, в процедуре имеется входной параметр I, в котором мы можем передать целое число, определяющее количество строк в полученном наборе данных. Также у нас имеется два выходных параметра K и V, соответственно, целое число и строка из 25 символов. Эти параметры сформируют поля полученного набора данных. Далее мы обнуляем целую переменную и вызываем цикл WHILE, который будет выполняться до тех пор, пока переменная K будет меньше входящего параметра. В цикле мы вначале прибавляем к этой переменной единицу, после чего формируем строку типа «Строка № 1». При этом мы используем знак конкатенации (объединения) строк «||», а в качестве второй подстроки подставляем целое число, которое хранится в переменной K. Происходит неявное преобразование типов данных. Только не забывайте, целое число можно преобразовать в строку, но не наоборот! Далее мы вызываем оператор SUSPEND, который помещает полученную строку в набор данных. Цикл будет продолжаться столько раз, сколько мы укажем во входящем параметре процедуры. Когда мы вызовем эту процедуру оператором SELECT, то получим следующий результат:
Рис. 19.2. Результат работы процедуры PrimerProc Теперь, открыв в дереве серверов IBConsole базу данных First, и выделив подраздел Stored Procedures, вы увидите три созданных хранимых процедуры, которые в дальнейшем можно вызывать неоднократно.
162
Лекция 20. Генераторы и триггеры. Реализация автоинкрементного поля. Генераторы Генераторами называется специальная область данных, которая хранится в базе данных и содержит какое то целое число. Генераторы – это счетчики, но в отличие от локальных БД, увеличение значения этих счетчиков осуществляется с помощью триггеров. В основном, генераторы используют для создания автоинкрементных полей. Для каждого такого поля придется создавать собственный генератор. Генератор, совместно со специальным триггером, гарантирует, что значение этого поля всегда будет уникальным. Создаются генераторы с помощью оператора CREATE GENERATOR: CREATE GENERATOR Gen1; Внимание! Генераторы можно создавать, но удалить их не получится, поэтому в реальной базе данных прежде продумайте, какие генераторы у вас будут, а потом только создавайте их. Откройте утилиту IBConsole, войдите в локальный сервер и откройте нашу базу данных FIRST. Затем запустите Interactive SQL, и создайте генератор Gen1, как в примере выше. Затем выделите раздел «Generators» в дереве серверов, и в правой части вы увидите наш генератор, а также его текущее значение:
Рис. 20.1. Раздел «Generators» базы данных Как видно из рисунка, генератору сразу присваивается значение 0. Тем не менее, во избежание возможных ошибок, вторым шагом нередко присваивают генератору это значение оператором SET GENERATOR: SET GENERATOR Gen1 TO 0; Выполните этот пример с помощью утилиты Interactive SQL. Таким образом, генераторам можно присваивать любое целое значение, даже отрицательное. Иногда бывает необходимым присваивать генератору не нулевое, а другое значение. Например, если вы перенесли базу данных из Paradox в InterBase. В этом случае, таблица уже содержит записи, которые пронумерованы. Автоинкрементное поле при переносе превращается в INTEGER. Требуется посмотреть последнее значение этого поля, и присвоить генератору именно его. 163
Увеличение шага генератора Как вы понимаете, для ввода каждой следующей записи вовсе нет необходимости открывать IBConsole, смотреть значение генератора и вручную устанавливать новое значение. Для этого используется специальная процедура GEN_ID() (выполнять этот пример не нужно): GEN_ID(Gen1, 1) Процедура содержит два параметра. Первый – имя генератора, второй – шаг, на который требуется увеличить значение. Обычно эту процедуру используют в триггерах, но можно выполнить ее и вручную, с помощью Interactive SQL. Выполните следующий запрос: SELECT GEN_ID(Gen1, 1) FROM RDB$DATABASE; В нижнем окне Interactive SQL будет выведен результат:
Рис. 20.2. Результат SQL-запроса Как видно из рисунка, мы получили значение 1. Выполните также команду COMMIT, чтобы завершить транзакцию, затем закройте Interactive SQL. Выделите раздел Generators и убедитесь, что значение изменилось. Что, собственно, произошло? Дело в том, что когда вы создаете новую базу данных, InterBase прежде всего создает в ней собственные системные таблицы. Одной из таких таблиц является RDB$DATABASE, которая всегда хранит только одну запись с некоторыми системными параметрами базы данных. Эту же таблицу иногда применяют для «пустых» запросов, которые возвращают значение одной из переменных или вычисляемое значение. Нашим предыдущим запросом мы вначале увеличили значение генератора на 1, затем вывели его на экран оператором SELECT. Узнать текущее значение генератора, не увеличивая его, можно строкой: SELECT GEN_ID(Gen1, 0) FROM RDB$DATABASE; где в процедуре GEN_ID() указывается шаг 0. Поскольку генератор может хранить отрицательные значения, а шаг процедуры GEN_ID также может быть отрицательным, то можно установить и обратный автоинкремент, где значения не увеличиваются, а уменьшаются. Впрочем, такой возможностью обычно не пользуются.
164
Совет: если генератор уже находится в использовании, в рабочей базе данных, НИКОГДА не переустанавливаете его значений вручную – это чревато порчей целостности и достоверности данных.
Триггеры Триггерами называются подпрограммы, которые всегда выполняются автоматически на стороне сервера, в ответ на изменение данных в таблицах БД. Триггеры используют тот же встроенный язык программирования, что и хранимые процедуры, но отличаются от них прежде всего тем, что триггеры никогда не вызываются напрямую, ни из клиентских программ, ни с помощью IBConsole, ни из хранимых процедур или других триггеров. Зато в теле триггера можно обратиться к хранимой процедуре. Триггеры начинают действовать в ответ на какое то событие, например, удалили запись в таблице или изменили значение в каком то поле. Кроме того, в триггерах добавлена возможность обращаться к старым и новым значениям столбцов с помощью встроенных переменных OLD и NEW. Триггер может выполняться в двух фазах изменения данных: до(Before) какого то события, или после(After) него. Синтаксис определения триггера следующий: CREATE TRIGGER <имя_триггера> FOR <имя_таблицы> [ACTIVE | INACTIVE] {BEFORE | AFTER} {DELETE | INSERT \ UPDATE} [POSITION <число>] AS [DECLARE [VARIABLE] <переменная тип_данных>;] BEGIN <операторы_триггера> END Как мы видим, создание триггера несколько отличается от создания хранимой процедуры, несмотря на то, что они используют один и тот же алгоритмический язык. Например, у триггера отсутствуют входные и выходные параметры. Разберем его синтаксис по частям.
[ACTIVE | INACTIVE] Необязательный параметр определяет, будет триггер запускаться в ответ на событие, или не будет. По умолчанию устанавливается ACTIVE, то есть триггер будет запускаться. Отключение триггера иногда может быть полезным при отладке приложения.
{BEFORE | AFTER} {DELETE | INSERT | UPDATE} Два обязательных параметра, комбинация которых может запрограммировать триггер на шесть различных событий: Таблица 20.1. Варианты возможных событий триггера Комбинация параметров Описание Триггер вызывается до создания новой строки. Такой триггер обычно используют для поддержки автоинкрементных полей. Также внутри BEFORE INSERT триггера можно изменить входные значения, или сгенерировать значение для какого либо поля. Триггер вызывается после создания новой записи, и не позволяет менять значения полей. Обычно такой триггер используют для AFTER INSERT модификации других, связанных таблиц. Триггер вызывается перед удалением записи. Чаще всего его BEFORE DELETE используют для реализации бизнес-правил. 165
AFTER DELETE BEFORE UPDATE AFTER UPDATE
Триггер вызывается после удаления записи. Его также используют для реализации бизнес-правил, либо модификации других таблиц. Триггер вызывается перед принятием новых значений в поля записи. Позволяет менять входные значения. Триггер вызывается после принятия изменений в запись. Не позволяет менять значения. Обычно используется для модификации связанных таблиц.
Эти шесть вариантов реагирования на события делают триггер самым мощным средством для реализации бизнес-правил, проверки целостности и непротиворечивости данных.
[POSITION <число>] Необязательный параметр, который определяет очередность запуска, если для той же таблицы и для того же события имеется другой триггер. По умолчанию устанавливается 0. Таким образом, можно создать несколько разных триггеров, которые будут запускаться в ответ на одно и то же событие, и каждый будет срабатывать в свою очередь.
AS Как и в хранимых процедурах, командой AS начинается тело триггера. Перед операторской скобкой BEGIN имеется возможность объявить одну или несколько локальных переменных, если они нужны, оператором [DECLARE [VARIABLE] <переменная тип_данных>;]
Переменные NEW и OLD Эти переменные объявлять не нужно, они уже присутствуют в каждом триггере. Соответственно, переменные хранят старое и новое значения какого либо поля. Обращаться к этим значениям можно так: NEW.<имя_поля> Эти переменные могут быть использованы для: Получения допустимых значений по умолчанию. Проверки входных данных, и при необходимости, их изменения. Получения значений полей для модификации других таблиц. Реализации автоинкрементных полей. Имеются некоторые ограничения на использование этих переменных. Так, значения NEW могут быть использованы в событиях INSERT и UPDATE, при удалении записи NEW имеет значение NULL. Значения OLD доступны в событиях UPDATE и DELETE, а при вставке новой записи OLD имеет значение NULL. Для примера создадим триггер, который срабатывает перед вставкой новой записи и проверяет входящее целое число. Если оно отрицательно, триггер изменяет его на ноль: SET TERM ^; CREATE TRIGGER NotOtric FOR Table_Cel ACTIVE BEFORE INSERT AS BEGIN IF (NEW.Dlinnoe < 0) THEN NEW.Dlinnoe = 0; 166
END^ SET TERM ;^ Создайте этот триггер с помощью Interactive SQL. Затем в этой же утилите введите два значения (подробней о редактировании мы поговорим на следующей лекции): INSERT INTO Table_cel (Dlinnoe) VALUES (5); INSERT INTO Table_cel (Dlinnoe) VALUES (-10); SELECT * FROM Table_cel; Как видите, в таблице появились две новые строки:
Рис. 20.3. Две новые записи В первом случае значение 5 сохранилось без изменения, а во второй записи триггер изменил значение -10 на 0.
Реализация автоинкрементных ключевых полей Для создания поля, значение которого автоматически увеличивается на единицу, нужно сделать несколько действий: 1. Создать генератор для ключевого поля. Ключевое поле должно иметь тип INTEGER, быть NOT NULL и объявлено как PRIMARY KEY. Собственно, генератор можно использовать для любого автоинкрементного поля, не обязательно ключевого. Но чаще всего генераторы используют именно для ключевых полей. 2. Присвоить генератору значение 0 (или иное, если таблица перенесена из другой БД, и уже содержит записи). 3. Создать триггер BEFORE INSERT, увеличивающий это значение на 1. Итак, приступим. В нашей базе данных имеется таблица Tovar, в которой первое поле ID объявлено как INTEGER NOT NULL. К сожалению, поле не было объявлено, как ключевое PRIMARY KEY. Изменим таблицу, добавив в нее первичный ключ по полю ID: ALTER TABLE TOVAR ADD PRIMARY KEY (ID); Теперь сделаем это поле автоинкрементным: /*Создаем генератор*/ CREATE GENERATOR Gen_Tovar ; /*Присваиваем генератору начальное значение*/ SET GENERATOR Gen_Tovar TO 0; /*Создаем триггер*/ SET TERM ^; CREATE TRIGGER Tr_Tovar FOR Tovar ACTIVE BEFORE INSERT AS BEGIN 167
IF (NEW.ID IS NULL) THEN NEW.ID = GEN_ID(Gen_Tovar, 1); END^ SET TERM ;^ /* Завершаем транзакцию: */ COMMIT; Операторы из данного примера создают автоматическое увеличение значения поля на 1. Таким образом, вставка первой же записи установит значение 1. Следующая запись будет 2 и так далее. Все это реализуется в пределах транзакции, то есть даже если множество пользователей вносит изменения в таблицу, значения генератора всегда будут уникальны. Кстати, именно потому, что изменения таблиц происходят внутри транзакций, а приложению может потребоваться узнать значение поля до того, как транзакция завершилась, настоятельно рекомендуется вместо простого присваивания: NEW.ID = GEN_ID(Gen_Tovar, 1); делать это вместе с проверкой на NULL: IF (NEW.ID IS NULL) THEN NEW.ID = GEN_ID(Gen_Tovar, 1); Теперь мы можем проверить работу нашего автоинкремента. Создайте следующий запрос: INSERT INTO Tovar (Nazvanie, Stoimost) VALUES (‘Сахар’, 10.50); INSERT INTO Tovar (Nazvanie, Stoimost) VALUES (‘Крупа’, 8.20); SELECT * FROM Tovar; Если вы все сделали правильно, то в таблице появятся две записи, а поле ID будет автоматически увеличиваться на 1:
Рис. 20.4. Демонстрация работы автоинкрементного поля Обратите внимание на то, что мы вносили значения только в поля Nazvanie и Stoimost. Значения для поля ID генерировались триггером автоматически. Не забудьте перед закрытием окна Interactive SQL закрыть транзакцию командой COMMIT. В отличие от хранимых процедур, для триггеров не предусмотрен раздел в дереве серверов утилиты IBConsole. Однако увидеть наш триггер можно. Триггер создавался для таблицы Tovar. Выделите ее, нажмите правую кнопку мыши и в контекстном меню выберите команду Properties. Откроется окно свойств таблицы, в котором следует перейти на вкладку Metadata. В этом окне, после описания создания таблицы, вы увидите описание нашего триггера Tr_Tovar.
168
Лекция 21. Команды модификации данных DML. Скрипты. Команды модификации данных относятся к языку DML (Язык Манипулирования Данными), который является подмножеством языка SQL. Значения могут быть помещены в таблицу, изменены или удалены следующими операторами: INSERT (Вставить) UPDATE (Изменить) DELETE (Удалить) В клиентском приложении мы имеем возможность пользоваться табличным компонентом Table, в котором эти действия можно выполнять с помощью методов, однако это удается не всегда. Например, мы вставим новую запись методом Append в таблицу, которая имеет автоинкрементное поле, заполняемое триггером автоматически. Затем мы введем все значения, кроме автоинкремента. Далее, при попытке выполнить метод Post, сохраняющий запись, мы, скорее всего, получим ошибку. Связано это с тем, что триггер BEFORE INSERT срабатывает после того, как табличный компонент выполнит метод Post. А поскольку ключевое поле имеет параметр NOT NULL, то InterBase не даст нам вставить запись с незаполненным ключевым полем. Зато вставка записи запросом INSERT осуществляется без проблем. Поэтому редактирование данных чаще всего перекладывают на компонент Query, свойству SQL которого присвоен нужный запрос. А значит, необходимо знать команды модификации, и уметь их применять.
INSERT С оператором INSERT мы сталкивались на прошлой лекции и знаем, что он предназначен для вставки в таблицу новой записи. Синтаксис оператора следующий: INSERT INTO <имя_таблицы> [(<список_полей>)] VALUES (<список_значений>); Очередность вставляемых значений должна совпадать с очередностью указанных полей. Список полей можно и не указывать, в этом случае подразумевается, что вставляются значения для всех полей таблицы, и в том же порядке, в каком в ней содержатся столбцы. Список полей, если указывается, может содержать не все поля таблицы, а только нужные, причем в произвольном порядке. Простейший пример вставки записи: INSERT INTO Tovar (Nazvanie, Stoimost) VALUES (‘Соль’, 3.00); Если в списке полей указаны не все поля таблицы, то отсутствующим полям автоматически добавляется значение NULL. Однако это значение можно добавить и явно, например, если в списке полей указано поле, для которого в настоящий момент нет значения. Разумеется, это поле не должно быть NOT NULL. Например: INSERT INTO Table_Cel(Dlinnoe, New_String) VALUES (10, NULL) ; Помимо простой вставки, оператор можно использовать для добавления группы записей из другой таблицы. При этом ВМЕСТО параметра VALUES указывается встроенный оператор SELECT, делающий выборку записей из другой таблицы. Рассмотрим следующий пример. Таблица Tovar, которую мы создавали в 17 лекции, имеет три поля, все со значением NOT NULL. Поле ID заполняется генератором автоматически, поля Nazvanie и Stoimost нужно заполнять. Создадим и заполним временную таблицу TempTable: /*Создаем таблицу*/ CREATE TABLE TempTable( TString VARCHAR(20) COLLATE PXW_CYRL, TDouble DOUBLE PRECISION); 169
/*Вводим новые записи*/ INSERT INTO TempTable VALUES ('Спички', 0.2); INSERT INTO TempTable VALUES ('Конфеты', 10.5); INSERT INTO TempTable VALUES ('Масло сливочное', 4.40); /*Подтверждаем сделанные изменения*/ COMMIT; Выполните этот код в IBConsole с помощью Interactive SQL. Убедитесь, что таблица TempTable действительно появилась в списке таблиц и имеет три записи. Теперь мы можем добавить все эти записи в таблицу Tovar одним оператором вставки: INSERT INTO Tovar (Nazvanie, Stoimost) SELECT * FROM TempTable; /*Подтверждаем сделанные изменения*/ COMMIT; Как видите, мы указали лишь два поля для вставки, а из таблицы TempTable выбрали все поля (их тоже два). Поле ID заполнилось триггером автоматически. Самое главное, чтобы количество, тип и очередность столбцов, указанных для вставки, совпадали с полученными столбцами в выборке SELECT. Выборка данных из другой таблицы может быть и сложней. Например, добавить только тот товар, стоимость которого не превышает 10, можно следующим образом: INSERT INTO Tovar (Nazvanie, Stoimost) SELECT * FROM TempTable WHERE TDouble < 10.00
UPDATE Оператор UPDATE позволяет изменить значение существующей записи. Синтаксис оператора следующий: UPDATE <имя_таблицы> SET <имя_столбца = значение> [,<имя_столбца = значение>, …] [WHERE <условия_поиска>] Попробуем выполнить простейший пример обновления данных. Откройте утилиту IBConsole, войдите в базу данных First и откройте Interactive SQL. Введите следующий код: UPDATE TempTable SET TDouble = 5.5; COMMIT; Теперь посмотрим, что у нас получилось, выполнив следующий запрос: SELECT * FROM TempTable;
170
Рис. 21.1. Результат изменения значений Как видно из рисунка, новое значение в поле TDouble получили все записи таблицы! Отсюда вывод: параметр WHERE в операторе UPDATE не является обязательным, однако если его исключить, то обновлению подвергнуться все записи таблицы. Если же нам необходимо изменить только одну запись, ее нужно найти с помощью WHERE: UPDATE TempTable SET TDouble = 15.75 WHERE TString = ‘Конфеты’; COMMIT; Командой UPDATE можно модифицировать не одно поле записи, а сразу несколько. В этом случае имена столбцов и значения разделяются запятыми: UPDATE Tovar SET Nazvanie = ‘Крупа манная’, Stoimost = 7.35 WHERE ID = 2; COMMIT; В операторе UPDATE в качестве новых значений можно указывать и выражения, которые возвращают результат, соответствующий типу поля. Например, если произошло очередное подорожание, то можно изменить стоимость всех товаров одновременно, например: UPDATE Tovar SET Stoimost = Stoimost * 1.5; COMMIT; Также мы имеем возможность в качестве значения присваивать NULL, если поле таблицы это допускает: UPDATE TempTable SET TDouble = NULL WHERE TString = ‘Конфеты’; COMMIT;
DELETE Оператор DELETE удаляет всю запись таблицы и имеет следующий синтаксис: DELETE FROM <имя_таблицы> [WHERE <условия_поиска>]; Как и в предыдущем случае, параметр WHERE не является обязательным, но если его не указывать, будут удалены ВСЕ ЗАПИСИ таблицы! Поэтому будьте осторожны с этим оператором: 171
/*Удаляем все записи временной таблицы*/ DELETE FROM TempTable; Используя WHERE, можно удалить одну запись или группу записей, удовлетворяющих условиям поиска: DELETE FROM Table_Cel WHERE Dlinnoe < 10; Если вы удаляете запись из таблицы, имеющей автоинкрементный ключ, имейте в виду, что нумерация значений ключевого поля зависит от значения генератора. Другими словами, если вы удалили запись со значением ключа 3, то это значение уже не будет использовано в других записях, если только вы не укажете его явно. Например: /*Удаляем старую запись*/ DELETE FROM Tovar WHERE ID = 3; /*Добавляем новую запись с этим же номером*/ INSERT INTO Tovar(ID, Nazvanie, Stoimost) VALUES(3, ‘Соль экстра’, 3.0); /*Подтвердим изменения*/ COMMIT; Значения для автоинкрементного поля генерируются с помощью триггера Tr_Tovar и генератора Gen_Tovar. Прежде чем изменить значение генератора, и присвоить его полю ID, триггер делает проверку на NULL. В приведенном выше примере в поле ID добавляется явное значение 3, то есть, триггер не сработает, значение генератора не изменится. Однако на практике, во избежание возможных ошибок, явно вмешиваться в нумерацию ключевого поля не рекомендуется.
Скрипты Программист не всегда находится там, где эксплуатируют его программу. Нередко он работает в другом офисе, или даже в другом городе. В этом случае за работой БД следит администратор БД или даже просто «продвинутый» оператор. При необходимости внести изменения в базу данных, порой очень сложно объяснить непрограммисту по телефону, что ему следует делать. В этом случае помогут специальные файлы, которые принято называть скриптами. Это обычные текстовые файлы с расширением *.sql, которые можно переслать по e-mail администратору БД. Последнему останется только подгрузить нужный скрипт в утилиту IBConsole и дать команду на выполнение. Для примера создадим скриптовый файл, который создает в базе данных таблицу, индекс, генератор и триггер для реализации автоинкрементного ключевого поля. Затем файл содержит команды вставки в таблицу новых записей. Откройте любой текстовый редактор, например, «Блокнот» или любой другой, который редактирует текст в кодировке Windows (старые редакторы используют DOS-кодировку, а менеджер файлов FAR позволяет переключаться между этими кодировками). Создайте файл MyScript.sql со следующим содержимым: /*------------Начало файла------------*/ /*Создаем таблицу*/ CREATE TABLE Days( ID INTEGER NOT NULL PRIMARY KEY, DayOfWeek VARCHAR(11) COLLATE PXW_CYRL ); 172
/*Создаем индекс*/ CREATE ASC INDEX I_Days ON Days(DayOfWeek); /*Создаем генератор*/ CREATE GENERATOR G_Days; /* Создаем триггер для реализации автоинкрементного поля */ SET TERM ^; CREATE TRIGGER T_Days FOR Days ACTIVE BEFORE INSERT POSITION 0 AS BEGIN IF(New.ID is NULL) THEN New.ID = Gen_ID(G_Days, 1); END^ COMMIT^ SET TERM ;^ /*Заполняем таблицу значениями*/ INSERT INTO Days(DayOfWeek) VALUES ('Понедельник'); INSERT INTO Days(DayOfWeek) VALUES ('Вторник'); INSERT INTO Days(DayOfWeek) VALUES ('Среда'); INSERT INTO Days(DayOfWeek) VALUES ('Четверг'); INSERT INTO Days(DayOfWeek) VALUES ('Пятница'); INSERT INTO Days(DayOfWeek) VALUES ('Суббота'); INSERT INTO Days(DayOfWeek) VALUES ('Воскресенье'); /*Подтверждаем изменения*/ COMMIT; /*------------Конец файла------------*/ Предположим, мы переслали этот файл администратору БД. Теперь ему нужно сделать следующие шаги (выполните их поочередно): 1. 2. 3. 4.
Открыть утилиту IBConsole и войти в базу данных First. Вызвать окно Interactive SQL. Выбрать команду меню Query -> Load Script, найти и открыть наш файл MyScript.sql. Нажать кнопку «Execute Query».
Все, таблица Days создана и заполнена! Эти четыре шага сможет сделать даже начинающий пользователь, а вы имеете возможность удаленного редактирования базы данных. Главное – правильно написать скриптовый файл и переслать его.
173
Лекция 22. Соединение с БД клиентской программы. Проблемы русских букв в InterBase. Соединение клиентской программы с базой данных производится с помощью одного из механизмов управления БД. Из стандартной библиотеки компонентов это, как правило, BDE, dbExpress и InterBase Express (IBX). Нередко используют и компоненты сторонних разработчиков, например FIBPlus, но в данном курсе мы не будем рассматривать работу с нестандартными компонентами. Что касается ADO, тут ситуация несколько сложней. Корпорация Microsoft не очень жалует сторонних разработчиков, особенно популярных, поэтому соединиться с базой данных InterBase через механизм ADO довольно проблематично. Встроенных в Windows средств для этого не существует, а искать и устанавливать драйверы InterBase для ODBS - хлопотно. Поэтому мы не будем рассматривать способы соединения с InterBase через ADO, тем более что для этого существует немало других, гораздо более удобных и прозрачных инструментов. Вкратце все же заметим, что для нормальной работы с InterBase через ADO нужен ODBC-драйвер Intersolv, который ранее входил в поставку InterBase 5.5, 5.6 или драйвер EasySoft, поставляемый отдельно. Эти драйверы позволяют установить на уровне настроек нужную кодировку, например WIN1251. Отдельно драйвер Intersolv не продается, не поставляется, и более не поддерживается с момента выхода InterBase 6. А вот «устаревший» механизм BDE позволяет соединиться с БД InterBase без всяких проблем. На этой лекции мы вкратце рассмотрим способы связывания приложения с БД через стандартные механизмы.
BDE В лекции № 15 с помощью утилиты SQL Explorer мы создавали псевдоним (alias) базы данных FirstIB. Этим псевдонимом мы теперь и воспользуемся. Убедитесь, что он у вас существует, а если нет – зарегистрируйте его, как описано в лекции № 15. Сервер InterBase при этом должен быть включен. Приступим к созданию приложения. Загрузите Delphi. С вкладки Data Controls установите на форму сетку DBGrid и навигатор DBNavigator, а также обычную кнопку. Для связи с базой данных нам потребуются компоненты Database, Table и Query с вкладки BDE, а также DataSource с вкладки Data Access для связи таблицы с сеткой и навигатором:
Рис. 22.1. Форма приложения Далее, выделите компонент Database. Здесь нам нужно сделать некоторые настройки: В свойстве AliasName выберите наш псевдоним FirstIB.
174
В свойстве DatabaseName впишите название базы данных. Оно может быть любым, например, DBase1. Свойство LoginPrompt переведите в False, чтобы программа при запуске не запрашивала имя пользователя и пароль. Раскройте сложное свойство Params и впишите туда следующие параметры:
Рис. 22.2. Редактирование свойства Params Теперь свойство Connected можно перевести в True. Если вы все сделали правильно, то это удастся. Иначе посмотрите, запущен ли у вас сервер InterBase? Далее, табличный компонент переименуйте в TTovar, а компонент запроса – в Q1 (он нам понадобится для служебных целей). У обоих этих компонентов в свойстве DatabaseName выберите только что созданное имя базы данных DBase1. У таблицы, кроме того, в свойстве TableName выберите TOVAR, а свойство Active переведите в True. Сетку и навигатор подключите к DataSource1 (через свойство DataSource), а его, в свою очередь, подключите к таблице TTovar (через свойство DataSet). У вас в сетке должно появиться содержимое таблицы. Сохраните проект, скомпилируйте и запустите полученную программу. Теперь попробуйте добавить запись при помощи таблицы, не заполняя поля ID (оно заполняется триггером). У вас выйдет ошибка:
Рис.22.3. Ошибка изменения данных таблицы Как видите, утверждение, что триггер BEFORE INSERT срабатывает ПОСЛЕ того, как таблица выполнит метод POST, подтвердилось. Здесь следует сделать одно замечание. Наш триггер вначале делает проверку на значение NOT NULL в ключевом поле. Если в этом поле будет значение, триггер ничего делать не будет. Однако заполнять автоинкрементное поле вручную нельзя, так как мы нарушим работу генератора. Единственное исключение – если мы удалим запись, а затем пожелаем вставить на ее место другую запись с таким же значением в ключевом поле. В этом случае мы будем вставлять запись, не изменяя значения генератора; триггер в этом случае не сработает. 175
Нажмите OK, а потом выберите команду меню Run -> Program reset, чтобы закрыть повисшее приложение. Итак, мы убедились, что добавлять запись в таблицу с автоинкрементным полем с помощью табличного компонента проблематично. Зато существующие записи можно редактировать или удалять. А для добавления воспользуемся компонентом запроса Query (который для краткости мы переименовали в Q1). Сгенерируйте обработчик нажатия на кнопку «Добавить запись», которая находится под сеткой. В полученной процедуре впишем следующий код: procedure TfMain.Button1Click(Sender: TObject); var s: String; tov,stoim: String; begin tov := ''; stoim := ''; //получим данные о товаре: if not InputQuery('Добавление товара', 'Введите новый товар:', tov) then Exit; if not InputQuery('Стоимость товара', 'Введите стоимость товара:', stoim) then Exit; //формируем строку запроса s:= 'Insert Into Tovar(Nazvanie, Stoimost) '+ 'Values (' + QuotedStr(tov) + ',' + stoim + ')'; //вписываем и выполняем запрос: Q1.SQL.Clear; Q1.SQL.Add(s); Q1.ExecSQL; //обновим таблицу: TTovar.Refresh; end;
Для демонстрации работы BDE мы использовали простейший интерфейс, без проверки введенных значений на правильность. Вначале функцией InputQuery мы запрашиваем наименование нового товара, здесь вам придется следить, чтобы вписать не более 20 символов (длина поля Nazvanie). Затем, вводя стоимость товара, нужно следить за тем, чтобы сумма вводилась в формате xxx.xxx То есть, разделителем между целой и дробной частью числа должна быть точка, как того требует синтаксис InterBase. В сетке DBGrid, тем не менее, разделителем будет отображаться запятая. Однако сумма может быть и без дробной части, если товар, например, стоит ровно 10 рублей. Если пользователь не введет название товара или его стоимость, происходит выход из процедуры. Получив данные, мы формируем строку запроса, вроде: Insert Into Tovar(Nazvanie, Stoimost) Values ('Крупа пшенная’, 7.35); После чего присваиваем эту строку свойству SQL компонента Q1. А поскольку запрос выполняемый, то вместо метода Q1.Open мы используем Q1.ExecSQL. После чего нам остается только обновить отображаемый набор данных в таблице TTovar. Теперь новые записи добавляются без проблем, а удаление и редактирование записей можно делать с помощью компонентов DBNavigator и Table.
Проблемы русских букв в InterBase Вообще то, эти проблемы касаются не только русских, а вообще любых букв, отличных от английского алфавита. Здесь нужно сделать несколько замечаний: Наименования идентификаторов (названий) таблиц, полей, индексов и проч. в InterBase недопустимо давать русскими буквами. 176
При создании базы данных не забывайте указывать кодировку WIN1251, как кодировку по умолчанию. Если вы подключаете к серверу через IBConsole уже существующую базу данных, также не забывайте указывать эту кодировку. Если вы подключаете клиентское приложение к БД через BDE, достаточно создать псевдоним БД, в котором в качестве языкового драйвера указать Pdox ANSI Cyrillic, как это мы делали в лекции № 15. При подключении приложения к БД с помощью механизмов «прямого доступа», таких как IBX, FIBPlus и т.п., в списке параметров нужно указать дополнительный параметр кодировки «lc_ctype=WIN1251», без этого вы сможете просматривать таблицы с русским текстом в полях, но не сможете добавлять новых записей, сделанных кириллицей. В InterBase имеется встроенная функция UPPER, которая преобразует символы строковых полей в заглавные буквы. Эту возможность нередко используют для поиска данных. Однако при работе с кириллицей она будет работать корректно лишь в том случае, если строковое поле было создано с кодировкой WIN1251 и порядком сортировки PXW_CYRL. В случае других сочетаний параметров, функция не сможет правильно отображать русский текст заглавными буквами. Например, отобразить текст поля Nazvanie из таблицы Tovar заглавными буквами можно следующим запросом: «SELECT UPPER(Nazvanie), Stoimost FROM Tovar» Если все же вы создали поле с кодировкой WIN1251, но не указали сортировку PXW_CYRL, а вам требуется выполнить поиск с функцией UPPER, то вы можете сменить порядок сортировки явно, прямо в запросе. Например, требуется найти запись со значением «Сахар» в поле Nazvanie. При этом мы не знаем, как пользователь вписал это название: «САХАР», «Сахар» или «сахар». Сделать поиск, не зависящий от регистра можно запросом: «SELECT * FROM Tovar WHERE UPPER(Nazvanie COLLATE PXW_CYRL) = 'САХАР'»
dbExpress Технология dbExpress реализует так называемый однонаправленный курсор, то есть, данные можно будет листать только сверху - вниз. Это существенно ускоряет работу с базой данных, но в большинстве случаев делает технологию неудобной для клиентских приложений. Правда, начиная с шестой версии Delphi, в dbExpress появился компонент TSimpleDataSet, который создает двунаправленный курсор и снимает большинство ограничений. В то же время, программа лишается преимущества скорости однонаправленного курсора. Все это приводит к тому, что технологию dbExpress на практике используют крайне редко. Тем не менее, рассмотрим подключение программы к базе данных с помощью этой технологии. Создайте новое приложение и спроектируйте форму такой же, как в предыдущем примере, но без компонентов BDE. Из компонентов доступа, помимо DBNavigator и DBGrid нам понадобятся: DataSource с вкладки DataAccess; компоненты SQLConnection, SimpleDataSet и SQLQuery с вкладки dbExpress. SQLConnection предназначен для подключения к базе данных, он является аналогом компонента TDatabase в BDE. Остальные компоненты подключаются к базе через него. SimpleDataSet будет играть роль таблицы с двунаправленным курсором, а SQLQuery представляет собой обычный компонент запросов. Выделите компонент SQLConnection. В его свойстве ConnectionName выберите из списка вариантов подключения IBConnection, при этом автоматически настроятся некоторые свойства, специфичные для InterBase. Далее щелкните по сложному свойству Params – загрузится окно параметров, которое нужно настроить так:
177
Рис. 22.4. Окно параметров SQLConnection Здесь нам нужно будет вручную вписать адрес и имя файла с базой данных в параметр Database. Затем указать параметры ServerCharSet (WIN1251) и SQLDialect (3). После чего кнопкой ОК закрыть окно. Далее перевести свойство LoginPrompt в False, чтобы программа не запрашивала имя и пароль пользователя, а свойство Connected в True, чтобы соединиться с базой данных. Выделим компонент SimpleDataSet. Переименуйте его свойство Name в TTovar (мы выбрали такое же имя, как у таблицы из прошлого примера, чтобы не менять код нажатия на кнопку). В свойстве Connection выберите SQLConnection1. Раскройте сложное свойство DataSet, и откройте подсвойство CommandText. Откроется окно редактора, в котором нужно будет составить запрос выборки всех данных из таблицы Tovar: SELECT * FROM TOVAR:
Рис. 22.5. Окно редактора запросов 178
Затем кнопкой ОК закройте окно, и можете закрыть сложное свойство DataSet. А вот свойство Active переведите в True, чтобы открыть полученный набор данных. Теперь займемся компонентом SQLQuery1. Свойство Name переименуйте в Q1, в свойстве SQLConnection выберите SQLConnection1. Напоследок соедините DataSource1 с компонентом TTovar, а сетку и навигатор – с DataSource1. Данные из таблицы должны отобразиться в сетке. Код для кнопки такой же, как в предыдущем примере. Сохраните проект, скомпилируйте и попробуйте добавить пару наименований товара. Как видите, реализация подключения несколько сложней, чем в предыдущем примере, но зато на клиентский ПК не нужно устанавливать BDE!
InterBase Express (IBX) Эта технология является «родной» для сервера InterBase, и из стандартных механизмов доступа наиболее удобна. А значит, чаще всего используют именно ее. В будущих лекциях мы поработаем с этим механизмом плотнее, а пока вкратце рассмотрим работу с ним на примере все того же приложения. Создайте новое приложение, и разместите на форме сетку DBGrid, навигатор DBNavigator и кнопку так же, как в прошлых примерах. На сетку поместите один компонент DataSource с вкладки DataAccess, и с вкладки InterBase следующие компоненты: IBDatabase, IBTransaction, IBTable и IBQuery. Компонент IBDatabase выполняет подключение к базе данных, все остальные компоненты вкладки соединяются с БД через него. Все действия с базой данных происходят на уровне транзакций, компонент IBTransaction как раз обеспечивает такую транзакцию. В приложении обязательно должен быть хотя бы один IBTransaction, соединенный с IBDatabase. В сложных многозвенных базах данных таких транзакций может быть несколько. Вообще, имеется возможность для каждого набора данных (IBTable или IBQuery) использовать собственный компонент IBTransaction с различными настройками. Однако делать так не рекомендуется. Обычно наборы данных разделяют на группы, например, НД только для чтения, НД только для записи, НД для чтения/записи с мягкими или жесткими условиями соединения (об этом в следующих лекциях). В этом случае для каждой группы наборов данных выделяют свой компонент IBTransaction. Компоненты IBTable и IBQuery почти ничем не отличаются от простых Table и Query. Итак, выделите компонент IBDatabase. Через его свойство DatabaseName найдите и подключите файл First.gdb. Далее откройте свойство Params (откроется окно редактора). В окне впишите следующие параметры: user_name=sysdba password=masterkey lc_ctype=win1251 Обратите внимание на то, что пробелы перед и после знака «=» недопустимы. Все слова можно вводить маленькими буквами. Каждый параметр указывайте на новой строке. Нажмите кнопку ОК и закройте окно редактора параметров. Свойство LoginPrompt переведите в False, а свойство Connected в True – связь с БД установлена. В сетевом варианте обычно LoginPrompt оставляют True, чтобы знать, какой пользователь на самом деле вошел в базу данных. Далее выделите компонент IBTransaction и в его свойстве DefaultDatabase выберите нашу БД IBDatabase1. Затем выделите таблицу IBTable1, переименуйте ее в TTovar (свойство Name), в свойстве Database выберите IBDatabase1. При этом в свойстве Transaction таблицы автоматически должен появиться компонент транзакций IBTransaction1. Если по каким то причинам этого не случилось, выберите его вручную. В свойстве TableName установите таблицу Tovar, а свойство Active переведите в True – таблица открыта.
179
Теперь выделим запрос IBQuery1. Свойство Name переименуем в Q1, в свойстве Database выберем IBDatabase1. И у этого компонента в свойстве Transaction автоматически должна появиться транзакция IBTransaction1. Осталось только связать DataSource с таблицей TTovar, а сетку и навигатор – с DataSource. Код нажатия на кнопку такой же, как в предыдущих примерах. Как видите, работа с различными механизмами доступа к серверу InterBase отличается лишь в деталях. Механизм IBX отличается тем, что в приложении обязательно должна быть хотя бы один компонент транзакций. Но в реальной практике таких компонентов может быть несколько, каждый из них обычно имеет свои настройки, а различные наборы данных могут подключаться к различным компонентам транзакций. Подробнее о транзакциях мы поговорим в лекции № 24.
180
Лекция 23. Стандартные функции InterBase. UDF. InterBase имеет в своем арсенале весьма незначительный набор стандартных функций, которые можно использовать в запросах. Это связано с тем, что, во-первых, основным достоинством InterBase является малый объем сервера, и низкие требования к аппаратному обеспечению, что позволяет использовать InterBase практически на любом компьютере. А во-вторых, InterBase предоставляет очень привлекательную возможность для программиста создавать собственные функции (UDF) и подключать их к серверу, к конкретной базе данных. В рамках лекции мы рассмотрим и эту тему.
Стандартные функции InterBase Стандартные функции InterBase представлены в таблице 23.1: Таблица 23.1 Функция Тип Агрегатная AVG () Агрегатная COUNT () MAX () MIN () SUM () CAST () UPPER () GEN_ID ()
Назначение Вычисляет и возвращает среднее значение из набора записей. Подсчитывает и возвращает количество записей, удовлетворяющих условию поиска запроса. Находит и возвращает максимальное значение из набора записей. Находит и возвращает минимальное значение из набора записей. Суммирует значения всех записей и возвращает результат. Преобразует значение столбца из одного типа данных в другой. Преобразует все символы строки в верхний регистр. Возвращает (и увеличивает) значение генератора.
С большинством из этих функций мы уже сталкивались, разберем их синтаксис подробней, и опробуем на примерах. Для этого откроем утилиту IBConsole (сервер InterBase должен быть запущен), откроем в нем нашу базу First.gdb и запустим Interactive SQL. Примеры из запросов будем делать в этом окне.
AVG Агрегатная функция, возвращает среднее арифметическое значение из множества значений в указанном числовом столбце или выражении. Если значение какого-либо столбца равняется NULL, оно автоматически исключается из вычисления, что предотвращает искажение возвращаемого результата. Если число строк, возвращенных запросом SELECT равно 0, то AVG вернет NULL. Синтаксис: AVG([ALL] <столбец|выражение> | DISTINCT <столбец|выражение>); Если указан необязательный параметр ALL (по умолчанию), то среднее арифметическое значение вычитается из всех столбцов или выражения. Если же указан параметр DISTINCT, то при вычислении будут исключены повторяющиеся значения. Пример: SELECT AVG(Stoimost) FROM Tovar
COUNT Функция подсчитывает и возвращает количество записей, удовлетворяющих условию поиска. Если условие не задано, функция возвращает количество всех записей набора данных. Синтаксис: COUNT ([DISTINCT] <имя_поля>);
181
Если указан необязательный параметр DISTINCT, из вычисления будут исключены повторяющиеся значения. Примеры (выполняйте их по очереди, а не разом, иначе в окне Interactive SQL вы получите результат только последнего примера – каждая новая выборка будет перекрывать результат работы предыдущей выборки): /*Количество всех записей:*/ SELECT COUNT(Nazvanie) FROM Tovar; /*То же самое, но исключив повторяющиеся значения:*/ SELECT COUNT(DISTINCT Stoimost) FROM Tovar; /*Количество всех записей, удовлетворяющих условию:*/ SELECT COUNT(Nazvanie) FROM Tovar WHERE Stoimost = 10;
MAX / MIN Агрегатные функции, которые подсчитывают и возвращают максимальное или минимальное число из множества значений в указанном столбце или выражении. Если какое-то значение из множества равно NULL, оно исключается из вычислений. Если число записей в запросе равно нулю, функции возвращают NULL. Если MAX / MIN применяются для строковых столбцов CHAR / VARCHAR, то максимум или минимум определяется в зависимости от символьного набора (CHARACTER SET) и порядка сортировки (COLLATION). Другими словами, функции возвращают максимальный или минимальный текст из всех строк, учитывая, что ‘А’ меньше, чем ‘Я’. Синтаксис: MAX([ALL] <столбец|выражение> | DISTINCT <столбец|выражение>); MIN([ALL] <столбец|выражение> | DISTINCT <столбец|выражение>); Примеры: /*Максимальное и минимальное значения из числового столбца стоимости товаров:*/ SELECT MAX(Stoimost), MIN(Stoimost) FROM Tovar; /*Максимальное и минимальное значения из строкового столбца с названием товаров:*/ SELECT MAX(Nazvanie), MIN(Nazvanie) FROM Tovar;
SUM Функция возвращает сумму всех значений из столбца таблицы или из выражения. Как и в предыдущих примерах, значения NULL автоматически исключаются из расчетов, а если количество строк в указанном наборе данных будет равно нулю, функция вернет NULL. Синтаксис: SUMM([ALL] <столбец|выражение> | DISTINCT <столбец|выражение>); Пример: /*Сумма всех значений из числового столбца стоимости товаров:*/ SELECT SUM(Stoimost) FROM Tovar;
182
CAST Функция позволяет преобразовывать один тип данных в другой, или трактовать его, как другой тип данных. Функцию удобно использовать в запросах, которые смешивают данные разных типов в одном поле. Также CAST может использоваться в условиях поиска. Следует помнить, что типы данных должны соответствовать преобразованию. То есть, любое число можно превратить в строку, однако не любую строку можно превратить в число. Если строка содержит значение ‘123’, она корректно преобразуется, и функция вернет правильный результат. Если строка содержит значение ‘АБВ’, то ее невозможно будет преобразовать в числовой тип, и функция вернет ошибку. Типы данных, преобразуемые функцией CAST, представлены в таблице 23.2: Таблица 23.2 Исходный тип данных NUMERIC CHAR, VARCHAR DATE
Возможный для преобразования тип данных CHAR, VARCHAR, DATE NUMERIC, DATE CHAR, VARCHAR, DATE
Под типом данных NUMERIC подразумеваются целые и вещественные числовые типы. Синтаксис: CAST(<поле | значение> AS <тип_данных>) Пример: /*Вывод в одном поле объединенных значений строкового столбца Nazvanie */ /*и числового поля Stoimost, преобразованного в строку:*/ SELECT Nazvanie || ‘ - ’ || CAST(Stoimost AS VARCHAR(25)) FROM Tovar; В примере использован символ конкатенации (объединения) строк «||», вторая часть строки преобразуется функцией CAST из типа DOUBLE PRECISION.
UPPER Преобразует все символы строки к верхнему регистру. Если набор символов и порядок сортировки поддерживают такое преобразование, функция UPPER вернет строку с символами в верхнем регистре. Иначе функция вернет строку без изменений. Как уже говорилось в предыдущей лекции, функция корректно преобразует строки с русскими буквами только в том случае, если установлен набор символов для строки WIN1251, а порядок сортировки PXW_CYRL. Синтаксис: UPPER(<значение>); Поскольку у нас в базе данных все символьные столбцы создавались с набором WIN1251 и порядком сортировки PXW_CYRL, то функция сработает правильно. Для наглядности в примере ниже мы выведем один и тот же столбец дважды, в первом случае не изменяя порядок сортировки, чтобы функция корректно перевела символы в верхний регистр. А во втором поле порядок сортировки изменим на WIN1251, чтобы функция не смогла сделать преобразование: SELECT UPPER(Nazvanie), UPPER(Nazvanie COLLATE WIN1251) FROM Tovar; 183
В результате мы получим примерно такой набор данных:
Рис. 23.1. Преобразование функцией UPPER строк с различным порядком сортировки Как видно из примера, текст с набором символов WIN1251 и порядком сортировки WIN1251 возвращается функцией UPPER без изменений.
GEN_ID Функция является механизмом, увеличивающим значение указанного генератора на указанный шаг, и возвращающим текущее значение этого генератора. Если шаг равен 0, увеличения значения не происходит. Синтаксис: CEN_ID(<генератор>, <шаг>); Работу этой функции мы достаточно подробно рассмотрели в лекции № 20.
UDF В практике программирования нередко встречаются ситуации, когда программисту недостаточно того набора функций, который предоставлен сервером InterBase. К счастью, InterBase дает возможность создавать и подключать к базе данных собственные функции, которые называются UDF (User Defined Functions – Функции, определенные пользователем). Прелесть этого механизма в том, что такие функции можно написать на любом языке программирования, включая Delphi, который позволяет делать библиотеки DLL (Dynamic Link Library – Динамически подключаемые библиотеки). Программист может реализовать в одном или нескольких DLL-файлах множество необходимых ему функций практически любой сложности, затем поместить этот файл (файлы) там, где установлен InterBase. Останется только подключить описанные функции к рабочей базе данных, после чего любую из этих функций можно использовать в запросах на любых клиентских ПК. Для демонстрации этой возможности как нельзя лучше подойдет функция Upper_Rus, описанная В.В.Фароновым в книге «Программирование баз данных в Delphi7. Учебный курс». Функция преобразует все символы строки в верхний регистр, но в отличие от стандартной Upper, она корректно сработает и с другими наборами символов и порядком сортировки.
184
Для начала нам нужно создать DLL-файл. Откройте Delphi. В нашем случае нам нужно будет создать отдельный DLL-файл, поэтому создавать его нужно как отдельный проект. Выберите команду меню Close All>, чтобы закрыть новое приложение, которое Delphi запускает автоматически. Затем выберем команду New -> Other>. Откроется окно, в котором на вкладке New нам нужно выбрать DLL Wizard:
Рис. 23.2. Выбор «мастера» DLL. При этом откроется окно модуля без всяких форм, которое содержит лишь следующий код (комментарии опущены): library Project1; uses SysUtils, Classes; {$R *.res} begin end.
Выберем команду Save All>, где нам предложат сохранить проект без всяких модулей. Создайте для проекта отдельную папку, а проект назовите Udf_Dll. Далее приводится весь код библиотеки Udf_Dll (без комментариев):
function Upper_Rus(InpString: PChar): PChar; cdecl; //Функция преобразует буквы входной строки в заглавные begin Result := PChar(ANSIUpperCase(String(InpString))); end; exports Upper_Rus; begin end.
Поскольку это DLL-проект, который не может работать самостоятельно, выбирать команду Run не нужно. Вместо этого нажмите , либо выберите команду Compile Udf_Dll>. В результате в указанной вами папке появится файл Udf_Dll.dll. В приведенном выше коде мы создали функцию Upper_Rus, которая имеет входной и выходной строковые параметры типа PChar (строковый тип Windows). Кроме того, для правильной работы с InterBase, эта экспортируемая функция задекларирована как cdecl (соглашение о передаче входных параметров). В теле функции входная строка преобразуется в верхний регистр функцией WinAPI ANSIUpperCase, благодаря чему ЛЮБОЙ набор символов (не обязательно русский) будет корректно преобразован в верхний регистр. В конце мы указываем, что описанная функция Upper_Rus предназначена для экспорта. Delphi можно закрыть. Полученный файл динамической библиотеки Udf_Dll.dll скопируйте в каталог UDF сервера InterBase (по умолчанию – C:\Program Files\Borland\InterBase\UDF). Если скопировать файл в другой каталог, InterBase его не найдет. Теперь эту функцию нужно зарегистрировать в базе данных First (сервер InterBase должен быть запущен, утилита IBConsole открыта, база данных First выделена и запущена утилита Interactive SQL). В окне запросов Interactive SQL укажите следующий запрос: DECLARE EXTERNAL FUNCTION UPPER_RUS CSTRING(256) RETURNS CSTRING(256) ENTRY_POINT 'Upper_Rus' MODULE_NAME 'UDF_DLL'; COMMIT; Здесь указан тип строк InterBase CSTRING, что соответствует типу PChar в Delphi, и установлено ограничение в 256 символов. Теперь, если вы посмотрите в дереве серверов IBConsole в разделе «External Functions» нашей БД First, вы увидите зарегистрированную функцию UPPER_RUS. В Interactive SQL мы можем ввести запрос, показывающий разницу между стандартной функцией UPPER и нашей функцией UPPER_RUS (может потребоваться перезагрузка IBConsole, или хотя бы закрытие (Disconnect) и открытие (Connect) базы данных First): SELECT UPPER(Nazvanie COLLATE WIN1251), UPPER_RUS(Nazvanie COLLATE WIN1251) FROM Tovar;
186
Рис. 23.3. Разница работы стандартной UPPER и UPPER_RUS Как видно из рисунка, там, где стандартная функция UPPER не смогла преобразовать текст в верхний регистр, функция UPPER_RUS с этой задачей справилась. Далее эту функцию можно использовать в пределах базы данных First на любом пользовательском ПК, который подключен к InterBase.
187
Лекция 24. Транзакции На протяжении всего курса нам не раз приходилось сталкиваться с этим термином. Тема «транзакций» в InterBase непростая, но очень необходимая для понимания. Эта лекция посвящена теории транзакций, и практике их применений в приложениях. В лекции № 14 мы упоминали, что транзакции – это пакет запросов, который последовательно производит изменения БД и либо принимается, если все изменения записи подтверждены, либо отвергается, если хоть один запрос завершился неуспешно. Запросы могут состоять из операторов SELECT / INSERT / UPDATE / DELETE, причем в контексте одной транзакции может быть как один такой запрос, так и множество запросов. Однако понятие «транзакция» гораздо глубже этого короткого определения. Транзакции запускаются на стороне сервера, причем стартуют они только по приказу клиентского приложения. Завершают работу они также по приказу клиентского приложения, причем при успешном завершении транзакция подтверждается, а при неудаче - отвергается. В триггерах или процедурах вызвать старт транзакции невозможно. Транзакция, по сути, это механизм, который позволяет совершать различные действия над базой данных, как единый логический блок, и который переводит базу данных из одного целостного состояний в другое. Или не переводит, если транзакция была отвергнута. Поясним эту суть на классическом примере перевода денег в банке с одного счета на другой. Допустим, наша БД работает без транзакций, и нам нужно произвести упомянутый перевод. Тут мы можем поступить двумя разными способами: 1. Вначале снимаем деньги с одного счета, затем добавляем их к другому счету. 2. Вначале добавляем деньги к другому счету, затем снимаем их с первого счета. Теперь предположим, что в середине этой операции произошел какой-то сбой: отключился сервер БД, например. В первом случае деньги будут потеряны – они ушли с одного счета, но не дошли до другого. Во втором случае деньги «размножатся» – появятся на втором счету, но при этом останутся и на первом. И в том, и в другом случае произойдет нарушение целостности БД – данные станут недостоверны. Однако все SQL-серверы баз данных работают с применением транзакций. Еще говорят, что все изменения базы данных происходят в контексте одной или нескольких транзакций. InterBase не исключение, более того, InterBase предоставляет гораздо более гибкие инструменты для управления транзакциями, чем многие другие SQL-серверы. Если произошел какой то сбой при переводе денег, то транзакция не получила подтверждения, а база данных осталась в прежнем состоянии – целостность и достоверность БД не нарушились. Более 20 лет назад исследователи Тео Хендер и Андреас Рютер опубликовали обзор, в котором описывали принципы поддержания целостности БД в много-клиентской среде. Эти принципы принято называть ACID (Atomicity, Consistency, Isolation, Durability – Атомарность, Согласованность, Изоляция и Устойчивость). Все транзакции действуют по этим принципам.
Атомарность (Atomicity) Атомарность подразумевает, что транзакция является единицей работы с базой данных. Внутри транзакции может происходить множество модификаций БД, однако транзакция действует по принципу «все или ничего». Когда транзакция подтверждается (Commit), то подтверждаются все изменения данных, сделанные в ее контексте. Когда она отвергается (откатывается, Rollback), то отвергаются и все изменения. В случае возникновения сбоя, система, восстанавливаясь, ликвидирует последствия транзакций, не успевших завершиться.
Согласованность (Consistency) Под согласованностью понимается, что целостность базы данных не нарушится, несмотря на то, была ли подтверждена транзакция, или отвергнута. В результате выполнения транзакции база данных переходит из одного целостного и согласованного состояния в другое. В случае если транзакция отвергнута, изменений БД не происходит. 188
На стороне сервера за согласованность отвечают ограничения CHECK, ограничения ссылочной целостности и триггеры. Программист, тем не менее, должен тщательно спроектировать механизмы бизнес-логики.
Изолированность (Isolation) В базе данных может выполняться множество транзакций одновременно. Бывает, что две, и более транзакции пытаются изменить одну и ту же запись. Чтобы гарантировать целостность данных, транзакции выполняются изолированно друг от друга. Можно сказать, что каждая транзакция работает со своей копией (версией) данных. Существует несколько степеней изолированности транзакций, о чем далее мы поговорим подробней.
Устойчивость (Durability) Если транзакция завершается успешно, изменения, сделанные в ее контексте, должны быть устойчивыми и сохранятся, независимо от ошибок в других транзакциях, ошибок оборудования или аварийного завершения работы сервера. Другими словами, результаты работы успешно завершенной транзакции физически сохраняются в базе данных.
Неявный и явный страт транзакций Все действия над базой данных, совершаемые в клиентском приложении, происходят внутри (в контексте) транзакции. В примерах предыдущих лекций мы соединяли клиентские приложения с базой данных, вообще не используя никаких транзакций. Однако это не значит, что их не было. Просто транзакции запускались неявно, автоматически. Причем с параметрами, созданными Delphi «по умолчанию». В серьезных приложениях БД это недопустимо, так как может привести к многочисленным конфликтам. Транзакцию можно стартовать и явно. Из стандартных механизмов доступа мы будем использовать, в основном, InterBase Express (IBX). В приложении должен присутствовать как минимум, один компонент IBTransaction. С помощью этого компонента можно явно указать параметры транзакции, управлять стартом, подтверждением или откатом транзакции. Делается это с помощью следующих методов компонента:
StartTransaction – Старт транзакции. Commit – Подтверждение транзакции с последующим ее закрытием. CommitRetaining – Подтверждение транзакции без ее закрытия. Rollback – Откат транзакции с последующим ее закрытием. RollbackRetaining – Откат транзакции без ее закрытия.
Впрочем, компоненты доступа к данным неявно производят запуск транзакции, поэтому StartTransaction обычно пропускают. А вот подтверждение или откат транзакции проверяют, как правило, в блоке try…except в клиентском приложении: try //какие то действия над данными... IBTransaction1.Commit; //подтверждаем Except //произошла ошибка ShowMessage('Невозможно выполнить операцию!'); IBTransaction1.Rollback; //делаем откат транзакции end; //try
В данном примере, если транзакция прошла успешно, то данные нормально сохраняются. В случае какой-то ошибки выводится сообщение, и транзакция откатывается. 189
Как транзакция работает В базе данных имеется специальная область, которая называется TIP (Transaction Inventory Page – Инвентарная Страница Транзакций). При старте транзакции, ей присваивается идентификатор (TID, Transaction ID) – инвентарный номер, который сохраняется в TIP. При этом у самой последней транзакции будет наибольший идентификатор. В TIP, помимо номера стартовавшей транзакции, сохраняется и ее состояние, которое может быть Active (В работе), Committed (Подтвержденная), Rolled Back (Отмененная, откат) и In Limbo (Неопределенная). Активной называется транзакция, которая в настоящий момент выполняется. Подтвержденной называется транзакция, которая успешно завершила свою работу, как правило, по команде Commit. Отмененной называют транзакцию, которая завершилась неудачно. При этом производится откат сделанных ей действий, как правило, командой RollBack. Неопределенной транзакцией (Limbo) называют транзакцию, которая работает одновременно с двумя или более базами данных. При завершении такой транзакции, InterBase совершает двухфазное подтверждение Commit, гарантируя, что изменения будут внесены либо во все БД, либо ни в одну. При этом подтверждения в базах данных будут даваться по очереди. Если в это время возникнет сбой системы, то может получиться, что в каких то БД изменения были сделаны, а в каких то нет. При этом транзакция переходит в неопределенное состояние, когда сервер не знает, следует ли подтвердить эту транзакцию, или откатить. Каждая транзакция, начиная работу, создает собственную версию записей таблиц, с которыми работает. Версия записи – это копия записи, которая создается, когда транзакция пытается ее изменить. Таким образом, каждая запись таблицы потенциально может иметь множество версий, при этом каждая транзакция работает с собственной версией этой записи. Если транзакция меняет данные, то она меняет их в собственной версии записи, а не в оригинале. Далее транзакция может либо подтвердиться, либо отмениться. Если транзакция подтвердилась, InterBase попытается пометить предыдущую оригинальную запись, как удаленную, а версию завершенной транзакции – как оригинал. Когда InterBase сохраняет изменения, то в новую запись помещается и идентификатор транзакции, которая внесла эти изменения (любая строка таблицы содержит идентификатор создавшей ее транзакции). Если транзакция завершилась неуспешно, оригинал записи так и остается оригиналом. Если же транзакция только читает запись, не пытаясь ее изменить, то для нее не создается собственной версии. Однако могут возникать и конфликты транзакций. Предположим, что стартовала транзакция Т1. Она создала версию записи, и поменяла ее данные. В это время стартовала конкурирующая транзакция Т2, и создала версию той же записи. Поскольку Т1 еще не завершилась, Т2 при старте не могла видеть изменения данных, сделанные Т1, а значит, создала свою версию из старого оригинала. Теперь Т1 завершает работу по Commit. Как должен поступить InterBase? Если он пометит версию записи Т1 как оригинал, а старую запись, как удаленную, то в версии Т2 окажутся ложные данные! Действия InterBase в этом случае будут зависеть от параметров этих транзакций, о чем ниже мы поговорим подробней. Если транзакция удаляет какую-то строку, то строка физически не удаляется из базы данных, а лишь помечается, как удаленная, сохраняя и номер удалившей ее транзакции. В случае если транзакция завершена неудачно, реального удаления строки не происходит, так как не было подтверждения. Таким образом, говорят, что InterBase имеет многоверсионную архитектуру (MGA – Multi Generation Architecture). Такая архитектура позволяет организовать работу с базой данных так, чтобы читающие пользователи не блокировали пишущих. Кроме того, при возникновении сбоев в системе, InterBase очень быстро восстанавливается, благодаря именно MGA. Кстати, InterBase является первым SQL-сервером, который поддерживает многоверсионную архитектуру. Наряду с преимуществами такого подхода, в базе данных со временем накапливается «мусор». Каждая транзакция, пытающаяся изменить данные, создает собственные версии строк, и если не позаботиться о своевременном удалении старых, уже никому не нужных версий, то база данных вскоре будет просто забита мусором. Но как удалять мусор? Можно ли удалить версию транзакции, которая завершилась, удачно или неудачно? Нет, если эта версия в настоящий момент используется другими транзакциями. Позже мы 190
поговорим об уровнях изолированности транзакций, пока лишь скажем, что некоторые транзакции могут видеть изменения, сделанные другими, еще не подтвержденными активными транзакциями. Предположим, что стартовала транзакция Т1. Эта транзакция создала версию записи и модифицировала ее. Позже стартовала транзакция Т2, которая настроена так, чтобы видеть все изменения данных, даже не подтвержденные. Она обратилась к той же записи, а поскольку она желает видеть последние изменения, ей предоставляют версию транзакции Т1. Затем Т1 завершила свою работу, но Т2 пока еще работает с ее версией записи, следовательно, эту версию удалять нельзя. В InterBase присутствует механизм удаления старых версий, который запускается новыми транзакциями. Новая транзакция, запрашивая запись, считывает все версии этой записи. При этом делается проверка на то, была ли транзакция, сделавшая эту версию, отменена (RollBack) или подтверждена (Commit). Если транзакция была отменена, значит, эта версия – мусор, который следует удалить. Если же имеется несколько версий, сделанных подтвержденными транзакциями, то актуальной считается версия с наибольшим идентификатором транзакций. Остальные версии считаются устаревшими и также подлежат удалению. Таким образом, молодые транзакции прибирают базу данных от мусора, оставленного более старыми транзакциями. Но чистят они не все старые версии подряд, а только версии той записи (или записей), к которой обращаются сами. Поскольку на сервере одновременно может выполняться множество транзакций, имеется терминология определения этих транзакций.
Активная транзакция – транзакция, которая стартовала, но еще не завершена. Заинтересованная транзакция – это транзакция, конкурирующая с текущей транзакцией. Старейшая активная транзакция – это такая активная транзакция, которая стартовала раньше других. Или иначе, это активная транзакция с наименьшим идентификатором. Старейшая заинтересованная транзакция – это такая заинтересованная транзакция, которая стартовала раньше других. Или иначе, это заинтересованная транзакция с наименьшим идентификатором.
В данном контексте, сборкой мусора занимается старейшая активная транзакция. Так как версий записи, сделанных более молодыми транзакциями, она видеть не может, то убирает мусор, оставленный еще более старыми транзакциями. Когда эта транзакция заканчивает свою работу, то статус «старейшей активной» переходит к другой транзакции. Таким образом, транзакции передают друг другу обязанность по сборке мусора.
Уровни изолированности транзакций Как уже упоминалось выше, каждая транзакция работает изолированно от других транзакций. Уровень изолированности транзакции определяет, какие изменения, сделанные другими транзакциями, увидит данная транзакция. InterBase имеет три уровня изолированности: READ COMMITTED – Читать подтвержденные данные. Этот уровень изоляции позволяет транзакции видеть изменения, сделанные другими, транзакциями, если эти изменения были подтверждены. К примеру, стартовали транзакции Т1 и Т2. Обе они изменили запись. Однако Т1 не увидит изменений, сделанных Т2, пока та не подтвердит сделанных ей изменений (по Commit). Этот уровень изоляции считается самым низким, открытым, позволяя транзакции видеть «самые свежие» данные. SNAPSHOT – Моментальный снимок данных. Это средний уровень изолированности. Он позволяет транзакции видеть только те данные, которые существовали в момент старта транзакции. Если другие заинтересованные транзакции изменили запись и подтвердили изменения, то SNAPSHOT – транзакция все равно не увидит этих изменений. Однако она не блокирует данные, с которыми работает. SNAPSHOT TABLE STABILITY – еще более закрытый уровень изоляции. Транзакции такого уровня не только делают снимок данных, они также блокируют на запись те данные, с которыми 191
работают. Пока такая транзакция не подтверждена, остальные транзакции гарантированно не смогут изменить эти данные. Кроме того, транзакция SNAPSHOT TABLE STABILITY просто не сможет получить доступ к таблице, если в настоящий момент другая транзакция изменяет в ней данные.
Параметры транзакций Мы имеем возможность гибко управлять параметрами транзакций, добиваясь наилучших результатов. Параметры, установленные по умолчанию, далеко не всегда являются самыми удачными. Рассмотрим эти параметры. Таблица 24.1. Возможные параметры транзакций Параметр Описание Разрешается чтение данных. Read Разрешается запись данных. Write При возникновении конфликта с другой транзакцией, текущая транзакция Wait ожидает определенное время (по умолчанию, 10 сек.), прежде чем попытается решить этот конфликт. При возникновении конфликта с другой транзакцией сразу генерируется NoWait ошибка. Read_committed позволяет читать подтвержденные изменения данных, Read_committed сделанные другими транзакциями. Дополнительный параметр Rec_Version, Rec_Version кроме того, позволяет читать и неподтвержденные изменения. Read_committed позволяет читать подтвержденные изменения данных, Read_committed сделанные другими транзакциями. Дополнительный параметр No_Rec_Version No_Rec_Vesion используется по умолчанию, и не позволяет читать неподтвержденные изменения. Создает уровень изоляции SNAPSHOT. Concurrency Создает уровень изоляции SNAPSHOT TABLE STABILITY. Consistency Программист может задать транзакции несколько параметров одновременно. Например, если нужно, чтобы транзакция могла и читать, и изменять данные, можно задать параметры Read Write Какие параметры устанавливать, решается для каждой конкретной задачи. Однако можно сделать такие рекомендации: Если транзакция используется только для чтения, например, для вывода данных в сетку запросом SELECT, то она должна иметь параметры, позволяющие только читать данные, что значительно ускорит работу транзакции. Причем данные должны быть самыми «свежими», даже из неподтвержденных транзакций. И, кроме того, транзакция не должна ожидать разрешения возможного конфликта. Набор параметров тут может быть следующим: Read Read_Committed Rec_Version NoWait Если транзакция используется для построения отчетов, то она также должна только читать данные, но только подтвержденные. При этом желательно сделать снимок данных, чтобы не зависеть от возможных изменений. Для подобной транзакции параметры удобней всего делать такими:
192
Read Concurrency NoWait И, наконец, если транзакция предназначена для изменения данных в БД, то ей лучше задать более жесткие параметры, например: Write Concurrency NoWait
Практика применения транзакций Для закрепления материала создадим приложение, работающее с таблицей Tovar нашей базы данных. Приложение будет использовать две транзакции с различными параметрами – одну для чтения данных, другую для записи. Прежде всего, убедитесь, что сервер InterBase запущен. Далее, создайте в Delphi новый проект. На главную форму поместите DBNavigator, сетку DBGrid и простую кнопку:
Рис. 24.1. Главная форма приложения Пока мы не подключим сетку и навигатор к таблице, данные не будут отображаться. Сохраните проект в отдельную папку под именем IBXTrans. Поскольку и навигатор, и сетка нам нужны только для чтения данных, выделите навигатор, откройте сложное свойство VisibleButtons и сделайте невидимыми все кнопки, кроме nbFirst, nbPrior, nbNext, nbLast и nbRefresh. Отрегулируйте ширину навигатора, чтобы кнопки стали квадратными, как на рисунке выше. А у сетки свойство ReadOnly переведите в True. С главной формой закончили. Командой File -> New -> Data Module создайте новый модуль данных. Свойство Name окна переименуйте в fDM, а сам модуль сохраните под именем DM. Перейдите в главное окно, и командой File -> Use Unit подключите к нему модуль DM. В модуль поместите следующие компоненты из вкладки InterBase: IBDatabase, IBTable, IBQuery, два компонента IBTransaction и один DataSource из вкладки Data Access. Займемся настройкой этих компонентов. Выделите компонент IBDatabase. Его свойство Name переименуйте в IBDB, чтобы было короче. Откройте свойство Params (откроется окно редактора параметров). Там укажите следующие параметры: 193
user_name=sysdba password=masterkey lc_ctype=win1251 Обратите внимание, все параметры можно писать маленькими буквами, пробелы перед- и после знака «=» недопустимы. Далее, в свойство DatabaseName найдите и поместите файл нашей базы данных First.gdb. Чтобы при загрузке программы не запрашивался логин и пароль, свойство LoginPrompt переведите в False. После этого свойство Connected переведите в True. Если вы все сделали правильно, база откроется, ошибок не возникнет. Теперь займемся компонентами транзакций. Выделите первый IBTransaction, его свойство Name переименуйте в IBTrans1. Этот компонент будет создавать транзакции для считывания данных в сетку и навигатор на главной форме. Откройте свойство Params (откроется окно редактора параметров), и впишите следующие параметры: read read_committed rec_version В свойстве DefaultDatabase выберите IBDB, а у IBDB в свойстве DefaultTransaction в свою очередь, выберите IBTrans1. После чего переведите свойство Active транзакции IBTrans1 в True. Вторую транзакцию назовите IBTrans2. Она будет нужна только для записи новых данных в таблицу. Поэтому в свойстве Params мы введем следующие параметры: write concurrency nowait В свойстве DefaultDatabase этой транзакции также выберите IBDB, а вот свойство Active оставьте False – транзакция будет автоматически запускаться, когда мы попытаемся изменить данные в таблице. Перейдем к компоненту IBTable. Свойство Name переименуйте в TTovar, в свойстве Database выберите IBDB, в свойстве Transaction выберите читающую транзакцию IBTrans1. Теперь в свойстве TableName найдите и откройте таблицу TOVAR, после чего свойство Active переведите в True. Таблица открыта. Выделите DataSource и переименуйте Name в DSTovar. В свойстве DataSet выберите таблицу TTovar. Теперь можно перейти на главную форму, выделить сетку и навигатор, и в их свойстве DataSource выбрать fDM.DSTovar. Данные должны отобразиться в сетке, а некоторые кнопки навигатора станут недоступны. Вернемся в модуль данных. Выделите запрос IBQuery. Свойство Name переименуйте в Q1. В свойстве Database выберите IBDB, а в свойстве Transaction – IBTrans2. Более ничего делать не нужно, запросы будем строить программно. Для добавления новых записей и редактирования существующих создадим еще одну форму командой File -> New -> Form. Окно редактора будет очень простым:
Рис. 24.2. Окно редактора Форму назовите fEditor, сохраните в папку проекта, дав модулю имя Editor (так как последним мы открывали файл First.gdb, то при попытке сохранения окна Delphi по умолчанию предложит папку с БД. 194
Измените ее на папку с проектом.). Командой File -> Use Unit подключите к редактору модуль DM. На форме расположите два компонента Label, два простых Edit и две кнопки, как на рисунке 24.2. Не помешает в свойстве BorderStyle формы выбрать bsDialog, а в свойстве Position – poMainFormCenter. И не забудьте очистить текст у компонентов Edit, а у кнопок и компонентов Label изменить свойство Caption в соответствии с рисунком. Вернемся к главной форме. Командой File -> Use Unit подключите к главной форме модуль Editor. Сгенерируйте событие нажатия на кнопку «Добавить запись». В полученной процедуре впишите код закрытия Q1 (на случай, если ранее запрос был активен), и вызова редактора: {Добавить запись} procedure TfMain.Button1Click(Sender: TObject); begin fDM.Q1.Close; fEditor.ShowModal; end;
Таким образом, мы будем добавлять новую запись. А для редактирования существующей записи, выделите сетку DBGrid и сгенерируйте для нее событие OnDblClick. То есть, открывать редактор мы будем по двойному щелчку на записи. При этом в таблице TTovar станет текущей нужная нам запись. Прежде, чем вызывать редактор, нам нужно в Q1 получить нужную запись. Для этого мы создадим запрос, откроем Q1 и только потом вызовем редактор. Итак, код события OnDblClick следующий: {Двойной щелчок по сетке - редактируем запись} procedure TfMain.DBGrid1DblClick(Sender: TObject); begin fDM.Q1.SQL.Clear; fDM.Q1.SQL.Add('select * from Tovar where ID = '+ IntToStr(fDM.TTovar.FieldByName('ID').AsInteger)); fDM.Q1.Open; fEditor.ShowModal; end;
Как видно из кода, в запросе Q1 мы генерируем запрос вроде такого: SELECT * FROM TOVAR WHERE ID = 3 Разумеется, номер ID будет зависеть от того, по какой записи мы щелкнули. При открытии Q1, в нее попадет лишь одна интересующая нас запись. Больше ничего в главной форме делать не нужно. Переходим к окну редактора. Тут у нас может быть два варианта: либо мы добавляем новую запись (Q1 закрыта), либо редактируем существующую (Q1 открыта и содержит нужную запись). В первом случае нам нужно будет очистить компоненты Edit, если там был текст, а во втором наоборот, вписать в них значения полей Nazvanie и Stoimost. В раздел глобальных переменных добавим переменную i, необходимую для хранения ID записи: var fEditor: TfEditor; i: Integer; //идетификатор записи
Затем выделим форму редактора, и сгенерируем для нее событие OnShow. Код события следующий:
195
{При показе редактора} procedure TfEditor.FormShow(Sender: TObject); begin if fDM.Q1.Active then i := fDM.Q1.Fields[0].AsInteger else i := 0; //очищаем или заполняем эдиты: if i = 0 then begin Edit1.Text := ''; Edit2.Text := ''; end //if else begin Edit1.Text := fDM.Q1.Fields[1].AsString; Edit2.Text := fDM.Q1.Fields[2].AsString; end; //esle end;
Как видно из приведенного кода, как только форма станет видимой, мы проверяем – активна ли Q1. Если да, то в переменную i прописываем идентификатор текущей записи, иначе i делаем равным нулю. Это нам нужно для того, чтобы в дальнейшем знать, с какой записью работать. Ведь в Q1 будут помещаться другие запросы, и она уже не будет содержать нужную запись. Далее, если i = 0 (новая запись), мы очищаем компоненты Edit, иначе считываем в них значения второго и третьего поля запроса (Nazvanie и Stoimost). Пойдем дальше. Для кнопки «Отменить» введем код простого закрытия формы: //закрываем форму: Close;
Так как пользователь может закрыть окно не только кнопкой «Отменить», но и клавишами или просто нажав на крестик в правом верхнем углу окна, то проверку на активность транзакции нужно делать в событии формы OnClose. Сгенерируем для формы редактора событие OnClose, в котором будем проверять, не в работе ли транзакция IBTrans2, и если да, то сделаем откат транзакции: if fDM.IBTrans2.InTransaction then fDM.IBTrans2.RollbackRetaining;
Прежде, чем перейдем к кнопке «Подтвердить», подумаем вот о чем: для InterBase в вещественных числах между целой частью и дробной должен быть разделитель точка, а пользователь может ввести запятую. Кроме того, в существующих записях разделитель также отображается как запятая, и если мы при показе формы автоматически заполним Edit2, то там тоже будет запятая. Значит, для Edit2 сгенерируем событие OnChange, где устроим простую проверку: {Изменение данных в Edit2} procedure TfEditor.Edit2Change(Sender: TObject); var ind: Byte; s: String; begin s:= Edit2.Text; for ind:= 1 to Length(s) do if s[ind] = ',' then s[ind]:= '.'; Edit2.Text := s; end;
Здесь, если в тексте будет обнаружена запятая, то она автоматически поменяется на точку. Однако приложение демонстрационное, поэтому мы не делаем проверку на то, что пользователь может ввести не только цифру, точку или запятую, но и какой-нибудь другой символ, например, пробел или букву. Вы можете сделать более детальную проверку, если желаете. 196
Нам осталось лишь сгенерировать нажатие на кнопку «Подтвердить». И здесь у нас будет два варианта: либо мы добавляем новую запись (i = 0), и тогда мы используем оператор INSERT, либо мы изменяем существующую. В последнем случае будет применяться оператор UPDATE. Код нажатия на кнопку следующий: {Подтвердить} procedure TfEditor.Button1Click(Sender: TObject); begin //очищаем SQL-запрос: fDM.Q1.SQL.Clear; //создаем новый запрос, в зависимости от показателя i: if i = 0 then begin //если i = 0, то это новая запись fDM.Q1.SQL.Add('insert into Tovar(Nazvanie, Stoimost)'); fDM.Q1.SQL.Add('values('+QuotedStr(Edit1.Text)+', '+Edit2.Text+')'); end //if else begin //модифицируем существующую запись fDM.Q1.SQL.Add('update Tovar set Nazvanie='+QuotedStr(Edit1.Text)+ ', Stoimost='+Edit2.Text+' where ID ='+IntToStr(i)); end; //else try //производим изменения: fDM.Q1.ExecSQL; //подтверждаем транзакцию: fDM.IBTrans2.CommitRetaining; except ShowMessage('Изменения данных не прошли!'); fDM.IBTrans2.RollbackRetaining; end; //try //обновляем НД TTovar: fDM.TTovar.Refresh; //закрываем форму: Close; end;
В случае добавления записи формируется запрос типа INSERT INTO TOVAR(Nazvanie, Stoimost) VALUES(‘Товар’, 10.00) В случае редактирования существующей записи формируется другой запрос: UPDATE TOVAR SET Nazvanie = ’Товар’, Stoimost = 10.00 WHERE ID = 3 Разумеется, название товара, его стоимость и идентификатор ID будут зависеть от того, что именно введет пользователь в компоненты Edit, и какую строку в таблице выберет. Обратите внимание, что у компонента IBTrans2 вместо методов Commit (подтверждение) и Rollback (откат) используются методы CommitRetaining и RollbackRetaining, которые также подтверждают или откатывают транзакцию, но не закрывают ее при этом, оставляя активной. В многопользовательской среде, где идет активная работа с базой данных, транзакции лучше закрывать, чтобы в базе данных не «висело» множество активных транзакций. Сохраните проект, скомпилируйте и попробуйте вводить новые значения и редактировать существующие. Проект использует две транзакции: для чтения с «мягкими» параметрами, и для записи с более «жесткими». В заключение заметим, что в проектах, которые интенсивно работают с базой данных, совершенно недопустимым будет использование только одной транзакции, это может привести к многочисленным конфликтам. Также недопустимым будет использование отдельной транзакции для каждого набора данных. Находите компромисс: разделяйте наборы данных на читающие, пишущие и НД для отчетов. И для каждой группы используйте отдельный компонент IBTransaction, с соответствующими задаче параметрами. 197
Лекция 25. Администрирование InterBase: безопасность БД. Под администрированием InterBase подразумеваются манипуляции с пользователями и ролями (добавление, удаление, предоставление и отъем прав и проч.), резервное копирование базы данных, ее восстановление и, возможно, ремонт. Тут следует сделать одно замечание: InterBase, входящий в дистрибутив Delphi, предназначен для разработки программ, а не для реальной серверной работы, поэтому он имеет ряд ограничений. Так, InterBase версии WI-V6.5.0.28, входящий в дистрибутив Delphi 7 использовался на четырех различных версиях Windows и дал одинаковый результат: создавать новых пользователей и роли можно, но войти в базу данных под другим, не SYSDBA пользователем, не получается. Поэтому если вы желаете не только изучить приемы администрирования сервера, но и опробовать все его возможности на практике, то рекомендую установить полную версию InterBase, желательно более свежую. Бесплатные пробные версии на 90 дней можно скачать отсюда: http://www.ibase.ru/interbase.htm Впрочем, помимо приведенного выше ограничения, весь материал данного курса можно изучить и опробовать но том InterBase, который вы уже установили вместе с Delphi. Администрирование можно осуществлять разными способами: с помощью утилиты IBConsole или подобных утилит сторонних разработчиков. Но наиболее надежным и гибким способом является администрирование встроенными в InterBase утилитами командной строки, которые все равно неявно вызываются IBConsole и другими графическими утилитами, а также компонентами администрирования Delphi. Примечание: платный сервер InterBase имеет бесплатных клонов: Firebird и Yaffil. Все, что мы изучали и продолжим изучать об InterBase, почти без изменений можно применить и к этим клонам, таким образом, вы изучаете не один, а сразу три сервера БД! Бесплатный – не значит плохой, эти клоны ничем не уступают своему предку, а в некоторых случаях даже превосходят его. Например, если в InterBase самый большой размер страницы 8192, то в Firebird можно установить страницу размером 16384 байт. Единственный минус: клоны InterBase не имеют графической утилиты вроде IBConsole. Но эта проблема легко решается: существует множество подобных утилит от сторонних разработчиков, гораздо более мощных и удобных, чем IBConsole. Особо можно порекомендовать графическую утилиту IBExpert. Эта утилита бесплатна для стран бывшего СССР, точнее, для русскоязычных пользователей – утилита определяет, что в операционной системе установлена поддержка кириллицы, и в этом случае работает без ограничений (спасибо разработчикам!). Кроме того, на сайте производителя, помимо последней версии утилиты, также можно скачать и довольно качественный русификатор. IBExpert поддерживает сервера InterBase, Firebird и Yaffil различных версий. Подробнее о клонах, утилитах, а также о пакетах компонентов для них можно узнать на сайте официального представителя этих СУБД в России http://www.ibase.ru/
Утилиты командной строки Сейчас уже найдется не так много пользователей, которым доводилось работать с командной строкой. Большей частью пользователи привыкли к удобным графическим утилитам, с которыми можно работать при помощи одной только мыши. Утилиты командной строки представляют собой консольные приложения, в виде черного экрана с белым текстом на нем. Такие утилиты вызываются вместе с параметрами, и находятся в папке BIN сервера InterBase, по умолчанию: C:\PROGRAM FILES\BORLAND\INTERBASE\BIN Вызвать такую утилиту можно с помощью окна CMD (для WinNT, 2000, XP или выше) или COMMAND (для Win95/98/ME). Так, чтобы узнать версию сервера, нажмите «Пуск -> Выполнить», а в окне введите cmd или command, в зависимости от вашей ОС. Кстати, в WinXP тоже есть команда command, однако она работает в режиме MS-DOS и не поддерживает длинных имен файлов и папок. Поэтому в XP (или выше) нужно использовать cmd. Откроется черное окно, в конце последней строки будет мигать курсор, это и есть командная строка. Введите следующие команды (подразумевается адрес InterBase по умолчанию), после каждой из них нажимая <Enter>: 198
c: cd program files\borland\interbase\bin gpre –z Сразу оговоримся, что текст в окне придется набирать вручную. Если скопировать текст, а затем выбрать в CMD контекстную команду «вставить», нет гарантии, что все символы скопируются правильно. В этом случае при выполнении команд вы получите ошибку. Зато в CMD имеется возможность повторить последнюю команду, не набирая ее. Достаточно нажать и удерживать клавишу «стрелка вправо», чтобы заново ввести текст последней команды. Это может быть полезным, когда приходится подряд набирать похожие команды: можно повторить, а затем отредактировать текст предыдущей команды. В результате выполнения примера, вы получите следующее окно:
Рис. 25.1. Утилита командной строки gpre.exe Для тех, кто впервые использовал подобные команды, разберем их подробней. Команда c: делает текущим диск C: (на случай, если ранее был текущим другой диск). Команда cd (Change Directories) меняет текущий каталог на указанный, то есть, командой cd c:\program files\borland\interbase\bin мы переходим в папку BIN сервера InterBase. Если у вас InterBase установлен по другому адресу, в этой команде следует сделать изменения. В конце мы даем команду gpre –z которая вызывает утилиту gpre.exe с параметром -z. Эта утилита является препроцессором языков C/C++ и предназначена для разработчиков, напрямую работающих с InterBase API. Не самая используемая утилита, но здесь для нас интересен параметр –z, который выводит информацию о версии InterBase и самой утилиты (этот же параметр есть и у остальных утилит, с которыми нам предстоит познакомиться). Как видите, расширение *.exe при загрузке утилиты можно не указывать, а регистр букв не имеет значения. Закрыть данное окно можно командой exit. В дальнейшем для краткости изложения предполагается, что текущей папкой в этом окне является папка BIN сервера InterBase, где и хранятся все остальные утилиты.
199
Пользователи В лекции № 14 мы уже рассматривали, как можно зарегистрировать нового пользователя с помощью утилиты IBConsole. Для регистрации пользователя, его удаления или смены пароля данный способ наиболее удобен, однако для порядка рассмотрим, как создать нового пользователя при помощи утилиты командной строки. В InterBase для этого существует утилита gsec.exe, подробные параметры которой можно вывести командой gsec -help Чтобы утилита позволила работать с данными пользователей, нужно указать, что мы имеем на это право. То есть, что мы – пользователь SYSDBA с паролем masterkey (если вы его не изменили). Так, для вывода информации об установленных пользователях имеется опция display, которая вызывается следующей командой (сервер InterBase должен быть запущен): gsec –user sysdba –password masterkey -display Добавить нового пользователя можно командой add: gsec –user sysdba –password masterkey –add MISHA –pw qaz мы добавили пользователя «MISHA» с паролем «qaz». Изменить пароль этого пользователя на «qaz123» можно командой modify: gsec –user sysdba –password masterkey –modify MISHA –pw qaz123 Удалить пользователя MISHA можно командой delete: gsec –user sysdba –password masterkey –delete MISHA Утилита gsec позволяет работать и в интерактивном режиме. Чтобы войти в этот режим, нужно ввести команду: gsec –user sysdba –password masterkey После чего строка с курсором примет такой вид: GSEC>_ Это означает, что мы находимся внутри утилиты. Далее имя и пароль администратора SYSDBA можно не указывать. Например, чтобы снова добавить пользователя MISHA, дадим команду: add MISHA –pw qaz123 а для вывода списка пользователей укажем команду display Выйти из утилиты GSEC можно командой quit. Подобным же образом происходит работа и с другими утилитами командной строки. Все пользователи и их параметры хранятся в специальной системной базе данных InterBase, которая устанавливается вместе с сервером: C:\PROGRAM FILES\BORLAND\INTERBASE\isc4.gdb 200
При переустановке сервера этот файл по умолчанию не удаляется, а переходит «в наследство» следующей версии InterBase, поэтому пользователи и их пароли остаются неизменными. Таблица, содержащая информацию о пользователях, в этой БД называется USERS. На случай, если вы удалите нужного пользователя, или измените его пароль, который потом забудете, рекомендуется делать резервные копии этой БД. Есть и плохая новость. Поскольку все пользователи и их пароли хранятся в БД isc4.gdb, то имеется вероятность того, что рабочая база данных предприятия может быть скопирована, перенесена на другой ПК с установленным InterBase, после чего может быть открыта пользователем SYSDBA. Даже если на вашем сервере пароль SYSDBA изменен, на другом ПК этот пароль будет другим, так что базу данных можно будет открыть. Политика InterBase подразумевает, что серверный ПК должен быть: 1) Недоступен посторонним лицам. 2) Не должен иметь открытых для общего доступа ресурсов (дисков или папок с базами данных). При соблюдении этих правил безопасность данных будет на высоком уровне.
Права Добавление нового пользователя еще не означает, что он сможет работать с БД. Чтобы он мог с ней работать, ему необходимо также выделить какие то права. Права в InterBase раздаются пользователям и ролям (и даже хранимым процедурам и триггерам) на какие либо действия с отдельными объектами БД. Под объектами подразумеваются таблицы и их столбцы, просмотры и хранимые процедуры. Права могут быть такими: Для таблиц и их полей команды SELECT, INSERT, UPDATE, DELETE и REFERENCES (право на создание ограничений внешнего ключа FOREIGN KEY для данной таблицы. Если таблица содержит внешний ключ, а пользователь должен иметь право на изменение таблицы, то это право ему необходимо также предоставить). Для просмотров VIEW команды SELECT, INSERT, UPDATE и DELETE. Для хранимых процедур команда EXECUTE. Таким образом, пользователю можно предоставить, например, права на изменение каких то отдельных столбцов; на другие столбцы только просмотр; возможность добавлять новую запись, но без возможности ее удалить. Могут быть пользователи с правами только на просмотр определенных таблиц или, наоборот, с полными правами на все объекты БД. Раздаются права командой GRANT. Предоставлять пользователям права удобней утилитой IBConsole. Загрузите IBConsole, войдите в локальный сервер и базу данных First. Дадим пользователю PUPKIN (которого мы создали в лекции №14) права на просмотр таблицы Tovar. Откройте окно Interactive SQL и введите следующую команду: GRANT SELECT ON TOVAR TO PUPKIN; После нажатия кнопки Execute Query, пользователь PUPKIN получит права на просмотр этой таблицы. Убедиться в этом можно, выделив в IBConsole таблицу Tovar, щелкнув по ней правой кнопкой и выбрав команду Properties. Перейдем на вкладку Permission:
201
Рис. 25.2. Права на таблицу Tovar Как видим, пользователь SYSDBA имеет на таблицу все права, тогда как пользователь PUPKIN – только на чтение. И еще мы можем сделать вывод, что если пользователи и их пароли хранятся в системной БД InterBase, то их права на объекты рабочей БД хранятся в этой самой БД. Значок открытой руки в правах пользователя SYSDBA означает, что эти права могут передаваться другим пользователям. Дадим пользователю PUPKIN права на модификацию записей, с возможностью передачи этого права другим пользователям. Для этого существует дополнительный оператор WITH GRANT OPTION. Снова откроем Interactive SQL и введем следующую команду: GRANT UPDATE ON TOVAR TO PUPKIN WITH GRANT OPTION; Если мы снова откроем вкладку Permission таблицы Tovar, то убедимся, что PUPKIN теперь имеет право не только изменять записи таблицы, но и передавать это право другим пользователям. Причем каждый пользователь может передавать лишь те права или их часть, которые есть у него самого. Чтобы предоставить пользователю права на отдельные столбцы в таблице, их нужно перечислить в скобках: GRANT UPDATE (FNAME, FTYPE, FCENA) ON FOOD TO PUPKIN; Предоставление прав для списка столбцов может быть только на команды UPDATE и PREFERENCES, для других команд необходимо указывать всю таблицу. Если пользователю требуется предоставить сразу несколько различных прав на таблицу, эти права можно указать через запятую: GRANT SELECT, INSERT, UPDATE ON PRIM_1 TO PUPKIN; Через запятую можно указать и список пользователей: GRANT SELECT, INSERT ON DAYS TO PUPKIN, MISHA; Чтобы предоставить пользователю ВСЕ права на объект, можно использовать оператор ALL: GRANT ALL ON SKLAD TO PUPKIN; А чтобы предоставить какое то право ВСЕМ зарегистрированным пользователям, можно воспользоваться «виртуальным» пользователем PUBLIC: GRANT ALL ON SDELKI TO PUBLIC; 202
Права можно не только дать, но и отнять. Делается это командой REVOKE, которая является копией GRANT, только с обратным действием (вместо TO <пользователь> синтаксис использует FROM <пользователь>). Например, снять все права у всех пользователей на таблицу Sdelki можно командой: REVOKE ALL ON SDELKI FROM PUBLIC; Пользователь SYSDBA при этом прав на таблицу не потеряет.
Роли Когда в организации работает множество пользователей, они, как правило, разбиваются на группы. Например, несколько бухгалтеров с одинаковыми правами. Чтобы не терять время на создание одинаковых прав для нескольких пользователей, в InterBase имеется механизм ролей. При пользовании этим механизмом, существует четыре последовательности действий: 1. Создать роль. 2. Присвоить этой роли необходимые права. 3. Назначить нужным пользователям эту роль. 4. При соединении с БД указать не только имя пользователя и пароль, но и его роль. Выполним эту последовательность в окне Interactive SQL: CREATE ROLE BUH; /*Создаем роль*/ GRANT ALL ON SDELKI TO BUH; /*присваиваем ей права*/ GRANT BUH TO PUPKIN; /*Предоставляем роль пользователю*/ Теперь, если мы выделим в дереве серверов в нашей БД раздел Roles, то увидим новую роль BUH:
Рис. 25.3. Создание роли А если щелкнуть по BUH в правой части окна правой копкой и выбрать команду Properties, мы увидим, что на эту роль имеет право пользователь PUPKIN. Чтобы подключить пользователя PUPKIN к БД в контексте роли, а не как просто пользователя, в параметрах компонента IBDatabase нужно будет указать следующие параметры: 203
user_name=pupkin password=qwerty sql_role_name=buh lc_ctype=WIN1251 Это сработает только в том случае, если данному пользователю действительно присвоена эта роль. Таким образом, в клиентском приложении можно сделать гибкую систему подключений пользователя: как простого пользователя с одними правами, и как пользователя в контексте роли с другими правами. Если же мы подключаемся к БД другим образом, например, через SQL-скрипт, то параметры подключения могут выглядеть иначе: CONNECT C:\DATABASES\FIRST.GDB USER PUPKIN ROLE BUH PASSWORD qwerty; Удаляется роль командой DROP, при этом все права роли на объекты утрачиваются: DROP ROLE BUH;
204
Лекция 26. Администрирование InterBase: обслуживание БД. Резервное копирование базы данных (Backup) В любой нормальной организации резервное копирование БД (backup) является безусловной обязанностью администратора. Ведь база данных может разрушиться по самым разным причинам: сбой питания на сервере, вирус, злоумышленник, ошибки операционной системы, и т.п. Кроме того, сами пользователи могут случайно ввести неправильные данные, которые потом сложно будет исправить. Конечно, такие неприятности не случаются ежедневно, база данных может нормально функционировать годами, тем не менее, вероятность порчи БД не исключена. Поэтому главной обязанностью администратора БД считается регулярное резервное копирование данных. Оптимальным вариантом было бы ежедневное копирование, в крайнем случае, еженедельное. Здесь имейте в виду, что если вдруг БД придется восстанавливать, то чем старее ваша резервная копия, тем больше работы персоналу организации придется сделать для восстановления утерянных данных! Еще один важный момент: копии базы данных должны храниться не просто на другом разделе жесткого диска, а вообще на другом жестком диске, ведь диск с рабочей БД тоже может выйти из строя. Если копии будут храниться на нем, то они также будут потеряны. Лучше всего хранить резервные копии на другом ПК, и (или) записывать их на CD-носители. К примеру, во многих банках существует правило: делать три резервных копии и хранить их в разных местах, даже в разных зданиях (на случай пожара). Так что отнеситесь к этой лекции серьезно. Проще всего было бы скопировать файл базы данных *.gdb на другое место, однако так делать не рекомендуется по разным причинам. Пользователи могут вносить данные в базу, следовательно, файл БД будет постоянно изменяться. Чтобы просто скопировать файл, придется отключить работу сервера, то есть, запретить пользователям работу с базой данных, что обычно не приветствуется персоналом организации. Кроме того, в лекции №18 мы говорили, что интенсивная работа с базой данных может привести к тому, что индексы становятся разбалансированными, значения в них располагаются, как попало, и использование индекса не ускоряет, а даже замедляет поиск данных. А в лекции №24 мы говорили о транзакциях, и знаем, что многие версии старых записей (мусор) обычно присутствуют в БД, возможно, есть и «повисшие» транзакции. Все эти проблемы не решаются простым копированием файла *.gdb, и остаются в такой копии. Для создания нормальной резервной копии БД, в InterBase имеются собственные механизмы. Использование встроенных механизмов InterBase имеет следующие преимущества: InterBase позволяет осуществлять резервное копирование БД параллельно с работой других пользователей. В начале копирования InterBase делает «мгновенный снимок» базы данных, с которым и работает утилита копирования. Все изменения в БД, сделанные после начала копирования, не попадут в резервную копию. Во время резервного копирования InterBase считывает каждую запись из таблиц, параллельно занимаясь «сборкой мусора», поэтому в резервной копии не останется устаревших версий записей. Оставшиеся данные переупаковываются, то есть, резервная копия не будет содержать тех «дырок», что были в базе данных. Можно сказать, что данные дефрагментируются. Индексы пересчитываются, что приводит к оптимизации работы базы данных. Созданная резервная копия может быть использована для миграции базы на другие серверы (InterBase новых версий, Firebird или Yaffil), а также при восстановлении позволяет исправить некоторые параметры БД, например, размер страниц. Таким образом, резервное копирование нужно осуществлять исключительно средствами InterBase. Резервную копию можно сделать разными способами: с помощью утилиты командной строки, с помощью IBConsole, а также программно (рассмотрим в следующей лекции). Испробуем вначале самый простой вариант – утилиту IBConsole.
Backup с помощью IBConsole Убедитесь, что сервер InterBase запущен, и загрузите IBConsole. Вначале откройте раздел локального сервера, в котором хранится наша БД First.gdb. То есть, вы должны войти в локальный 205
сервер, указав пароль пользователя SYSDBA. Не имеет значения, открыта ли сама база First, копирование можно проводить как при работающей, так и при закрытой БД. Выберите команду меню «Database -> Maintenance -> Backup/Restore -> Backup». В разделе Database в поле Alias выберите псевдоним нашей БД «first». В разделе Backup File(s) в поле Server укажите «Local Server», а в поле Alias также укажите «first». В нижнем разделе нужно вписать адрес и имя создаваемой резервной копии (в качестве имени я использовал текущую дату, у вас имя может быть другим), как на рисунке ниже. Когда вы в следующий раз будете делать резервное копирование, то адрес и имя резервной копии заполнятся автоматически, как только вы выберите алиас «first» в разделе Backup File(s). В этом случае вы сможете оставить имя файла и адрес без изменений, или же изменить их. Как видите, backup-файлам традиционно дают расширение *.gbk:
Рис. 26.1. Настройки резервного копирования В правой части окна можно ничего не менять, подробнее о параметрах резервного копирования мы поговорим ниже. Нажмите кнопку «ОК», и резервное копирование начнется, оно может занять некоторое время. По окончании копирования выйдет сообщение «Database backup completed». Теперь можете закрыть все окна и открыть папку с базой first.gdb. Там же должен появиться файл с резервной копией 03012010.gbk. Сравните размеры этих файлов и убедитесь, что backup-файл в десятки раз меньше. Далее полученную копию можно размножить уже обычным образом, перенести ее на другой ПК или (и) переименовать ее. При создании резервной копии можно указать любое имя файла. Мы указали текущую дату. Такой прием позволяет хранить много копий в одном месте, и всегда можно найти самую свежую из них.
Backup с помощью утилиты командной строки Как для создания резервной копии, так и для восстановления БД InterBase предлагает утилиту командной строки gbak, которая неявно вызывалась утилитой IBConsole и в предыдущем примере. Если вы еще не забыли, утилиты InterBase хранятся в папке BIN там же, где установлен InterBase. Утилита gbak является наиболее универсальным и гибким инструментом, позволяя задавать множество параметров. При миграции БД с одной версии InterBase к другой, действует правило: можно восстанавливать резервные копии более старых версий InterBase, но не наоборот. То есть, если ваша резервная копия сделана на InterBase версии 6.5, ее без проблем можно перенести в 7.1 или выше. При попытке сделать обратную миграцию, вы можете столкнуться с массой проблем. Также gbak можно 206
использовать для миграции между InterBase, Firebird или Yaffil, все клоны всех версий также содержат эту утилиту. Синтаксис утилиты простой: gbak <параметры> <файл_оригинал> <файл_копия> Параметры gbak для резервного копирования отражены в следующей таблице: Таблица 26.1. Параметры gbak для резервного копирования Параметр Описание Выполнить резервное копирование. Обязательный параметр для создания -b[ackup_database] резервной копии. Конвертирует внешние файлы, если они есть, во внутренние таблицы. -co[nvert] Копирование БД без сжатия. Нежелательный параметр при обычном -e[xpand] копировании. Устаревший параметр. Устанавливает коэффициент блокирования n для -fa[ctor] n ленточных устройств. Параметр запрещает сборку мусора. Обычно не используется, но может -g[arbage_collect] быть применен при попытке ремонта БД. Запрет на сверку с контрольными суммами. Можно применить, если -ig[nore] резервное копирование аварийно завершилось из-за ошибок контрольных сумм. Запрет на завершение повисших limbo-транзакций (см. Лекцию 24). Не -l[imbo] используйте для регулярного копирования! Копировать только метаданные. В результате получим пустую БД с -m[eta_data] таблицами, индексами, генераторами, триггерами и хранимыми процедурами, но без данных. Параметр делает копию непереносимой на другие серверы (InterBase / -nt Firebird / Yaffil). По умолчанию используется обратный параметр –t. Параметр сохраняет только метаданные в старых форматах InterBase. -ol[d_descriptions] Обязательный параметр с паролем пользователя, который делает -pas[sword] <пароль> резервную копию. Параметр с именем роли, если пользователь хочет зайти в контексте -ro[le] <имя_роли> роли. Параметр создает копию на том же ПК, где находится рабочая БД, то -se[rvice] <сервис> есть, на сервере. При этом вызывается Service Manager серверного ПК. Имеет смысл, если вы запускаете резервное копирование на сервере удаленно, через сеть. Параметр по умолчанию, создает копию, переносимую на другие -t[ransportable] серверы. -user <имя_пользователя> Обязательный параметр с именем пользователя, который делает резервную копию. Включает подробные сообщения о том, что делает gbak при -v[erify] копировании. Задает файл для вывода отчета о копировании. Если файл задан, а –v не -y <файл> был использован, то при удачном копировании файл будет пустым, иначе он будет содержать отчет об ошибках. Если такой файл уже есть, копирование не удастся. Показывает версию утилиты gbak. -z Загрузите окно cmd, сделайте текущей папку c:\program files\borland\interbase\bin 207
Если у вас InterBase установлен по другому адресу, то следует указать ваш адрес. Обычную копию можно сделать командой (выполните эти примеры, в окне cmd их придется вводить вручную): gbak –user sysdba –pas masterkey –b c:\databases\first.gdb c:\databases\first.gbk Копию с выводом информации на экран: gbak –user sysdba –pas masterkey –b –v c:\databases\first.gdb c:\databases\first2.gbk Копию пустой БД (только метаданные): gbak –user sysdba –pas masterkey –b –m c:\databases\first.gdb c:\databases\empty_first.gbk
Restore с помощью IBConsole Если резервное копирование базы данных нужно делать как можно чаще, то восстановление (restore) делается изредка, по необходимости. Например, для оптимизации работы базы данных (сборка мусора, удаление повисших транзакций, пересчет индексов), при миграции на другой SQL-сервер, или при разрушении БД, что к счастью, случается нечасто. Оптимизацию желательно делать не реже, чем раз в месяц. Если же вы делаете восстановление с целью перехода, например от InterBase 6.5 на InterBase 7.1 или выше, то резервную копию нужно делать на старом сервере, а восстановление – уже на новом. Если база данных большая, то восстановление может занять какое то время. Кроме того, если резервное копирование можно делать параллельно с работой других пользователей, то для восстановления нужен монопольный режим. Проще всего вынуть сетевой кабель из сервера, чтобы пользователи не могли продолжать работу с БД. Откройте IBConsole и войдите в локальный сервер (база данных first должна быть закрыта). Выберите меню «Database -> Maintenance -> Backup/Restore -> Restore». Часть параметров заполнится автоматически, однако их можно поменять. Например, можно выбрать другой файл с резервной копией, указать иной размер страниц, и т.д. Взгляните на следующий рисунок:
Рис. 26.2. Восстановление БД из резервной копии 208
Если вы помните, в лекции № 15 мы создавали базу данных с размером страницы 4096. Это было сделано специально, чтобы в данной лекции продемонстрировать изменение размера страницы при восстановлении резервной копии на более подходящий размер 8192. Также в поле Overwrite (Писать поверх существующего файла) мы указали True, чтобы заменить старую БД. В реальной практике записывать восстанавливаемую БД поверх существующей рабочей версии ни в коем случае не рекомендуется. Что, если восстановление пройдет неудачно, а рабочую БД мы уже «затрем»? Поэтому рекомендуется восстанавливать базу данных в другую папку, и только в случае, если при восстановлении не выйдет ошибок, поместить ее вместо старой рабочей версии. Причем желательно на всякий случай сделать простую копию (Проводником Windows или другим файловым менеджером) и старой рабочей версии. Теперь стоит только нажать «ОК», и восстановление базы начнется. Подробная информация о восстановлении будет выведена в отдельное окно. Закончится восстановление сообщением «Service ended at 03.01.2010 11:58:25» (у вас будут другие дата - время). После этого сообщения все окна Restore можно закрыть, а восстановленную БД – открыть и работать с ней.
Restore с помощью утилиты командной строки Для восстановления базы данных из резервной копии также используется утилита gbak, но уже с другими параметрами: Таблица 26.2. Параметры gbak для восстановления БД Параметр Описание Устанавливает размер буфера в страницах БД для восстановления. -bu[ffers] Выполнить восстановление БД. Обязательный параметр при -c[reate_database] восстановлении. При восстановлении БД делает индексы неактивными. Применяется -i[nactive] обычно при ремонте БД, если обычное восстановление прошло неуспешно из-за ошибок индексов. Не создавать теневые копии shadow (о теневых копиях см. ниже). -k[ill] Задает доступ к восстанавливаемой БД. Может быть «read_write» (по -m[ode] <доступ> умолчанию, чтение/запись) или «read_only» (только чтение). Не восстанавливать проверки ограничений. Применяется обычно при -n[o_validity] ремонте БД, если обычное восстановление прошло неуспешно из-за нарушений ограничений CHECK. Восстанавливать одну таблицу за раз. Применяется обычно при ремонте -o[ne_at_a_time] БД, если база содержит разрушенные данные. Устанавливает для восстанавливаемой БД новый размер страниц. -p[age_size] <значение> Значение может быть 1024, 2048, 4096 или 8192 (наиболее предпочтительный). Обязательный параметр с паролем пользователя, который делает -pas[sword] <пароль> восстановление. Если указанная БД уже существует, она будет перезаписана при -r[eplace_database] восстановлении. Если БД не существует, будет создан файл с указанным именем. Параметр восстанавливает БД на том же ПК, где находится резервная -se[rvice] <сервис> копия (на сервере). При этом вызывается Service Manager серверного ПК. Используется, если восстановление запускается с удаленного ПК. Параметр заставляет страницы восстанавливаемой БД заполняться на -use_[all_space] 100%, вместо 80% по умолчанию. Полезен только при восстановлении БД с параметром «-m read_only» (только для чтения), так как рабочим БД требуется место для хранения версий строк. -user <имя_пользователя> Обязательный параметр с именем пользователя, который делает восстановление. 209
-v[erify] -y <файл>
-z
Включает подробные сообщения о том, что делает gbak при восстановлении. Задает файл для вывода отчета о восстановлении. Если файл задан, а –v не был использован, то при удачном восстановлении файл будет пустым, иначе содержать отчет об ошибках. Если такой файл уже есть, восстановление не удастся. Показывает версию утилиты gbak.
Обычное восстановление утилитой gbak с выводом информации на экран может быть выполнено командой (сначала перенесите существующий first.gdb на другое место): gbak –user sysdba –pas masterkey –c –v c:\databases\03012010.gbk c:\databases\first.gdb Восстановление может занять некоторое время. Когда восстановление закончится, вы увидите мигающий курсор после адреса папки BIN. Как видите, синтаксис восстановления отличается от резервного копирования: вначале указывается резервная копия, из которой мы желаем восстановить базу данных, затем указывается файл создаваемой БД. Если при восстановлении БД мы хотим изменить размер страниц, укажем их в параметре –p: gbak –user sysdba –pas masterkey –c –p 8192 –v c:\databases\03012010.gbk c:\databases\first.gdb
Теневые (Shadow) копии Сервер InterBase имеет механизм теневого (shadow) копирования базы данных. Такое копирование создает «зеркало» базы данных на случай какого-либо аппаратного или сетевого сбоя, или случайного удаления или повреждения базы данных операционной системой. В этом случае имеется возможность вручную или даже автоматически перейти с основной базы данных на теневую копию, и продолжить работу системы. Однако теневое копирование ни в коем случае не может заменить резервное копирование утилитой gbak, так как все ошибки и мусор, присутствующие в основной базе данных, будут присутствовать и в ее теневой копии. Shadow – файлы являются дополнительным средством безопасности данных, но никак не панацеей от всех возможных бед. Теневые копии можно создавать и в другой папке того диска, на котором находится основная база данных, однако при этом теряются все преимущества использования shadow копий. Обычно такие копии создают на другом жестком диске, физически подключенном к серверному ПК. Создание и удаление теневых копий происходит посредством команд DDL (часть SQL), которые можно вводить разными способами, например, с помощью IBConsole. Для примера создадим теневую копию для нашей базы данных first.gdb. Эту копию мы поместим по адресу D:\DataBases\ , однако если у вас на компьютере нет диска D:, то просто создайте другую папку, например, C:\DataBases2\ и соответственно меняйте адрес в примерах ниже на собственный. На момент создания теневой копии указанная папка должна существовать. Запустите сервер InterBase, если он не работает, и загрузите IBConsole. Войдите в локальный сервер пользователем SYSDBA (также на создание теневой копии имеет право владелец (создатель) базы данных, если ее создавал не SYSDBA). Далее откройте базу данных first. Теневая копия при создании привязывается к той базе данных, для которой она создается, поэтому база данных должна бать открыта. А при удалении теневой копии из БД удаляются и ссылки на нее. Итак, нажмем кнопку «SQL», открывающую окно Interactive SQL, и в окне запросов введем следующий оператор: CREATE SHADOW 1 AUTO ‘D:\DATABASES\First.shd’; Затем нажмем кнопку «Execute Query», чтобы выполнить запрос. Вы можете открыть указанную папку Проводником или любым файловым менеджером, и убедиться, что файл с теневой копией создан. Чтобы полноценно связать shadow файл с базой данных и наполнить его данными, придется закрыть IBConsole, или хотя бы прервать контакт с базой данных (disconnect). Как только это случится, теневая 210
копия наполнится данными, и будет иметь такой же размер, как основная БД. При следующем открытии базы и работе с ней все изменения также будут производиться и в теневой копии. Удалить теневую копию можно также в Interactive SQL при подключенной БД командой DROP SHADOW 1; При этом в базе данных удаляются все ссылки на shadow копию, а файл с этой копией физически удаляется с диска. Разберем синтаксис создания и удаления Shadow-копий. CREATE SHADOW <номер> [AUTO | MANUAL] [CONDITIONAL] 'спецификация-файла' [LENGTH [=] <целое> [PAGE[S]]] [<вторичный-файл>]; Здесь: <номер> - любое целое число, идентификатор shadow-копии. [AUTO] – автоматический режим, устанавливается по умолчанию. Этот режим позволяет базе данных работать в случае, если теневая копия по каким-то причинам станет недоступной, или если недоступной станет рабочая база данных при доступной теневой копии. Если недоступной станет рабочая база данных, InterBase заменит ее теневой копией и восстановит соединение. В таком случае выводится окно с сообщением, чтобы проинформировать администратора о случившемся. [MANUAL] – ручной режим. При выборе этого режима, если вдруг теневая копия становится недоступной, то доступ к базе данных прекращается. Чтобы возобновить доступ, администратору необходимо вручную удалить испорченную теневую копию, и создать новую. [CONDITIONAL] – этот режим является дополнением к [AUTO], и подразумевает, что при разрушении базы данных InterBase автоматически заменит базу данных теневой копией и восстановит соединение. Но если при режиме [AUTO] администратору придется заново создавать новую теневую копию, то в случае [CONDITIONAL] InterBase сделает это сам. Режим AUTO CONDITIONAL является наиболее предпочтительным для создания бесперебойной работы системы. 'спецификация-файла' описывает адрес и имя файла теневой копии. По традиции принято таким копиям назначать расширение *.shd [LENGTH] – необязательный параметр, который используется при создании многофайловой теневой копии. Атрибут <целое> - это целое число, указывающее размер первичного и вторичного файлов в страницах. Синтаксис удаления теневой копии еще проще: DROP SHADOW <номер> Где номером является идентификатор shadow-копии. Как узнать этот номер, если мы создавали копию давным-давно, и уже его не помним? Давайте создадим еще одну CONDITIONAL-теневую копию: CREATE SHADOW 3 CONDITIONAL ‘d:\DataBases\first.shd’; Затем прервем соединение с базой данных, и вновь соединимся с ней, чтобы наполнить копию данными. Теперь выделим в IBConsole базу данных first и выберем команду меню «Database -> View Metadata». Откроется окно, показывающее, как создавалась БД:
211
Рис. 26.3. Метаданные базы first Как видно из рисунка, в числе прочего указано и создание теневой копии вместе с ее адресом и идентификатором. Создание многофайловой теневой копии имеет смысл, если размеры базы данных становятся огромными, по 2 и более гигабайта. Тогда может случиться, что на диске, выделенном под теневую копию, не хватит для нее места. В этом случае ее можно разбить на несколько частей, создавая каждую часть на своем диске. Пример создания многофайловой теневой копии (если у вас нет записывающих дисков D:, E: и F:, то можете просто указать другую папку на диске C:): CREATE SHADOW 4 CONDITIONAL ‘d:\DataBases\first1.shd’ LENGTH 15000 FILE ‘e:\DataBases\first2.shd’ LENGTH 15000 FILE ‘f:\DataBases\first3.shd’; В этом случае создастся три файла. Первый файл будет наполняться, пока его размер не достигнет 15 тысяч страниц БД, затем начнет наполняться второй файл теневой копии. Как только размер второго файла достигнет 15 тысяч страниц, начнет наполняться третий файл. Можете удалить теневую копию №4, она нам больше не нужна.
Утилита командной строки gfix Эта утилита является одним из основных инструментов администратора БД. Утилита gfix позволяет: Выполнять принудительную чистку (sweep) базы данных; Изменять интервал автоматической чистки; Закрывать базу данных для получения монопольного доступа, и затем снова открывать ее; Переводить базу в режимы «чтение/запись» или «только чтение»; Переключаться между синхронным и асинхронным вводом (Forced Writes); Изменять диалект БД; Устанавливать размер кэша; Отыскивать повисшие транзакции и отменять или подтверждать их; Активировать или удалять теневые копии; Производить ремонт поврежденных баз данных. Синтаксис утилиты очень простой: 212
gfix [параметры] <база данных>; Все параметры утилиты приведены в следующей таблице: Таблица 26.3. Параметры утилиты gfix Параметр Описание -ac[tivate] <теневая копия> Параметр предназначен для активации теневой копии. <Теневая копия> адрес и имя файла теневой копии (или первого из файлов). Дополнительный параметр к –shut. Предназначен для запрета новых -at[tach] n соединений с БД. n указывает количество секунд, через которое произойдет отключение БД. Отключение отменится, если к этому времени еще останутся активные соединения. Устанавливает новый размер кэша (буфера) БД в страницах. n – -b[uffers] n количество страниц. По умолчанию, кэш = 75 страниц. Параметр зарезервирован для будущих версий и не используется. -ca[che] n Завершает подтверждением зависшую транзакцию с идентификатором -c[ommit] {ID| all} ID, или все зависшие транзакции (all) Дополнительный параметр к –shut. Предназначен для принудительного -force n закрытия базы данных. n указывает количество секунд, через которое произойдет закрытие. Если остались активные пользователи, они отключатся, последние результаты их работы будут потеряны. Такое средство нужно применять с осторожностью, как последнюю возможность. Используется вместе с –v для проверки структур записей и таблиц; -f[ull] освобождает неназначенные фрагменты записей. Изменяет интервал транзакций для автоматической чистки sweep (по -h[ousekeeping] n умолчанию 20 000). n устанавливает новый интервал. Если n=0, автоматическая чистка запрещена. Игнорировать ошибки контрольных сумм при проверке или чистке. -i[gnore] Удаляет все неиспользуемые теневые копии базы данных. -k[ill] <база данных> Показывает ID всех зависших транзакций. Также показывает, что -l[ist] произойдет при наличии зависших limbo-транзакций и использования опции –t. Помечает разрушенные записи как неиспользуемые. -m[end] -mo[de] {read_write | Устанавливает режим БД. Может быть read_write (чтение-запись, по умолчанию), или read_only (только чтение). read_only} Используется вместе с –v для проверки разрушенных или -n[o_update] неразмещенных структур. Если таковые есть, они отобразятся в сообщении, но не будут исправлены. Открывает закрытую после –shut базу данных. -o[nline] Пароль пользователя SYSDBA или владельца базы данных для работы с -pa[ssword] <пароль> gfix. Используется вместе с –l. Выводит подсказки при восстановлении -p[rompt] транзакций. Завершает откатом зависшую транзакцию с идентификатором ID, или все -r[ollback] {ID | all} зависшие транзакции (all) Запускает принудительную чистку БД. -sweep Изменяет диалект базы данных. n может быть 1 или 3. -s[ql_dialect] n Закрывает базу данных. Используется с одним из дополнительных -sh[ut] параметров –attach, –force или –tran. Автоматическое двухфазное восстановление limbo транзакции с номером -t[wo_phase] {ID | all} ID, или всех транзакций (all). Дополнительный параметр к –shut. Предназначен для запрета запуска -tr[an] n новых транзакций. n указывает количество секунд, через которое 213
произойдет отключение БД. Отключение отменится, если к этому времени еще останутся активные транзакции. Включает 100% заполнение страниц БД (full) или 80% заполнение по умолчанию (reserve). 100%-е заполнение имеет смысл для баз только для чтения. Имя администратора или владельца базы данных для работы с gfix. Определяет и показывает неназначенные страницы БД. То есть, созданные, но не назначенные для каких либо структур данных. Переключает режимы синхронной / асинхронной записи Forced Writes. Выводит версию InterBase и утилиты gfix.
Чистка sweep происходит в фоновом режиме и может выполняться параллельно работе пользователей. Этот способ более предпочтителен, чем чистка gbak при восстановлении БД. Утилита gbak не делает полной чистки, так как остаются версии удаленных записей и записей отмененных транзакций. Принудительную чистку можно сделать так: gfix –sweep c:\databases\first.gdb –user sysdba –pa masterkey Чистка базы данных происходит автоматически через определенное количество (по умолчанию 20 000) транзакций. Расчет интервала делается из старейшей транзакции, зарегистрированной в области TIP (Инвентарная страница транзакций), и из старейшей активной транзакции. Когда инвентарный номер старейшей активной транзакции окажется больше на указанный интервал, чем инвентарный номер старейшей зарегистрированной в TIP транзакции, произойдет автоматический запуск чистки. Изменить интервал на 10 000, например, можно так: gfix –h 10000 c:\databases\first.gdb –user sysdba –pa masterkey Если же вместо 10 000 указать 0, автоматическая чистка для данной БД вообще будет отменена. Как уже говорилось, чистка не требует монопольного доступа к базе данных, однако если БД очень большая, и много пользователей интенсивно с ней работают, чистка может заметно замедлить работу с БД. В этом случае, перед чисткой рекомендуется вначале отключить базу данных. Отключение базы данных делается командой –shut с одним из трех дополнительных параметров. Чтобы безусловно отключить базу данных через 10 минут, выполните команду: gfix –sh –force 600 c:\databases\first.gdb –user sysdba –pa masterkey Однако такой радикальный способ рекомендуется использовать с осторожностью: пользователи, которые на этот момент продолжали работу с БД, потеряют результаты своей работы. Вначале вместо «-force» лучше попробовать более «мягкие» дополнительные параметры «–attach» или «–tran». После того, как база данных закрыта, и с ней выполнены все необходимые действия, ее нужно открыть командой gfix –o c:\databases\first.gdb –user sysdba –pa masterkey Кэш (или буфер) – это оперативная память, выделяемая сервером для работы с базой данных. Операции в оперативной памяти происходят гораздо быстрее, чем если данные постоянно считываются с диска. Размер кэша указывается в страницах БД. Если размер страницы установлен 8192, то кэш в 5000 страниц займет примерно 40 мегабайт ОЗУ. По умолчанию, InterBase использует кэш в 75 страниц. Если сразу много пользователей одновременно обращаются к базе данных, может случиться, что серверу не хватит выделенной оперативной памяти. В этом случае он начнет работать с диском, что замедлит производительность БД. Изменить размер кэша для базы данных на 300 страниц можно так: gfix –b 300 c:\databases\first.gdb –user sysdba –pa masterkey
214
Другим способом установить размер кэша по умолчанию для всех вновь создаваемых БД, является изменение конфигурационного файла ibconfig, который находится в папке InterBase. По умолчанию, это c:\program files\borland\interbase\ibconfig Это обычный текстовый файл, в котором все параметры закомментированы (первым символом идет «#»). Нужно снять комментарий, и установить новое значение нужного параметра. То есть, вместо #DATABASE_CACHE_PAGES
75
указать DATABASE_CACHE_PAGES
300
Однако более предпочтительным способом для этих целей является утилита gfix, так как она позволяет установить собственный размер кэша для каждой базы данных. Если какой то БД пользуются реже, размер кэша для нее можно оставить по умолчанию, или даже уменьшить. База данных может работать в одном из двух режимов доступа: только для чтения, или для чтения / записи (по умолчанию). Если вам понадобилось запретить пользователям модифицирование данных, вы можете поменять режим командой: gfix –mo read_only c:\databases\first.gdb –user sysdba –pa masterkey Операции изменения режима занимают время! Не забудьте потом вернуть режим read_write, иначе пользователи не смогут вносить изменения в БД. Режимы Forced Writes требуют особого пояснения. Forced Writes, или режим синхронного ввода, определяет, как будет происходить работа с БД. При включенном Forced Writes добавление новых записей, удаление старых, появление новых версий записей сразу же физически сохраняется на диске. При отключенном синхронном вводе сервер InterBase возлагает это на операционную систему: физическое сохранение изменений происходит позже – когда переполнится буфер, или когда ОС решит, что компьютер долго простаивает. Отключение Forced Writes следует делать лишь на очень надежных машинах, с обязательным источником бесперебойного питания (UPS). Ведь может случиться, что физической записи на диск не происходит целый день, а при сбое системы или отключении питания потеряются результаты всей работы! Отключенный режим немного увеличивает скорость работы с БД, однако данные становятся менее защищенными. По умолчанию, все БД работают с включенным Forced Writes и отключать этот режим не рекомендуется. Если все же вы не удовлетворены производительностью БД, и при этом стопроцентно уверены в своем серверном ПК, можете попробовать отключить Forced Writes командой: gfix –w async c:\databases\first.gdb –user sysdba –pa masterkey Команда gfix –v c:\databases\first.gdb –user sysdba –pa masterkey выведет на экран все неназначенные страницы, которые являются мусором. Если на экран ничего не вышло, значит, таких страниц в БД нет. Если база данных разрушилась, можно вместо нее активировать теневую копию командой gfix – ac d:\databases\first.shd –user sysdba –pa masterkey
Рекомендации по ремонту поврежденных баз данных К счастью, сервер InterBase – достаточно надежная система, ваша база данных может работать годами без необходимости ремонта. Систематическое резервное копирование также гарантирует вас от 215
всяких неожиданных неприятностей. Если все же когда-нибудь придется ремонтировать базу данных, воспользуйтесь следующими рекомендациями: 1. Прежде всего, отключите от базы всех пользователей, не позволяйте им вносить изменения в БД. 2. Сделайте копию рабочей базы данных средствами файловой системы (gbak не сможет выполнить резервное копирование с разрушенными данными). Все попытки восстановления делайте с полученной копией, не трогая оригинал. 3. Проведите полную проверку (gfix –v –full <база данных> <пользователь> <пароль>). Если выводятся сообщения об ошибках контрольных сумм, можно добавить переключатель –i, чтобы игнорировать эти ошибки. 4. Далее можно попытаться исправить разрушенные данные, помечая их как недоступные: gfix –mend –full –ignore <база данных> <пользователь> <пароль> 5. После этого вновь выполните проверку на наличие разрушенных структур, как в № 3, но без –i. 6. Затем попробуйте снова выполнить резервное копирование утилитой gbak, например: gbak –b –v –i <база данных> <резервная копия> <пользователь> <пароль> 7. Если это удалось, то все хорошо. Иначе попробуйте сделать еще одно резервное копирование, добавив параметр –g (не собирать мусор). Если разрушения связаны с повисшими limboтранзакциями, то –limbo. 8. В большинстве случаев, такие меры позволяют сделать нормальную резервную копию. Попробуйте восстановить ее командой gbak –create –v <резервная копия> <база данных> <пользователь> <пароль> Как правило, такой порядок действий позволяет восстановить разрушенную БД. Если же этого не случилось, попробуйте использовать другие переключатели. Например, -inactive у утилиты gbak делает неактивными индексы, и если разрушены именно они, восстановление удастся. Параметр – one_at_a_time будет восстанавливать по одной записи за раз, что позволит восстановить целые таблицы, и пропустить поврежденные. Если вы грамотно спроектируйте вашу базу данных и организуете систематическое резервное копирование, то возможно, вам никогда и не придется прибегать к ремонту БД. Но знать, как это делается, необходимо каждому программисту и администратору баз данных.
216
Лекция 27. Программное администрирование баз данных InterBase. Данная лекция позволит изучить способы администрирования баз данных программно, из проекта Delphi. Ведь может случиться, что вы будете писать программу и проектировать базу данных «на заказ», для какой-нибудь небольшой фирмы. Такая фирма, как правило, не может позволить себе иметь в штате профессионального программиста и (или) администратора. В подобных фирмах роль администратора берет на себя один из сотрудников, который является более-менее опытным пользователем. Обучить такого пользователя премудростям и тонкостям работы с утилитой IBConsole непросто, еще сложнее научить его пользоваться утилитами командной строки. И здесь нам на помощь приходит набор компонентов с вкладки «InterBase Admin» палитры компонентов, который входит в стандартный состав Delphi. С помощью этих компонентов можно создать простое приложение, которое будет выполнять административные функции с базами данных, и с которым сможет работать любой пользователь.
Разработка программы AdminIB Итак, наша задача: создать приложение, которое позволит регулярно делать резервную копию базы first.gdb как в ручном, так и в автоматическом режиме. Также приложение, в случае необходимости, должно уметь восстанавливать базу данных из резервной копии, и сохранять лог-файлы о копировании и восстановлении. В случае каких-то проблем, администратор вышлет вам эти логи, по которым вы сможете определить проблему. Также программа должна уметь добавлять новых пользователей, удалять или редактировать старых. Разумеется, пользователя SYSDBA мы удалять не позволим. Убедимся, что сервер InterBase у нас работает, и загружаем Delphi. Выделим форму, в свойство Name которой впишем fMain, а в свойство Caption – «Администрирование базы данных First.gdb». Сохраним проект в отдельную папку, модулю дадим имя Main, а проекту в целом – AdminIB. Неплохо было бы сразу установить свойство BorderStyle в bsDialog, чтобы программа не могла менять размеры, а свойство Position – в poDesktopCenter.
Реализация резервного копирования Переходим на вкладку Win32 Палитры компонентов, находим и устанавливаем на форму компонент PageControl (каждая задача будет на своей вкладке). Переименуем полученный PageControl1 в PC1 для краткости кода, а свойству Align присвоим значение alClient. Щелкаем по компоненту PC1 правой кнопкой и выбираем команду New Page – создалась новая вкладка. Выделим ее, и в свойстве Name вместо TabSheet1 напишем TSh1, а в свойстве Caption – «Резервное копирование базы данных». Будьте внимательны: очень легко ошибиться, и вместо листа TabSheet выделить весь компонент PageControl. Примерный вид вкладки смотрите на рисунке:
217
Рис. 27.1. Вкладка «Резервное копирование базы данных» Вначале установим на лист панель Panel1, свойству Align которого присвоим значение alTop. Очистим свойство Caption. На панель поместим две радиокнопки RadioButton. В свойству Name первой кнопки укажем RB1, у второй – RB2. А в свойстве Caption, соответственно, «Ручное копирование» и «Автоматическое копирование». Свойство Checked компонента RB2 переведем в True. Ниже поместим простую кнопку Button, которую переименуем в bStartBackup, а в свойстве Caption напишем «Начать копирование». Свойство Enabled переведем в False. Поскольку по умолчанию, программа будет выполнять автоматическое копирование, эта кнопка будет недоступна. Как только мы выделим радиокнопку «Ручное копирование», сразу вернем доступность этой кнопке. В правой части панели поместите компонент Label. Чтобы компонент мог содержать многострочный текст, свойство AutoSize переведите в False, а свойство WordWrap – в True. В свойстве Caption напишите текст «Время запуска автоматического резервного копирования:», и подгоните размеры, как на рисунке. Ниже находятся еще два Label с текстом «Часы:» и «Минуты:». Рядом с ними располагаются два компонента SpinEdit с вкладки Samples Палитры компонентов. Первый переименуйте в SE1, второй – в SE2. У первого в параметре MinValue оставьте 0, в параметре MaxValue укажите 23, а в параметре Value поставьте 1 (по умолчанию, автокопирование будет начинаться в час ночи, когда сотрудников на работе нет). У второго SpinEdit параметры такие: MinValue = 0, MaxValue = 59, Value = 0. Вместо этих компонентов можно было бы использовать один MaskEdit с маской «99:99», но, к сожалению, сам компонент не проверяет правильность ввода значений часов и минут, придется делать это самим. Поэтому остановимся на компонентах SpinEdit, которые позволят пользователю вводить только целые числа в заданном диапазоне. Под панелью установите компонент Memo. Свойство Align установите в alClient, свойство ScrollBars в ssVertical, и не забудьте очистить от текста свойство Lines. Далее главное: нужно добавить два не визуальных компонента – Timer с вкладки System и IBBackupService с вкладки InterBase Admin. Компоненты не визуальные, их можно расположить на 218
любом месте, первый будет запускать копирование автоматически, когда подойдет указанное время, второй – осуществлять само копирование. У Timer свойство Interval установите 60000, чтобы таймер срабатывал один раз в минуту. Компонент IBBackupService предназначен для создания резервных копий базы данных InterBase. Этот компонент позволяет делать различные настройки резервного копирования, в зависимости от того, какие параметры в свойстве Options включены. У IBBackupService свойство Name для краткости переименуйте в IBBS. На этом приготовления вкладки закончены, приступим к кодированию. Все резервные копии должны попадать в одну папку, например, C:\DataBases\Backup. Эта папка физически должна существовать на диске. При этом мы не сможем делать копии с одним и тем же именем, ведь операционная система не позволит создавать файл с таким же именем. Конечно, можно перед очередным копированием удалять старую копию, но это тоже не выход – что, если новая резервная копия будет создана с ошибками, то есть, в базе данных появились нарушения? Можно было бы восстановить ее из старой копии, но для этого ее не нужно удалять! Выход: добавлять к имени файла дату и время его создания, тогда можно делать сколько угодно копий, и у всех будут разные имена. Для этого в разделе Private модуля главной формы опишем функцию GetName: private { Private declarations } function GetName():String;
Установите курсор на название функции и нажмите , чтобы сгенерировать саму функцию. Вот ее код: {Функция возвращает префикс имени файла в виде yyyymmdd_hhmmss_ } function TfMain.GetName: String; var ye, mo, da : Word; //для даты ho, mi, se, ms : Word; //для времени st : String[2]; //для добавления нуля, например 05 begin DecodeDate(Date, ye, mo, da); //декодируем на составные дату DecodeTime(Time, ho, mi, se, ms); //декодируем время //теперь собираем строку: Result:= IntToStr(ye); //добавили год //получаем и добавляем месяц: st:= IntToStr(mo); if Length(st) = 1 then st:= '0' + st; //если 1 символ, добавим спереди 0 Result := Result + st; //получаем и добавляем день: st:= IntToStr(da); if Length(st) = 1 then st:= '0' + st; Result := Result + st + '_'; //теперь получаем и добавляем час: st:= IntToStr(ho); if Length(st) = 1 then st:= '0' + st; Result := Result + st; //получаем и добавляем минуты: st:= IntToStr(mi); if Length(st) = 1 then st:= '0' + st; Result := Result + st; //получаем и добавляем секунды: st:= IntToStr(se); if Length(st) = 1 then st:= '0' + st; Result := Result + st + '_'; //теперь функция вернет префикс имени файла, например: //20100205_010012_ end;
Здесь мы декодировали на составные части дату и время. Год уже имеет 4 цифры, поэтому его дополнительно обрабатывать не нужно. А вот месяц, день, час, минута или секунда могут состоять из одной цифры. Чтобы не запутаться, делаем проверку: 219
st:= IntToStr(mo); if Length(st) = 1 then st:= '0' + st; //если 1 символ, добавим спереди Result := Result + st;
0
В переменную st мы получаем номер месяца. Если этот номер состоит из одной цифры, перед ней добавим ‘0’. И в конце прибавим результат в переменную Result. Таким же образом мы проверяем и остальные данные. Эта функция гарантирует нам не только уникальность имени файла, но и правильную сортировку файлов по дате. К тому же по имени сразу видно – когда был создан файл. Теперь сделаем доступной кнопку bStartBackup, если пользователь отметил радиокнопку RB1 (Ручное копирование). Для этого сгенерируем для RB1 событие onClick: {Щелкнули по "Ручное копирование"} procedure TfMain.RB1Click(Sender: TObject); begin //если отмечено Ручное копирование, делаем //кнопку доступной: if RB1.Checked then bStartBackup.Enabled:= True; end;
Точно также сделаем кнопку недоступной, если щелкнули по RB2 (Автоматическое копирование). Для этого сгенерируем событие onClick для RB2: {Щелкнули по "Автоматическое копирование"} procedure TfMain.RB2Click(Sender: TObject); begin //если отмечено Автоматическое копирование, делаем //кнопку недоступной: if RB2.Checked then bStartBackup.Enabled:= False; end;
Соображаем дальше. Копирование у нас будет начинаться либо по нажатию кнопки, либо по срабатыванию таймера, в зависимости от того, какая радиокнопка выделена. Значит, чтобы дважды не писать один и тот же код, создадим процедуру резервного копирования, которую будем вызывать из двух мест. Поскольку процедура будет напрямую работать с компонентом IBBS, вначале объявим ее в разделе private, после функции GetName: private { Private declarations } function GetName():String; procedure BackupCopy;
Установите курсор на процедуру и нажмите , чтобы сгенерировать тело процедуры. Ее код: {Процедура реализации резервного копирования} procedure TfMain.BackupCopy; var s : String; //для получения префикса файла begin //получим префикс: s:= GetName(); //очистим Memo1, если там что то есть: Memo1.Clear; //задаем параметры компонента IBBackupService: IBBS.ServerName := 'MyServ'; IBBS.LoginPrompt:= False; IBBS.Params.Add('user_name=sysdba'); IBBS.Params.Add('password=masterkey'); IBBS.Active := True;
220
//начинаем копирование try IBBS.Verbose:= True; IBBS.DatabaseName:= 'c:\DataBases\first.gdb'; IBBS.BackupFile.Clear; IBBS.BackupFile.Add('c:\DataBases\Backup\' + s + 'first.gbk'); IBBS.ServiceStart; //пока не дошли до конца, записываем параллельно лог в Memo1: while not IBBS.Eof do Memo1.Lines.Add(IBBS.GetNextLine); finally IBBS.Active:= False; end; //сохраним лог в файл: Memo1.Lines.SaveToFile('c:\DataBases\Backup\' + s + 'first.log'); end;
Здесь мы вначале получили в переменную префикс имени файла. Зачем это нужно? Нам требуется: 1. Сделать резервную копию. 2. Сохранить лог копирования в файл. Эти действия будут производиться в разное время, поэтому чтобы префикс копии совпадал с префиксом лог-файла, мы и сохраняем его в переменную s: //получим префикс: s:= GetName();
Далее мы очищаем Memo1, если вдруг там уже был текст. После чего приступаем к настройкам параметров компонента IBBS: IBBS.ServerName := 'MyServ'; IBBS.LoginPrompt:= False; IBBS.Params.Add('user_name=sysdba'); IBBS.Params.Add('password=masterkey'); IBBS.Active := True;
Тут все достаточно прозрачно. Поскольку копирование будет производиться на серверном ПК, то будет использован локальный адрес, следовательно, свойство ServerName мы можем назвать, как угодно. Если бы сервер был сетевым, пришлось бы писать сетевое имя этого ПК, или его IP-адрес. При этом следует обратить внимание на свойство Protocol. Это свойство позволяет выбрать протокол соединения. Поскольку программа будет загружаться там же, где установлен InterBase и где хранится база данных, оставляет значение по умолчанию – Local. Если программа будет работать с удаленным сервером, то здесь нужно будет выбрать один из сетевых протоколов, обычно выбирают TCP. Далее с помощью LoginPrompt мы заявляем, что не нужно запрашивать имя пользователя и пароль (кто же введет эти данные в час ночи, когда запустится авто-копирование?). Затем мы вводим в свойство Params имя пользователя SYSDBA и его пароль. Если вы изменили пароль (а это обязательно нужно сделать в реальной системе), вместо «masterkey» укажите свой пароль. В конце мы делаем IBBS активным. Само копирование мы помещаем в блок try…finally…end, поскольку копирование может быть и неудачным, если БД разрушена: try IBBS.Verbose:= True; IBBS.DatabaseName:= 'c:\DataBases\first.gdb'; IBBS.BackupFile.Clear; IBBS.BackupFile.Add('c:\DataBases\Backup\' + s + 'first.gbk'); IBBS.ServiceStart; //пока не дошли до конца, записываем параллельно лог в Memo1: while not IBBS.Eof do Memo1.Lines.Add(IBBS.GetNextLine); finally
221
IBBS.Active:= False; end;
Здесь свойство Verbose (многословность) определяет – будет ли выводиться лог резервного копирования. При значении True лог ведется, иначе – нет. Нам нужно, чтобы лог создавался. В свойство DatabaseName помещаем адрес и имя нашей рабочей базы данных. Свойство BackupFile должно содержать имя файла (или файлов) резервной копии. У нас база данных состоит из одного файла, и резервная копия также будет состоять из одного файла, поэтому прежде, чем мы поместим туда имя очередной резервной копии, это свойство нужно очистить. Затем формируем имя файла из адреса, префикса и самого имени «first.gbk». А затем стартуем копирование методом ServiceStart. Заметим, что установка True в свойстве Active компонента не начинает резервное копирование, а только делает компонент активным. Параллельно копированию в компонент Memo1 помещаем отчет о текущем действии, от начала копирования до конца. Метод GetNextLine компонента IBBackupService возвращает очередную строку лог-отчета. Когда копирование окончено, делаем компонент IBBS неактивным. И в самом конце процедуры записываем полученный в Memo1 лог в файл с таким же именем, как резервная копия, но расширением *.log: //сохраним лог в файл: Memo1.Lines.SaveToFile('c:\DataBases\Backup\' +
s + 'first.log');
Теперь для ручного копирования нам осталось сгенерировать процедуру нажатия на кнопку «Начать копирование», откуда вызовем процедуру BackupCopy: {Нажали на кнопку "Начать копирование"} procedure TfMain.bStartBackupClick(Sender: TObject); begin BackupCopy; end;
Попробуйте выполнить ручное копирование. Напомню, что сервер InterBase должен быть запущен, а папка «C:\DataBases\Backup» должна физически существовать на диске. Как только вы нажмете на кнопку, копирование начнется. Закончится оно примерно такой строкой: gbak: closing file, committing and finishing. 38912 bytes written. Зайдите в папку Backup – там должна появиться пара файлов с одинаковым именем и расширениями *.gbk и *.log. Это и есть наши резервная копия и лог-файл. Займемся автоматическим копированием. Выделите таймер и сгенерируйте для него событие onTimer, которое будет срабатывать каждую минуту (Interval=60000): {Сработал таймер} procedure TfMain.Timer1Timer(Sender: TObject); var ho, mi, se, ms : Word; //для времени begin //если включено ручное копирование, просто выходим: if RB1.Checked then Exit; //иначе декодируем время DecodeTime(Time, ho, mi, se, ms); //декодируем время //если нужный час и нужная минута, начинаем копирование: if (ho = SE1.Value) and (mi = SE2.Value) then BackupCopy; end;
Вначале мы проверяем, не включено ли ручное копирование. Если включено, то сразу выходим, ничего не делая. Затем мы снова декодируем время, чтобы посмотреть – совпадают ли данные в компонентах SpinEdit с текущими часом и минутой. И если совпадают, вызываем процедуру BackupCopy. 222
Заметим, что таймер может сработать не сразу, как системные часы покажут нужное время. Ведь в таймере учитываются и секунды. Значит, если копирование у нас должно начаться в 01:00, а когда программа была запущена, ваше системное время показывало, скажем, 25 секунд, то и таймер сработает в 01:00:25. Что, в принципе, не столь важно. Сохраните проект, скомпилируйте и запустите. Попробуйте установить время авто-копирования на минуту-другую больше, чем показывают ваши системные часы (должна быть выделена радиокнопка «Автоматическое копирование»). Дождитесь, когда системное время сравняется с указанным. Возможно, придется подождать еще несколько секунд, после чего резервное копирование начнется автоматически. Опять появится пара файлов в папке Backup. Таким образом, достаточно выделить радиокнопку RB2 (у нас она выделена по умолчанию), и не выключать на сервере программу, чтобы обеспечить регулярное резервное копирование базы данных. Разумеется, серверный ПК с InterBase, вашей базой данных и администраторской программой должен быть включен в назначенное время (серверные компьютеры обычно работают в режиме 24/7, то есть 24 часа в сутки, 7 дней в неделю). Напоследок заметим, что компонент IBBackupService имеет сложное раскрывающееся свойство Options, которое имеет следующие переключатели: Таблица 27.1. Переключатели свойства Options компонента IBBackupService Переключатель Описание Игнорировать ошибки контрольных сумм. Применяется обычно при попытке IgnoreChecksums восстановить поврежденную БД. Игнорировать ошибки двухфазного подтверждения limbo-транзакций. Также IgnoreLimbo применяется при восстановлении поврежденной БД. При включенном переключателе в резервную копию попадут только MetadataOnly метаданные. Можно использовать, если вы хотите получить пустую БД с таблицами, индексами, генераторами, триггерами и проч., но без записей. Не делать сборку мусора во время резервного копирования. NoGarbageCollection Делать резервную копию в старом стиле. Как правило, не применяется. OldMetadataDesc Делать резервную копию, непереносимую на другие версии InterBase (а также NonTransportable Firebird и Yaffil). Преобразует внешние файлы во внутренние таблицы. Также может ConvertExtTables применяться при восстановлении поврежденной БД. Как видно из таблицы, резервное копирование можно делать с некоторыми параметрами. Мы не использовали этих параметров, то есть, делали копию с параметрами «по умолчанию». В результате получили обычную резервную копию, для чего и нужна наша программа. Если же доведется ремонтировать базу данных, то делать это нужно утилитами gfix и gback, которые предоставляют гораздо больше возможностей, чем указанные переключатели.
Реализация восстановления из резервной копии Определимся с задачей. Нам нужно: 1. Получить имя файла резервной копии, из которой будем делать восстановление. 2. Выбрать необходимые параметры восстановления. 3. Произвести само восстановление. Причем восстанавливать будем в отдельную папку, например C:\DataBases\Restore, которая уже должна существовать на диске. Восстанавливаемой БД перед именем присвоим префикс из даты и времени, как и у резервного копирования, также будем создавать лог-файл. Если восстановление будет успешным, потом администратор сможет переименовать полученную БД и перенести ее в рабочую папку средствами Windows или файловым менеджером. Для восстановления базы данных сделаем новую вкладку. Выделите компонент PC1 (PageControl). Чтобы выделить именно компонент, а не страницу на нем, нужно щелкать правее ярлычка вкладки. Щелкнем по нему правой кнопкой и выберем команду New Page. В свойстве Name новой вкладки 223
вместо TabSheet1 напишем TSh2, а в свойстве Caption – «Восстановление базы». Внешний вид вкладки представлен на рисунке ниже:
Рис. 27.2. Вкладка «Восстановление базы» Здесь у нас будет: Одна простая панель Panel. Три компонента Label. Один Edit. Одна панель GroupBox. Шесть флажков CheckBox. Один SpinEdit. Один ComboBox. Две простых кнопки. Компонент Memo. Компонент IBRestoreService из вкладки InterBase Admin. Компонент предназначен для восстановления базы данных InterBase из резервной копии. Имеет достаточно широкие возможности настроек. Диалог OpenDialog. Сделаем дизайн вкладки, как на рисунке 27.2. Вначале ставим панель Panel, свойство Align = alTop, свойство Caption очищаем, высоту делаем немного меньше половины страницы. На панель сверху устанавливаем Label, Edit и Button. В свойстве Caption компонента Label выводим текст «Выберите резервную копию, из которой будем восстанавливать БД:», свойство Text компонента Edit1 очищаем. Кнопку делаем квадратной, размерами под Edit1: Height = Width = 21. В свойстве Caption помещаем «…». Свойство Name переименуем в bOpenBak. 224
Ниже на панель помещаем панельку GroupBox, в свойстве Caption которой напишем «Параметры восстановления». Внутрь GroupBox1 поместим шесть компонентов CheckBox, которые назовем соответственно CB1…CB6 (сверху - вниз по первой колонке, и сверху – вниз по второй). В свойстве Caption этих флажков поместим соответственно текст: «Отключить индексы» «Отключить теневые копии» «Отключить проверку внеш.ключей» «Записывать данные для таблиц отдельно» «Создать новую БД» «Заполнить страницы на 100%» Подгоните размеры компонентов, как на рисунке. У флажка CB5 свойство Checked переводим в True, так как нам по умолчанию нужно создать новую базу данных. Кроме того, включим еще и флажок CB2, чтобы не восстанавливать теневые копии, если они уже есть. Когда база данных будет восстановлена, ей всегда можно назначить новую теневую копию. А попытка восстановления БД с теневой копией приведет к ошибке, если сначала не удалить этот shadow-файл. Ниже помещаем Label с текстом «Размер буфера страницы:», а рядом помещаем SpinEdit, свойство Name которого переименуем в SE3. В справочнике Delphi размер буфера страницы у компонента IBRestoreService указан как 3000 килобайт. Не будем спорить с производителями Delphi и по умолчанию установим именно этот размер. Для этого сделаем следующие настройки SE3: MinValue = 1000 (минимальное значение) MaxValue = 5000 (максимальное значение) Increment = 100 (шаг приращения/убывания) Value = 3000 (текущее значение) Ниже разместим еще один Label с текстом «Размер страницы:», а правее поместим ComboBox. Переименуем его свойство Name в CBox1, откроем редактор свойства Items и впишем построчно следующие возможные размеры страниц в InterBase: 1024 2048 4096 8192 В свойство Text поместим рекомендованное значение 8192. Правее этих компонентов поместим обычную кнопку, в свойстве Caption которой напишем «Начать восстановление БД», а свойство Enable переведем в False (мы сделаем кнопку доступной, когда пользователь выберет какую-нибудь резервную копию, из которой требуется сделать восстановление). Свойство Name кнопки переименуйте в bStartRestore. Ниже панели Panel поместим Memo (оставим имя по умолчанию Memo2). Свойство Align переведем в alClient, в свойстве ScrollBars выберем ssVertical, чтобы появилась вертикальная прокрутка, а также очистим текст в свойстве Lines. Займемся не визуальными компонентами. OpenDialog переименуем в OD1. Откроем редактор свойства Filter и в столбец Filter Name впишем «Резервные копии БД», а в столбец Filter – «*.gbk». В свойстве InitialDir укажем папку с нашими резервными копиями «C:\DataBases\Backup». Также в свойстве DefaultExt укажем расширение файлов по умолчанию «gbk» (без точки). Перейдем к компоненту IBRestoreService. Для краткости обращений переименуем его свойство Name в IBRS. Как видите, этот компонент также имеет сложное раскрывающееся свойство Options, в котором можно настроить следующие переключатели: Таблица 27.2. Переключатели свойства Options компонента IBRestoreService Переключатель Описание Сделать индексы в базе данных неактивными. DeactivateIndexes При восстановлении БД не восстанавливать ее теневые копии. NoShadow Отключить проверку внешних ограничений (Foreign Key). Делать это нужно NoValidityCheck 225
OneRelationAtATime Replace CreateNew UseAllSpace
осторожно, предварительно сделав копию метаданных. Восстановить метаданные и данные для каждой таблицы по одной записи за раз. Бывает полезно, если обычное восстановление не получается из-за разрушенных данных или нарушенных внешних ограничений. При значении True можно будет восстанавливать копию поверх существующей базы данных. Крайне не рекомендуется. При True создается новый файл базы данных. При восстановлении заполнять страницы базы на 100%, вместо положенных по умолчанию 80%. Чтобы не ухудшить производительность БД, делать это стоит лишь с базами «только для чтения».
Для шести из семи этих переключателей у нас имеются соответствующие компоненты ComboBox, мы опустили только переключатель Replace, так как восстанавливать резервную копию, затирая существующую базу данных, не рекомендуется. Кроме того, имя восстанавливаемой БД, благодаря префиксу, будет уникальным, так что восстановить копию поверх БД все равно не получится. Если программа не предусматривает настройки параметров восстановления, то эти параметры можно было бы указать прямо в свойстве Options компонента. Однако наша программа предусматривает настройку, поэтому параметры восстановления будем устанавливать программно, в зависимости от того, какие CheckBox включены. Займемся кодированием. Сгенерируйте процедуру нажатия на кнопку «…». Ее код: {Щелкнули по кнопке "..."} procedure TfMain.bOpenBakClick(Sender: TObject); begin //если диалог состоялся: if OD1.Execute then begin //укажем выбранный файл Edit1.Text:= OD1.FileName; //делаем доступной кнопку "Начать восстановление БД" bStartRestore.Enabled:= True; end; //if end;
Остается реализовать само восстановление. Сгенерируйте процедуру нажатия на кнопку «Начать восстановление БД». Ее код: {Щелкнули по кнопке "Начать восстановление БД"} procedure TfMain.bStartRestoreClick(Sender: TObject); var st: String; begin //получим префикс для восстановленной БД и лога: st := GetName(); //Очистим Memo2: Memo2.Clear; //устанавливаем начальные параметры: IBRS.ServerName:= 'MyServ'; IBRS.LoginPrompt:= False; IBRS.Params.Add('user_name=sysdba'); IBRS.Params.Add('password=masterkey'); IBRS.Active:= True; //начинаем восстановление try IBRS.Verbose:= True; //вести лог IBRS.Options:= []; //очистим свойство Options //далее мы включаем в Options переключатель, //если его ComboBox включен: if CB1.Checked then IBRS.Options := IBRS.Options + [DeactivateIndexes]; if CB2.Checked then IBRS.Options := IBRS.Options + [NoShadow]; if CB3.Checked then IBRS.Options :=
226
IBRS.Options + [NoValidityCheck]; if CB4.Checked then IBRS.Options := IBRS.Options + [OneRelationAtATime]; if CB5.Checked then IBRS.Options := IBRS.Options + [CreateNewDB]; if CB6.Checked then IBRS.Options := IBRS.Options + [UseAllSpace]; //устанавливаем размер буфера страницы: IBRS.PageBuffers:= SE3.Value; IBRS.PageSize:= StrToInt(CBox1.Text); //размер самой страницы //очищаем свойство с именем БД, которая должна получиться: IBRS.DatabaseName.Clear; //устанавливаем новое имя с префиксом: IBRS.DatabaseName.Add('c:\DataBases\Restore\' + st + 'first.gdb'); //очищаем свойство с именем резервной копии: IBRS.BackupFile.Clear; //устанавливаем новое имя из OpenDialog: IBRS.BackupFile.Add(OD1.FileName); //стартуем восстановление: IBRS.ServiceStart; //параллельно ведем лог, пока не наступит конец восстановления: while not IBRS.Eof do Memo2.Lines.Add(IBRS.GetNextLine); finally IBRS.Active:= False; //деактивируем компонент end; //try //запишем полученный лог в файл: Memo2.Lines.SaveToFile('c:\DataBases\Restore\' + st + 'first.log'); //теперь очистим Edit1 и снова сделаем кнопку недоступной: Edit1.Text:= ''; bStartRestore.Enabled:= False; end;
Как видно из листинга, работа компонента IBRestoreService мало отличается от работы IBBackupService, который мы рассматривали достаточно подробно. Имеется два новых свойства компонента: PageBuffers и PageSize. Первое свойство устанавливает буфер страницы – размер кэша, или размер оперативной памяти, который будет использоваться для считывания данных. Если размер данных окажется больше, чем размер буфера, программа будет обращаться к жесткому диску. В справочнике Delphi в примере работы компонента IBRestoreService указан размер буфера 3000 килобайт. Свойство PageSize устанавливает размер страниц восстанавливаемой базы данных. Как уже говорилось в прошлых лекциях, при восстановлении БД из резервной копии можно изменить размер страниц этой БД. Например, если старая версия БД имела размер 4096, то при восстановлении можно установить размер страниц 8192. Код процедуры подробно комментирован, и в дополнительных пояснениях не нуждается. Сохраните проект, скомпилируйте его и попробуйте сделать восстановление из какой-нибудь резервной копии БД, которую делали ранее. Восстановление закончится, когда в Memo2 появится строка «gbak: finishing, closing, and going home», а в папке C:\DataBases\Restore появятся два файла: восстановленная база данных и лог-файл.
Работа с пользователями Определимся с задачей: 1. Добавление нового пользователя. 2. Изменение данных существующего пользователя (пароль, имя, отчество, фамилия). Если пользователь – SYSDBA, редактирование запрещаем. Ведь при резервном копировании и восстановлении мы жестко задавали имя SYSDBA и пароль masterkey (если вы изменили этот пароль, значит у вас свой вариант). И если администратор изменит этот пароль, программа не сможет работать, вам придется изменить этот пароль в программе и перекомпилировать ее. Впрочем, в дальнейшем вы можете усложнить программу, добавив в нее, например, работу с ini227
файлом (см. лекцию №18 курса «Введение в программирование на Delphi»). В этом случае, вы сможете прописать в ini-файл новый пароль пользователя SYSDBA, если администратор изменит его, и в дальнейшем использовать новый пароль. Если же администратор не будет менять пароля, то можно использовать пароль по умолчанию, «masterkey» (или ваш вариант). 3. Удаление любого пользователя, кроме SYSDBA. Для реализации этой задачи создадим новую вкладку, которую назовем TSh3, а в свойстве Caption напишем «Пользователи». Внешний вид вкладки показан на следующем рисунке:
Рис. 27.3. Вкладка «Пользователи» Здесь мы установили компонент ListBox, свойство Name которого переименовали в LB1, а свойство Align перевели в alLeft, чтобы компонент занял всю левую часть окна. Измените размеры компонента, как на рисунке. В правой части установили три простых кнопки Button. Свойства Name кнопок переименовали в bAddUser, bModifyUser и bDeleteUser, а в свойстве Caption, соответственно, прописали «Добавить», «Редактировать» и «Удалить». Кроме того, мы добавили не визуальный компонент IBSecurityService с вкладки InterBase Admin. Компонент для краткости обращения переименовали в IBSS. Этот компонент предназначен для работы с пользователями, зарегистрированными в InterBase, и позволит нам редактировать, добавлять и удалять пользователей. Несмотря на кажущуюся простоту окна, реализация работы с пользователями немного сложнее предыдущих примеров. В отличие от компонентов IBBackupService и IBRestoreService, компонент IBSecurityService выполняет не одну, а три задачи, что выражено кнопками в правой части окна. Кроме того, IBSecurityService работает не с нашей базой данных first.gdb, а с системной БД InterBase isc4.gdb, что накладывает некоторые ограничения. Например, имя, отчество и фамилию пользователя не получится вносить русскими буквами, так как isc4.gdb создавалась не в кодировке WIN1251. Кроме 228
того, в целях безопасности, компонент IBSecurityService не выводит существующий пароль пользователя, хотя и позволяет менять его. Компонент позволяет оперировать такими данными, как логин пользователя, его пароль, имя, отчество, фамилия, идентификатор пользователя UserID и идентификатор группы GroupID. Для доступа к данным пользователя компонент IBSecurityService имеет свойство UserInfo, которое представляет собой список пользователей. Индекс свойства начинается с 0, как и список строк ListBox. Так как индексы ListBox.Items и IBSecurityService.UserInfo будут совпадать, это сильно облегчит нашу задачу. Первым делом нам нужно при создании главной формы заполнить компонент IBSecurityService свежими данными о пользователях, и обновить этими данными список ListBox.Так как нам то же самое придется делать при добавлении, редактировании и удалении пользователей, в разделе private добавим новую процедуру ReloadUsers: private { Private declarations } function GetName():String; procedure BackupCopy; procedure ReloadUsers;
Реализация этой процедуры: {Перечитываем данные о пользователях} procedure TfMain.ReloadUsers; var i: Integer; //для счетчика begin //вначале очистим ListBox: LB1.Clear; //заполняем список пользователями: IBSS.ServerName:= 'MyServ'; IBSS.LoginPrompt:= False; IBSS.Params.Add('user_name=sysdba'); IBSS.Params.Add('password=masterkey'); IBSS.Active:= True; try IBSS.DisplayUsers; //получаем информацию о пользователях for i:=0 to IBSS.UserInfoCount -1 do LB1.Items.Add(IBSS.UserInfo[i].UserName); finally IBSS.Active:= False; end; //try end;
Здесь все достаточно прозрачно. Вначале очищаем ListBox от возможных старых записей. Затем настраиваем сервис IBSecurityService, так же, как ранее настраивали компоненты IBRestoreService и IBBackupService. Метод DisplayUsers получает в компонент всю информацию о пользователях, после чего она доступна в свойстве UserInfo. Свойство UserInfoCount показывает общее количество пользователей. Подсвойство UserName свойства UserInfo содержит логин текущего пользователя. Строкой LB1.Items.Add(IBSS.UserInfo[i].UserName);
мы добавляем в ListBox имя очередного пользователя. Теперь нужно вызвать эту процедуру при создании формы. Для этого сгенерируйте для главной формы событие OnCreate, в котором сделаем вызов этой процедуры: ReloadUsers;
Проверить результат можно, скомпилировав и загрузив программу. При загрузке, в окне ListBox должны отобразиться зарегистрированные пользователи. 229
Далее нам придется сделать новую форму – редактор данных пользователей. Выберите команду File -> New -> Form. Форму назовите fEditor, в свойстве Caption напишите «Редактирование данных пользователя», сохраните новый модуль под именем Editor. Свойство BorderStyle формы переведем в bsDialog, чтобы пользователь не мог менять размеры окна, а в свойстве Position выберем poMainFormCenter. Внешний вид новой формы показан на рисунке ниже:
Рис. 27.4. Форма fEditor. Итак, у нас имеется: Два компонента GroupBox. Шесть компонентов Label. Шесть компонентов Edit. Две кнопки Button. В свойстве Caption первого GroupBox напишем «Обязательные данные», а второго – «Дополнительные данные». На верхний GroupBox помещаем три Label и заполняем текст, как на рисунке. Затем помещаем три Edit. У первого свойство Name переименуем в eUser, у второго – ePass, и у третьего – ePass2. Выделите одновременно (с нажатой Shift) компоненты ePass и ePass2. В свойстве PasswordChar укажите символ «*», чтобы скрыть реальный пароль. Не забудьте удалить текст из свойства Text всех компонентов Edit. На нижний GroupBox также помещаем три Label и заполняем текст, как на рисунке. Затем помещаем три Edit. У первого свойство Name переименуем в eName, у второго – eMiddle, и у третьего – eLast. Очищаем свойство Text у всех Edit. Ниже расположим две кнопки, которые переименуем соответственно, в bAccept и bCancel, а свойства Caption – как на рисунке. Теперь немного подумаем. Эта форма будет показываться в двух случаях: при добавлении нового пользователя, и при редактировании существующего. В первом случае пользователь еще не имеет пароля, во втором – имеет. Причем при редактировании существующего пользователя, администратор может как сменить старый пароль, так и не трогать его. Как узнать, менялся ли пароль существующего пользователя, если мы не имеем возможности вывести его с помощью IBSecurityService? Вариант: при редактировании существующего пользователя поместим в ePass и ePass2 какой-нибудь длинный пароль «по умолчанию», например, двадцать единичек. А при сохранении результата редактирования будем смотреть – если в ePass пароль «по умолчанию», значит сохранять пароль не нужно. В случае же добавления нового пользователя ePass и ePass2 заполняться не будут. Пойдем дальше. Как уже говорилось, база данных isc4.gdb не поддерживает кодировку win1251, следовательно, русские буквы в нее помещать нельзя. Значит, при вводе текста во все шесть 230
компонентов Edit придется делать проверку – что вводит пользователь. Если английские буквы или цифры, или BackSpace, то ничего не делаем, иначе запрещаем символ. Чтобы шесть раз не писать один и тот же код, в разделе private создадим функцию KeyCan: private { Private declarations } function KeyCan(c:Char):Boolean;
Код реализации функции представлен ниже: {Проверяем допустимость символа} function TfEditor.KeyCan(c: Char): Boolean; begin case c of 'A'..'z': Result:= True; //англ. буквы разрешаем '0'..'9': Result:= True; //цифры разрешаем #8 : Result:= True; //BackSpace разрешаем else Result:= False; //остальное запрещаем end; //case end;
Теперь нам нужно реализовать проверку во всех шести Edit. Выделите первый, и сгенерируйте для него событие OnKeyPress. В процедуру поместите только одну строку: if not KeyCan(Key) then Key:= #0;
То же самое проделайте с оставшимися пятью Edit. Таким образом, мы реализовали проверку на ввод допустимых символов. Если пользователь попытается ввести что-нибудь, кроме английских букв, цифр или BackSpace, то его действия будут игнорироваться. Пойдем дальше. Если мы редактируем нового пользователя, то в ePass и ePass2 у нас будет текст в двадцать единиц. Если администратор захочет изменить пароль, то он изменит текст в ePass. Значит, для ePass нужно сгенерировать событие OnChange, в котором очищаем текст у ePass2: ePass2.Text:= '';
Теперь подумаем вот о чем. Окно редактора пользователей мы будем вызывать из главной формы, там же мы будем сохранять изменения, если администратор нажмет кнопку «Подтвердить» в окне редактора. А как из главной формы узнать – хочет ли администратор сохранить изменения, или нет? Введем глобальную переменную izmen в модуль редактора пользователей: var fEditor: TfEditor; izmen: Boolean;
Теперь для формы редактора пользователей сгенерируем событие OnShow, в котором сразу пропишем: izmen:= False;
В событии нажатия на кнопку «Подтвердить» вписываем код: izmen:= True; Close;
А при нажатии на кнопку «Отменить» просто закрываем форму: Close;
Теперь, если администратор закроет эту форму иначе, чем нажатием на «Подтвердить», мы ничего предпринимать не будем. Осталось сделать проверки на то, ввел ли администратор логин нового 231
пользователя, совпадают ли пароли в ePass и ePass2? Для этого сгенерируем событие OnClose для формы редактора: {при закрытии формы} procedure TfEditor.FormClose(Sender: TObject; var Action: TCloseAction); begin //если изменения не нужно делать, просто выходим: if not izmen then Exit; //Иначе делаем проверку. Если нет имени пользователя: if eUser.Text = '' then begin ShowMessage('Введите имя пользователя!'); Action := caNone; //запрещаем покидать форму eUser.SetFocus; //переводим фокус на имя пользователя end; //Если пароль не '11111111111111111111', делаем проверку //совпадает ли ePass и ePass2: if ePass.Text <> '11111111111111111111' then if ePass.Text <> ePass2.Text then begin ShowMessage('Введите правильный пароль!'); Action := caNone; //запрещаем покидать форму //очистим ePass и ePass2: ePass.Text:= ''; ePass2.Text:= ''; ePass.SetFocus; //переводим фокус на имя пользователя end; end;
Комментарии достаточно подробны, чтобы вы смогли разобраться с кодом. С этой формой закончили, вернемся на главную форму. Не забудем сразу же командой File -> Use Unit подключить к ней модуль Editor.
Добавление нового пользователя Реализуем добавление нового пользователя. Сгенерируйте обработчик нажатия на кнопку «Добавить». Код обработчика следующий: {Нажали "Добавить"} procedure TfMain.bAddUserClick(Sender: TObject); begin //вначале очистим данные, которые могут быть //в редакторе пользователей: fEditor.eUser.Text:= ''; fEditor.ePass.Text:= ''; fEditor.ePass2.Text:= ''; fEditor.eName.Text:= ''; fEditor.eMiddle.Text:= ''; fEditor.eLast.Text:= ''; //теперь показываем форму: fEditor.ShowModal; //если изменений делать не нужно, просто выходим: if not Editor.izmen then Exit; //иначе готовим компонент IBSS к созданию нового пользователя. IBSS.Active:= True; try //теперь вводим данные нового пользователя: IBSS.UserName:= fEditor.eUser.Text; IBSS.Password:= fEditor.ePass.Text; IBSS.FirstName:= fEditor.eName.Text; IBSS.MiddleName:= fEditor.eMiddle.Text; IBSS.LastName:= fEditor.eLast.Text; //вызываем метод AddUser, который добавляет пользователя: IBSS.AddUser;
232
finally IBSS.Active:= False; //закрываем компонент end; //try //перечитываем информацию о пользователях: ReloadUsers; end;
Поскольку пользователь у нас новый, данных на него пока никаких нет. Значит, вначале мы очистим все Edit на форме редактора, и только потом покажем ее. Когда администратор закончит работу с формой, и закроет ее, начинает работу следующая строка процедуры: //если изменений делать не нужно, просто выходим: if not Editor.izmen then Exit;
То есть, если администратор не нажал кнопку «Подтвердить», мы просто выйдем, ничего не делая. Если же процедура работает дальше, то нужно сохранить нового пользователя: IBSS.Active:= True; try //теперь вводим данные нового пользователя: IBSS.UserName:= fEditor.eUser.Text; IBSS.Password:= fEditor.ePass.Text; IBSS.FirstName:= fEditor.eName.Text; IBSS.MiddleName:= fEditor.eMiddle.Text; IBSS.LastName:= fEditor.eLast.Text; //вызываем метод AddUser, который добавляет пользователя: IBSS.AddUser; . . .
Как видите, при сохранении данных мы используем свойства компонента IBSecurityService UserName (логин), Password (пароль), FirstName (имя), MiddleName (отчество) и LastName (фамилия). Есть еще два свойства, которые мы не использовали, и которые устанавливаются по умолчанию в ноль: UserID (идентификатор пользователя) и GroupID (идентификатор группы), обычно программисты не задействуют эти параметры. Физически новый пользователь добавляется методом AddUser. В заключение процедуры мы отключаем IBSecurityService и вызываем процедуру ReloadUsers для обновления данных. Сохраните проект, скомпилируйте его и попробуйте добавить нового пользователя. Пользователь должен появиться в окне ListBox, кроме того, он должен быть виден и в утилите IBConsole, в разделе Users дерева серверов.
Редактирование пользователя Редактирование выбранного пользователя вызывается кнопкой «Редактировать». Сгенерируйте этот обработчик. Вот его код: {Нажали "Редактировать"} procedure TfMain.bModifyUserClick(Sender: TObject); var i: Integer; //счетчик begin //если не один пользователь не выбран, просто выходим: if LB1.ItemIndex = -1 then begin ShowMessage('Выберите пользователя!'); Exit; end //если выбран SYSDBA, тоже выходим: else if LB1.Items[LB1.ItemIndex] = 'SYSDBA' then begin ShowMessage('Редактировать пользователя SYSDBA нельзя.'); Exit; end; //else if
233
//иначе редактируем //установим у IBSecurityService выбранного пользователя: IBSS.Active:= True; IBSS.UserName:= LB1.Items[LB1.ItemIndex]; //найдем в IBSS индекс нужного пользователя: for i:= 0 to IBSS.UserInfoCount - 1 do if IBSS.UserInfo[i].UserName = LB1.Items[LB1.ItemIndex] then break; //теперь i содержит индекс пользователя //заполняем редактор пользователей данными: fEditor.eUser.Text:= IBSS.UserInfo[i].UserName;; //раз пользователь уже есть, выводим единички в качестве пароля: fEditor.ePass.Text:= '11111111111111111111'; fEditor.ePass2.Text:= '11111111111111111111'; fEditor.eName.Text:= IBSS.UserInfo[i].FirstName; fEditor.eMiddle.Text:= IBSS.UserInfo[i].MiddleName; fEditor.eLast.Text:= IBSS.UserInfo[i].LastName; //теперь покажем редактор: fEditor.ShowModal; //если изменений делать не нужно, просто выходим: if not Editor.izmen then Exit; //иначе сохраняем их: try //Имя пользователя менять не будем. //Сохраним пароль, если там не единички: if fEditor.ePass.Text <> '11111111111111111111' then IBSS.Password:= fEditor.ePass.Text; //Далее сохраняем остальные параметры: IBSS.FirstName:= fEditor.eName.Text; IBSS.MiddleName:= fEditor.eMiddle.Text; IBSS.LastName:= fEditor.eLast.Text; //сохраняем изменения физически методом ModifyUser: IBSS.ModifyUser; finally IBSS.Active:= False; end; //try //перечитываем информацию о пользователях: ReloadUsers; end;
Подробно разбирать весь код не будем, так как комментариев достаточно, но остановимся на некоторых моментах. Строка IBSS.UserName:= LB1.Items[LB1.ItemIndex];
делает в компоненте IBSS текущим пользователем того, который в данный момент выбран в списке ListBox. Затем мы находим индекс этого пользователя в списке. Зачем это нужно? Дело в том, что данные о пользователе можно вытащить только через свойство UserInfo. Другими словами, если мы хотим посмотреть отчество пользователя, то IBSS.MiddleName вернет пустую строку, даже если отчество есть, а IBSS.UserInfo[i].MiddleName вернет отчество. При сохранении отчества нужно напротив, обращаться к IBSS.MiddleName. Физическое изменение данных реализуется методом ModifyUser. Сохраните проект, скомпилируйте его и попробуйте отредактировать какого либо существующего пользователя, например, добавив к нему дополнительные данные (фамилию, имя, отчество). После сохранения результата, в утилите IBConsole должны отобразиться эти данные.
234
Удаление пользователя Это самая простая операция с IBSecurityService. Ее код: {Удаление пользователя} procedure TfMain.bDeleteUserClick(Sender: TObject); var s: String; //для формирования строки begin //если не один пользователь не выбран, просто выходим: if LB1.ItemIndex = -1 then begin ShowMessage('Выберите пользователя!'); Exit; end; //иначе продолжаем. формируем запрос: s:= 'Вы действительно желаете удалить пользователя ' + LB1.Items[LB1.ItemIndex] + '?'; //попросим подтверждения. выходим, если не подтвердили: if Application.MessageBox(PChar(s), 'Удаление пользователя', MB_YESNOCANCEL+MB_ICONQUESTION) <> IDYES then Exit; //если не вышли, значит удаляем пользователя IBSS.Active:= True; try //делаем пользователя текущим: IBSS.UserName:= LB1.Items[LB1.ItemIndex]; //удаляем его методом DeleteUser: IBSS.DeleteUser; finally IBSS.Active:= False; end; //try //перечитываем информацию о пользователях: ReloadUsers; end;
Тут все почти так же, как в предыдущих примерах, за исключением нового метода DeleteUser, который физически удаляет пользователя. Перед окончательной компиляцией программы не забудьте сделать текущей вкладку «Резервное копирование базы данных», чтобы именно она открывалась при загрузке программы. Вкладка InterBase Admin Палитры компонентов содержит еще целый ряд сервисных компонентов. Однако в рамках лекции просто невозможно разобрать их все, мы рассмотрели работу только основных компонентов. Остальные работают похожим образом, вы сможете самостоятельно разобраться с ними, изучив справку Delphi по компонентам.
235
Лекция 28. Многоуровневая архитектура. В последнее время многоуровневая (multitier) архитектура пользуется все большей популярностью, поскольку имеет массу преимуществ перед файл-серверными или клиент-серверными приложениями. Такая архитектура в различных публикациях также называется многозвенной, или распределенной архитектурой. Суть многоуровневой архитектуры в том, что помимо сервера БД и приложений-клиентов дополнительно присутствует еще один или несколько серверов приложений. Сервер приложений является промежуточным уровнем, обеспечивающим организацию взаимодействия клиентов и сервера БД. Сервер приложений также называют брокером данных (broker - посредник). Чаще всего используют трехуровневую модель. Прежде, чем мы двинемся дальше, давайте разберемся, что же такое уровень. Имеется три основных уровня: Уровень данных. Этот уровень отвечает за хранение данных. Как правило, для этого уровня выделяется отдельный ПК, на котором устанавливают один из SQL-серверов, например, InterBase. Клиентские ПК непосредственно не имеют никакой связи с этим уровнем. Бизнес-уровень. Этот уровень предназначен для получения данных с уровня данных, выполнения окончательной проверки данных, и служит посредником между клиентами и данными. Как правило, сервера приложений находятся именно на этом уровне. Уровень представления данных. Этот уровень известен так же, как уровень графического интерфейса пользователя. На этом уровне полученные данные отображаются в таких компонентах вывода данных, как DBGrid, DBEdit, DBMemo и проч. Разумеется, этот уровень находится на клиентских ПК. Взгляните на рисунок:
Рис. 28.1. Трехуровневая модель На рисунке представлены три уровня архитектуры: так называемый, «тонкий» клиент (thinclient), сервер приложений и сервер данных. На ПК сервера БД вместе с данными расположен один из SQL-серверов. Как видите, на клиентском ПК, помимо компонентов доступа к данным, располагается только компонент связи с сервером приложений, а довольно громоздкие механизмы доступа к данным отсутствуют. Из-за этого клиентское приложение и называется «тонким» клиентом. Такой подход не только облегчает распространение приложений, но и позволяет в качестве клиентских ПК использовать дешевые, неприхотливые компьютеры. На сервере приложений вы можете видеть область, обозначенную как «Интерфейс IAppServer». Этот интерфейс обеспечивается специальным удаленным модулем данных, о котором дальше мы поговорим подробней. Интерфейс IAppServer используют компоненты-провайдеры TDataSetProvider на стороне сервера приложений, и компоненты TClientDataSet на стороне клиента. При небольшом количестве клиентов ничто не мешает нам отказаться от использования SQLсервера, расположив данные на самом сервере приложений, и используя к ним обычный локальный доступ через механизм BDE, ADO и т.п. При этом можно использовать обычные таблицы Paradox или MS Access, например. Такой подход много удобней, чем файл-серверная архитектура, поскольку позволяет не только «облегчить» пользовательское приложение, но и обеспечивает безопасность данных. Ведь пользователи не будут иметь прямого доступа к самим данным, обмен информацией будет происходить через посредника, как на рисунке ниже:
236
Рис. 28.2. Объединение уровня данных и бизнес-уровня на одном сервере Несмотря на кажущуюся сложность архитектуры, организовать такую модель достаточно просто. Delphi для этого предлагает технологию DataSnap (в старых версиях Delphi эта технология называлась MIDAS – Multi-tier Distributed Applications Services – Серверы многозвенных распределенных приложений). Благодаря этой технологии, вы сможете создать простой сервер приложений буквально за минуту, не введя ни сточки кода.
Преимущества многоуровневых моделей
Централизованная бизнес-логика. Как мы уже знаем, бизнес-логикой называют некоторые правила обработки данных. Например, удаляя запись из одной таблицы, требуется удалить связанные с ней записи других таблиц. В обычных файл-серверных или клиент-серверных приложениях, часть бизнес-логики ложилась на клиентское приложение. При необходимости изменения каких-то правил, приходилось переделывать клиентское приложение, затем тратить усилия на его распространение на клиентские ПК. Теперь же эта бизнес-логика хранится на уровне сервера приложений, и при ее изменении клиенты сразу получают возможность работать по новым правилам. Архитектура «тонкого» клиента. Как известно, для получения данных из БД приложения используют один из механизмов доступа к данным – BDE, ADO, ODBC, IBX, dbExpress и т.п. Все это приводит к тому, что на ПК каждого клиента приходится устанавливать и настраивать соответствующие драйверы, а это сильно усложняет процесс распространения приложения. В многозвенной архитектуре механизмы доступа к данным располагаются на сервере приложений. Только там нужно устанавливать эти драйверы (например, BDE). Клиентские же машины не нуждаются в них, за что и называются «тонкими». Модель «портфеля» (briefcase model). Модель «портфеля» подразумевает возможность отложенной обработки данных. Представьте, что вам в выходные дни требуется проделать какую-то работу с данными. При использовании файл-серверной или клиент-серверной модели, вам для этой работы придется прибыть в офис, иначе вы не сможете получить доступа к данным. В многоуровневой модели вы можете сохранить на переносном ПК все необходимые вам данные в виде локального файла. Дома вы сможете загрузить этот файл, и провести необходимую работу с данными. Затем, прибыв в офис, вы сможете перенести эти изменения в реальную базу данных. Снижение трафика сети. За счет возможности отложенной обработки данных значительно снижается нагрузка на сеть.
Сервер приложений Сервер приложений нам придется создавать самостоятельно. Давайте познакомимся с созданием такого сервера на практике, попутно разбирая новый материал. Для примера мы создадим сервер приложений, работающий с базой данных ok.mdb, которую мы создавали во второй лекции (надеюсь, вы еще не удалили эту БД?). Поместим этот файл по адресу C:\DataBases\ok.mdb Delphi поддерживает следующие технологии удаленного доступа: 237
DCOM (Distributed Component Object Model – Распределенная компонентная модель объектов) Модель рассчитана на локальную сеть и позволяет использовать объекты, расположенные на другом ПК. Если клиентское приложение работает под управлением Windows 95 (что маловероятно в настоящее время), то придется также установить поддержку DCOM95, остальные версии Windows в этом не нуждаются. Поскольку модель является «родной» для ОС Windows, использовать ее довольно просто. Вероятно, наиболее популярная модель на сегодняшний день. Сокеты – позволяют использовать сеть по протоколу TCP/IP. Эта модель, пожалуй, дает наиболее быстрое соединение, однако имеется ряд замечаний. Во-первых, программисту приходится прилагать дополнительные усилия для организации связи и слежением за возможными ошибками. Во-вторых, чтобы можно было загрузить сервер приложений, сначала на компьютере с этим сервером нужно загрузить утилиту SCKTSRVR.EXE. Эта утилита устанавливается вместе с Delphi и по умолчанию находится в папке C:\PROGRAM FILES\BORLAND\DELPHI\BIN. При загрузке программы ее ярлык появляется в трее (в правом нижнем углу экрана), и с этого момента клиентские ПК смогут соединяться с этим сервером. Обычно запуск этой утилиты прописывают на сервере в автозагрузке. MTS (Microsoft Transaction Server – Сервер транзакций Microsoft) – основана на DCOM и имеет некоторые дополнительные возможности. CORBA (Common Object Request Broker Architecture – Общедоступная архитектура брокеров при запросе объектов). SOAP (Simple Object Access Protocol – Простой протокол доступа к объекту.)
Загрузите Delphi и начните новый проект. Главная форма нам совсем не понадобится, назовите ее fMain, в свойстве Caption напишите «Сервер приложений», уменьшите ее размер (например, Height=120, Width=250) и сохраните в отдельную папку, дав модулю имя Main, а проекту в целом – MyNewServer. Основой сервера является удаленный модуль данных, который обеспечивает связь сервера с клиентами, а также является контейнером для размещения компонентов, вроде обычного Data Module. Delphi позволяет использовать следующие удаленные модули:
Remote Data Module – используется для серверов DCOM, сокетов и OLEnterprise; Transactional Data Module – используется для сервера MTS; CORBA Data Module – для сервера CORBA; SOAP Data Module – для сервера SOAP; WebSnap Data Module – использует Web-службы и Web-броузер в качестве сервера.
Помимо одного из этих удаленных модулей данных, в состав сервера приложений также входят компоненты TDataSetProvider, которые предназначены для передачи данных на клиентское приложение. Каждому набору данных (таблица, запрос), предназначенному для передачи клиентам, следует предоставить по одному компоненту TDataSetProvider. Важно! Следует знать, что обмен данными между сервером приложений и «тонкими» клиентами обеспечивается динамической библиотекой Midas.dll, которая должна быть зарегистрирована на компьютере сервера приложений. Мы будем использовать технологию DCOM, поэтому выберите команду меню File -> New -> Other, чтобы открыть окно депозитария Delphi. Перейдите на вкладку Multitier и выберите Remote Data Module. Откроется окно мастера создания удаленного модуля данных:
238
Рис. 28.3. Мастер создания удаленного модуля данных В первом поле «CoClass Name» нам необходимо ввести имя создаваемого модуля, назовем его MyRDM. Следующие два поля требуют более детального изучения. Поле Instancing требует выбора способа создания экземпляров сервера, когда клиент пытается получить доступ к данным. Заметим, что Windows автоматически загружает сервер приложений, когда начинает работать клиент. Возможны следующие способы загрузки сервера:
Internal – при такой модели сервер COM не сможет создаваться из внешних приложений. Используется редко, в основном, когда нужно управлять доступом с помощью промежуточного уровня прокси. Single Instance – при выборе этой модели для каждого клиентского соединения будет создан свой экземпляр сервера. Multiple Instance – в этой модели все клиентские соединения используют единый экземпляр сервера.
В этом поле оставляем способ по умолчанию Multiple Instance. Поле Threading Model предлагает выбрать модель потоков, что позволяет распределять соединения по отдельным потокам без необходимости применения дополнительного кода. Допустимы следующие модели:
Single (Одиночная) – для клиентов выделяется один поток, все клиенты работают последовательно. Выбор такой модели может оказаться неудачным в многопользовательской среде, и используется редко. Apartment (Раздельная) – для каждого клиента создается собственный поток. В сочетании с Multiple Instance этот способ дает самые высокие результаты и наиболее часто применяется. Free (Свободная) – один экземпляр модуля данных может одновременно отвечать на несколько запросов клиентов, используя разные потоки. Both (Оба) – объединяет модели Free и Apartment. Neutral (Нейтральный) – разные клиенты могут одновременно вызвать удаленный модуль данных из нескольких потоков, при этом модель COM следит, чтобы не было конфликта вызовов. Однако может возникнуть конфликт потоков, который отслеживается только в версии COM+. При отсутствии этой версии нужно использовать модель Apartment.
В этом поле оставляем модель по умолчанию Apartment. Поясним материал на схеме:
239
Рис. 28.4. Поведение сервера в зависимости от способа создания экземпляров Нажмите кнопку «ОК», окно Мастера создания удаленного модуля закроется, и у нас появится модуль данных MyRDM. Сохраните его на диск под именем RDM. В этот контейнер с вкладки ADO поместите компонент ADOConnection и две таблицы ADOTable. Щелкните дважды по ADOConnection, чтобы открыть редактор подключений. Нажмите кнопку Build, чтобы открылся список поставщиков данных. Здесь выберем «Microsoft Jet 4.0 OLE DB Provider» и нажмем копку «Далее». В следующем окне, в поле 1 поместим адрес и имя файла (вы уже поместили файл по этому адресу?): C:\DataBases\ok.mdb Как видите, локальную базу данных мы превращаем в распределенную, предназначенную для подключения многих клиентов, что гораздо удобней и надежней файл-серверной архитектуры. Далее нажимаем «ОК» и закрываем редактор подключений. Свойство LoginPrompt компонента переведем в False, чтобы при подключении не выходило запроса на имя пользователя и пароль. А свойство Connected переведем в True, чтобы физически подключить компонент к базе данных. Займемся табличными компонентами. В обеих ADOTable в свойстве Connection выберите наш ADOConnection1. У первой таблицы в свойстве TableName выберите таблицу LichData, а свойство Name переименуйте в TLichData. У второй таблицы в свойстве TableName выберите таблицу Telephones, а свойство Name переименуйте в TTelephones. Свойство Active обеих таблиц переведите в True, чтобы открыть эти наборы данных. Как видите, пока что подключение сервера к базе данных мало отличается от того, что мы проходили во второй лекции, при разработке локальной базы данных для отдела кадров. Однако дальше начинаются различия. Как вы помните, посредником между НД и компонентами отображения данных является компонент DataSource. Однако в серверном приложении у нас нет компонентов отображения данных, поэтому компоненты DataSource для этого нам не нужны. Однако нам нужно будет организовать связь таблиц один-ко-многим, а для этого один DataSource нам все-таки понадобится. Также нам понадобится посредник для связи с клиентским приложением, и этим посредником является компонент DataSetProvider, который находится на вкладке Data Access Палитры компонентов. Такой компонент требуется устанавливать к каждому набору данных (Table или Query), с которым будет соединяться клиентское приложение. Установите в контейнер MyRDM два таких компонента. У 240
первого свойство Name переименуйте в DSPLichData, а в свойстве DataSet выберите таблицу TLichData. У второго свойство Name переименуйте в DSPTelephones, а в свойстве DataSet выберите таблицу TTelephones. Теперь установим связь главная-подчиненная между таблицами, для этого установите один компонент DataSource. Его свойство Name переименуйте в dsLichData, а в свойстве DataSet выберите таблицу TLichData. Организация связи производится в таблице TTelephones. В ее свойстве MasterSource выберите dsLichData, затем раскройте сложное свойство MasterFields. Откроется окно редактора связи:
Рис. 28.5. Редактор связей В разделе подчиненного поля выберите «Сотрудник», в разделе главного поля – «Ключ», нажмите кнопку Add, чтобы создать связь, и кнопку OK, чтобы подтвердить это. Внешний вид полученного контейнера представлен на рисунке ниже:
Рис. 28.6.Удаленный модуль данных Это все, сервер приложений готов. Как видите, даже не пришлось подключать удаленный модуль данных к главной форме командой File -> Use Unit. Сохраните проект и командой Run -> Run (или кнопкой Run на панели инструментов) скомпилируйте и загрузите полученную программу. При этом произошли две вещи: проект скомпилировался в выполняемую программу, а при первом запуске наш сервер зарегистрировался в реестре Windows. Теперь Windows знает, где находится наш сервер, и при необходимости сможет его автоматически загрузить. К слову сказать, при переносе серверного файла на другой ПК нет необходимости даже загружать эту программу, чтобы прописать ее в реестре Windows, достаточно из командной строки загрузить ее с параметром /regserver, при этом программа лишь пропишется в реестре и сразу отключится. А для 241
удаления регистрации этого сервера из реестра нужно загрузить его с параметром /unregserver, например, так (у вас может быть другой адрес): C:\ MyNewServer\ MyNewServer.exe /unregserver Однако не забудьте, что для дальнейшей правильной работы он должен быть прописан в реестре Windows. Поэтому если сейчас вы удалили сервер из реестра, вновь загрузите программу MyNewServer.exe, чтобы заново прописать ее. Как уже упоминалось, для правильной работы серверов DCOM на серверном ПК должна быть установлена и зарегистрирована библиотека midas.dll. В нашем случае этого делать не нужно, так как при установке Delphi библиотека устанавливается и регистрируется автоматически. Однако если вы будете использовать созданный сервер приложений на другом ПК, где не устанавливалась Delphi, то зарегистрировать библиотеку придется вручную. Для этого на вашем ПК нужно найти файл библиотеки, он устанавливается по адресу (для Windows XP): C:\Windows\System32 Этот файл нужно скопировать на ПК, который вы собираетесь использовать в качестве сервера, по этому же адресу. В этой же папке находится утилита regsvr32.exe, которая предназначена для регистрации библиотек *.dll. Чтобы зарегистрировать нашу библиотеку, надо из командной строки (или в окне команды Пуск -> Выполнить) вызвать утилиту, передав ей в качестве параметра имя библиотеки: C:\Windows\System32\regsvr32 midas.dll Таким образом, мы зарегистрируем библиотеку в реестре. Снять регистрацию можно командой: C:\Windows\System32\regsvr32 /u midas.dll Не снимайте регистрацию на вашем ПК, иначе потом вы не сможете загрузить сервер приложений! Если в качестве источника данных вы будете использовать сервер InterBase, то набор компонентов в удаленном модуле данных, скорее всего, будет с вкладки InterBase, а их подключение к базе данных будет таким же, как мы изучали в прошлых лекциях. Связь наборов данных сервера с клиентским приложением обеспечивается компонентом DataSetProvider, рассмотрим некоторые полезные свойства и методы этого компонента. Таблица 28.1. Свойства компонента DataSetProvider Свойство Описание Если содержит True, клиенту пересылается информация о наложенных на данные ограничениях. Клиент имеет возможность организовать локальный контроль Constraints данных. Содержит имя связанного с компонентом набора данных (TTable, TQuery, DataSet TStoredProc) При значении True клиент имеет возможность использовать интерфейс IAppServer Exported при обращении к провайдеру. Сложное раскрывающееся свойство. Определяет, какие данные будут включены в передаваемый клиенту пакет. Подробнее о параметрах этого свойства смотрите Options следующую таблицу. Определяет, как обновляются данные. Если содержит True – то в НД, указанном в ResolveToDataSet свойстве DataSet. Иначе – непосредственно в серверной БД. Определяет критерии поиска записи в наборе данных при сохранении изменений этой записи клиентом. Если значение upWhereAll, поиск записи ведется по всем UpdateMode полям; если upWhereChanged – по ключевым и измененным полям; если upWhereKeyOnly – только по ключевым полям. 242
Таблица 28.2. Параметры свойства Options компонента DataSetProvider Параметр Описание По умолчанию, данные из BLOB полей клиенту не пересылаются, чтобы излишне не загружать трафик. Если свойство содержит True – данные пересылаются. Иначе, эта возможность отключена, и чтобы poFetchBlobsOnDemand получить BLOB-данные записи, клиентское приложение должно явно использовать метод FetchBlobs. Данные из вложенных или подчиненных таблиц не включаются в пакет. Чтобы их получить, клиентское приложение использует метод poFetchDetailsOnDemand FetchDetails. Если свойство имеет значение True, то эти данные включаются в пакет автоматически. В пакет включаются такие свойства полей, как Alignment, DisplayLabel, DisplayWidth, Visible, DisplayFormat, EditFormat, MaxValue, MinValue, poIncFieldProps Currency, EditMask, DisplayValues. Дает серверу распоряжение автоматически удалять каскадным методом записи из подчиненных таблиц, если пользователь удалил связанную с poCascadeDeletes ними запись в главной таблице. Дает серверу распоряжение автоматически изменять каскадным методом записи из подчиненных таблиц, если пользователь изменил poCascadeUpdates связанную с ними запись в главной таблице. Данные клиенту предоставляются только для чтения. poReadOnly Параметр допускает индивидуальные обновления сразу нескольких poAllowMultiRecordUpdates записей. Если параметр = False, множественные обновления будут автоматически прерваны. Параметр запрещает клиенту вставку новых записей. poDisableInserts Параметр запрещает клиенту редактирование записей. poDisableEdits Параметр запрещает клиенту удаление записей. poDisableDeletes Запрещает обновление набора данных сервера перед передачей записей poNoReset клиенту. Разрешает автоматическое обновление записей клиента при их poAutoRefresh изменении. Для ускорения работы эта опция по умолчанию отключена. Обновления, сделанные в событиях BeforeUpdateRecord или AfterUpdateRecord передаются клиенту в пакете, и объединяются с poPropogateChanges клиентским набором данных. Предоставляет клиенту возможность отменить НД, заменяя его НД, poAllowCommandText полученным SQL-запросом. Запрещает клиенту изменять сортировку записей, полученную по poRetainServerOrder умолчанию. Таблица 28.3. Некоторые полезные методы компонента DataSetProvider Метод Описание Обновляет данные. Имеет параметры: Delta – измененные, новые или удаленные записи; MaxError – максимальное количество ошибок, при котором обновление ApplyUpdates прекращается (0 – не ограничено); ErrorCount – количество допущенных ошибок. Метод возвращает клиенту набор записей, при обновлении которых произошла ошибка. Вызывается для каждой записи, которая должна быть удалена. DoDelete Вызывается для каждой новой записи. DoInsert Вызывается для каждой модифицированной записи. DoUpdate Вызывается в момент завершения обновлений. EndUpdate Добавляет ошибочную запись в журнал ошибок, который затем передается LogUpdateError клиентскому приложению. На следующей лекции мы подробно разберем создание клиентской части многоуровневых приложений. 243
Лекция 29. Многоуровневая архитектура. Создание «тонкого» клиента. На прошлой лекции мы рассматривали создание сервера приложений на основе удаленного модуля данных Remote Data Module. Эта лекция посвящена созданию клиентской части многоуровневых приложений на основе технологии DCOM. Связать клиентское приложение с одной таблицей очень просто. Связать две или более таблицы в отношении один-ко-многим несколько сложней, тут есть свои тонкости. Именно такую связь мы и будем делать. Кроме того, мы разберем способ реализации отложенной обработки, когда данные можно сохранить в файл, и при необходимости, загрузить их из файла без соединения с удаленным сервером приложений. Связь клиентского приложения с удаленным модулем данных организуется одним из компонентов связи, расположенных на вкладке DataSnap Палитры компонентов, а для связи с наборами данных используется компонент ClientDataSet, который находится на вкладке Data Access Палитры компонентов. Каждый ClientDataSet соединяется с одним набором данных (таблица, запрос). Разберем новый материал на практике, попутно останавливаясь на важных моментах.
Создание клиентского приложения Начните новый проект. В свойстве Name главной формы напишите fMain, в свойстве Caption напишите «Клиент удаленного сервера», сохраните ее модуль под именем Main, а проект в целом – MyNewClient. На форму поместите три простых панели, сразу же очистив их свойство Caption. У верхней панели в свойстве Align выберите alTop, а в свойстве AutoSize установите True. Поместите на эту панель навигатор DBNavigator с вкладки Data Controls Палитры компонентов. У нижней панели в свойстве Align выберите alBottom, чтобы она заняла всю нижнюю часть формы. На панель поместите сетку DBGrid и шесть простых кнопок. Средняя панель в свойстве Align будет иметь значение alClient. На ней помещается только одна сетка DBGrid, которая в свойстве Align также имеет значение alClient. Внешний вид формы представлен на рисунке ниже:
244
Рис. 29.1. Главная форма клиентского приложения Свойство Name у кнопок переименуйте, соответственно, в: bConnect bRefresh bSaveToDB bUnConnect bSaveToFile bLoadFromFile Свойство Caption кнопок измените, как на рисунке. Верхнюю сетку и навигатор позже соединим с главной таблицей LichData, нижняя сетка отобразит данные подчиненной таблицы Telephones. Для невизуальных компонентов доступа создадим обычный модуль данных. Выберите команду File -> New -> Data Module. В свойстве Name модуля данных укажите fDM, а сам модуль сохраните на диск под именем DM. Не забудьте в главной форме командой File -> Use Unit подключить этот модуль. Перейдем на вкладку DataSnap. Вы видите целый ряд компонентов, реализующих подключение с удаленным модулем данных различными технологиями. Нам нужен компонент DCOMConnection, который использует технологию DCOM, поместите его в модуль данных. Для краткости обращений к компоненту переименуйте его свойство Name в DCOMC. Теперь нужно подключить его к удаленному серверу. В свойстве ComputerName нужно указать сетевое имя или IP-адрес компьютера, на котором расположен сервер приложений. Поскольку у нас все хранится на одном компьютере, укажем адрес 127.0.0.1, то есть, локальный IP-адрес. Теперь щелкнем по выпадающему свойству ServerName, которое хранит список всех доступных серверов на указанном компьютере. Если в прошлой лекции вы все сделали правильно, сейчас список должен содержать одну строку – имя серверного проекта и имя удаленного модуля данных. У нас это MyNewServer.MyRDM 245
Выберем эту строку. В свойстве ServerGUID автоматически должен появиться уникальный идентификатор сервера. Убедимся, что свойство LoginPrompt установлено в False, чтобы при соединении не запрашивались имя пользователя и пароль, и переведем свойство Connected в True, чтобы физически подключить клиентское приложение к серверу. Если вы все сделали правильно, то соединение произойдет, при этом Windows автоматически загрузит сервер приложений, который мы делали на прошлой лекции. Вот таким простым способом клиентское приложение связывается с удаленным модулем данных. Как видите, компонент содержит минимум необходимых свойств. Теперь нам нужно подключиться к наборам данных на сервере. Для этого используется компонент ClientDataSet из вкладки Data Access Палитры компонентов. Для каждого набора данных требуется установить по одному такому компоненту. Это важный компонент, поэтому рассмотрим его подробней. Компонент ClientDataSet предназначен для: Предоставления удаленных данных, расположенных на сервере приложений; Загрузки данных из локального файла; Сохранение набора (или связанных наборов) данных в локальный файл. Компонент ClientDataSet является наследником объекта DataSet, как таблицы или запросы, поэтому он имеет почти те же свойства, методы и события, что и эти наборы данных. Например, открытие этого НД можно осуществить как методом Open, так и присвоением True свойству Active. Однако, в связи с удаленным способом связи, имеются и различия. Прежде всего, различие связано с тем, что компонент считывает данные в буфер (или кэш), после чего работает с ними. Выполнение метода Post, к примеру, сохраняет данные не в базе данных на сервере, а в буфере (в оперативной памяти клиентского приложения). Чтобы сохранить эти данные в базу, нужно явно вызвать метод ApplyUpdates. Это позволяет существенно снизить нагрузку на сетевой трафик. Таким способом и реализуется отложенная обработка. Рассмотрим основные свойства и методы компонента ClientDataSet.
Основные свойства и методы компонента ClientDataSet Таблица 29.1. Важные свойства компонента ClientDataSet Свойство Описание Содержит количество изменений в буфере. После явного сохранения ChangeCount изменений в базу данных, это свойство содержит 0. Содержит текст SQL-запроса, который может быть выполнен методом Execute компонента. Таким образом, можно изменять НД, CommandText предоставляемый сервером. Свойство содержит пакет данных, предназначенный для передачи по Data сети в специальном транспортном формате. Размер в байтах пакета Data. DataSize При отношении один-ко-многим, свойство у подчиненного ClientDataSet позволяет выбрать специальное поле типа TDataSetField, которое DataSetField инкапсулирует данные подчиненного НД. Подробней об этом поговорим ниже. Пакет с измененными данными, которые еще не сохранены на сервере. Delta При значении True (по умолчанию) разрешает компоненту получать очередной пакет данных по мере надобности, например при прокрутке FetchOnDemand сетки DBGrid. Может содержать имя файла, в которое при необходимости будет сохранять данные, или считывать их. Если этого имени нет, можно FileName использовать SaveDialog или OpenDialog для получения этого имени от пользователя. Указывает количество записей, получаемых из серверного НД в одном PacketRecords 246
ProviderName RemoteServer
пакете. По умолчанию равно -1, то есть считываются все записи. Если значение равно 0, то считываются только метаданные. Имя компонента-провайдера на стороне сервера (DataSetProvider), который предоставляет доступ к нужному набору данных. Имя компонента соединения (например, DCOMConnection), с помощью которого компонент подключается к удаленному модулю данных. Это свойство нужно настраивать в первую очередь.
Теперь рассмотрим основные методы компонента ClientDataSet. Таблица 29.2. Важные методы компонента ClientDataSet Метод Описание Создает локальный индекс. ClientDataSet не может эффективно управлять индексами на стороне сервера, однако он позволяет создавать локальные индексы, которые существенно облегчают работу с данными. AddIndex Однако эти индексы невозможно сохранить вместе с набором данных, поэтому их нужно перестраивать при каждом открытии Включает механизм фильтрации по созданному ранее диапазону. ApplyRange Обновляет записи в базе данных. Имеет параметр – максимальное количество ошибок (по умолчанию 0), после которых обновление ApplyUpdates прекращается. Возвращает целое число – количество действительно допущенных ошибок. Отменяет все неподтвержденные изменения записи. Cancel Отменяет механизм фильтрации по указанному диапазону. CancelRange Отменяет все неподтвержденные изменения из пакета Delta, CancelUpdates предназначенные для дальнейшей передачи на сервер. Удаляет локальный индекс. DeleteIndex Очищает буфер от всех записей. EmptyDataSet Выполняет SQL-запрос из свойства CommandText, меняя НД на стороне Execute сервера. По умолчанию, большие BLOB-столбцы, которые могут содержать изображение, музыку или какие либо двоичные данные, в целях FetchBlobs разгрузки трафика клиенту не передаются. Метод FetchBlobs явно запрашивает с сервера содержимое текущего BLOB-столбца. Запрашивает с сервера недостающие данные из вложенных или подчиненных таблиц. Если свойство FetchOnDemand имеет значение FetchDetails True, то эти данные подгружаются автоматически, и применять метод не нужно. Получает с сервера приложений текущие параметры набора данных FetchParams провайдера. Если параметров нет, метод не делает ничего. Запрашивает с сервера очередной пакет записей, который будет добавлен к свойству Data. Длина пакета в записях определяется свойством GetNextPacket PacketRecords. Загружает данные из локального файла. Вместе с SaveToFile позволяет организовать отложенную обработку данных. Подробнее об этом LoadFromFile поговорим ниже. Загружает данные из потока. LoadFromStream Подтверждает сделанные изменения в буфере. На сервере данные при Post этом не меняются, для этого нужно явно вызвать метод ApplyUpdates. Обновляет текущую запись, запрашивая ее с сервера. RefreshRecord Восстанавливает текущую запись, если она была изменена, но еще не RevertRecord сохранена на сервере. Сохраняет данные в локальный файл. Отличается от аналогичного SaveToFile метода других компонентов наличием второго параметра – формата 247
SaveToStream
данных файла. Может быть три формата: dfBinary – двоичный формат, dfXML – формат XML и dfXMLUTF8 – формат XML в кодировке UTF8. Сохраняет данные в поток.
Реализация подключений к серверным наборам данных Вернемся к нашему проекту. Поместите в модуль данных два компонента ClientDataSet. Для подключения этих компонентов к наборам данных с сервера приложений, прежде всего, в свойстве RemoteServer нужно выбрать проводника, у нас это DCOMC. Далее в свойстве ProviderName следует выбрать нужного провайдера, который обеспечит связь с НД сервера. Для первого ClientDataSet это будет DSPLichData, для второго – DSPTelephones. Соответственно, у первого компонента ClientDataSet в свойстве Name впишите CDSLichData, у второго – CDSTelephones. Для того, чтобы связать навигатор DBNavigator и сетки DBGrid на главной форме, нам потребуются два компонента DataSource с вкладки Data Access. Первый назовите dsLichData и в свойстве DataSet свяжите его с набором данных CDSLichData, второй назовите dsTelephones и свяжите его с CDSTelephones. Откройте оба ClientDataSet, переведя их свойство Active в True. Вернитесь на главную форму. Навигатор и верхнюю сетку через свойство DataSource подключите к fDM.dsLichData, нижнюю сетку подключите к fDM.dsTelephones. Сетки при этом должны отобразить данные. Сохраните проект, скомпилируйте и загрузите. Сразу же при этом проявляется ошибка: нижняя сетка содержит подчиненные данные из таблицы Telephones, относящиеся к первой записи главной таблицы LichData. При смене записи в главной таблице, подчиненная таблица записей не меняет, несмотря на то, что мы организовали связь один-ко-многим на стороне сервера. Обусловлено это тем, что при начале работы подчиненная таблица получила нужные записи в локальный буфер, и теперь с ними работает, не изменяя его. Точно также, если мы сейчас попробуем сохранить данные таблиц в локальный файл, то записи главной таблицы сохранятся полностью, подчиненная же таблица сохранит лишь те записи, которые есть в кэше. Исправим эту ситуацию. Вернитесь в модуль данных. Щелкните дважды по компоненту CDSLichData, который содержит данные главной таблицы. Откроется редактор полей. Щелкните по нему правой кнопкой, и командой AddFields (добавить поля) добавьте все имеющиеся там поля. Как вы можете заметить, помимо всех полей таблицы LichData редактор отобразил еще одно поле: TTelephones, которое имеет тип TDataSetField. Это специальное поле, которое содержит данные подчиненной таблицы, через него и будет создаваться нужная связь один-ко-многим. Кнопкой OK закройте редактор полей, добавив все имеющиеся там поля. Перейдем к подчиненному компоненту CDSTelephones. В свойстве DataSetField выберите полученное поле главной таблицы CDSLichDataTTelephones. При этом автоматически очистятся свойства RemoteServer и ProviderName. Теперь этот НД связан с сервером через главную таблицу, а не напрямую. Снова сохраните проект, скомпилируйте и загрузите его. Теперь полный порядок: при изменении записи в главной таблице, изменяются записи подчиненной таблицы. Однако это не все. На модуль данных установите также два диалога: SaveDialog и OpenDialog. Переименуйте их соответственно, в SD1 и OD1. Выделите оба диалога и откройте редактор свойства Filter. В редакторе укажите единственную строчку: Файлы данных | *.dat В свойстве DefaultExt диалогов укажите расширение по умолчанию, «dat» (разумеется, без кавычек). В свойстве FileName укажите имя файла по умолчанию: «LichData.dat». В свойстве InitialDir укажите папку по умолчанию: «C:\». Таким образом, мы настроили оба диалога на работу с файлом C:\LichData.dat Конечно, пользователь сможет изменить и адрес, и имя файла, но по умолчанию будут использованы наши настройки. Осталось изменить свойство Title диалогов. У OD1 укажите «Открыть файл с личными данными», у SD1 – «Сохранить файл с личными данными». 248
Вернемся к главной форме. Создайте обработчик нажатия на кнопку «Подключиться к базе». Код обработчика приведен ниже: procedure TfMain.bConnectClick(Sender: TObject); begin //если нет подключения: if not fDM.DCOMC.Connected then begin // подключаем DCOMConnected fDM.DCOMC.Connected:= True; //открываем наборы данных: fDM.CDSLichData.Open; fDM.CDSTelephones.Open; end; //if end;
Комментарии достаточно подробны, чтобы вы смогли разобраться с кодом. Обработчик нажатия на «Обновить данные» будет таким: procedure TfMain.bRefreshClick(Sender: TObject); begin //отменяем все неподтвержденные изменения: fDM.CDSLichData.CancelUpdates; fDM.CDSTelephones.CancelUpdates; end;
При отмене неподтвержденных изменений с сервера считывается текущий набор данных, и сетки будут отображать действительное положение дел. Кнопка «Сохранить изменения в базу» вызовет следующий обработчик: procedure TfMain.bSaveToDBClick(Sender: TObject); var i,k: Integer; //для подсчета ошибок begin i:= fDM.CDSLichData.ApplyUpdates(0); k:= fDM.CDSTelephones.ApplyUpdates(0); if i + k > 0 then ShowMessage('При сохранении данных выявлено ошибок: '+ IntToStr(i+k)) else ShowMessage('Данные сохранены'); end;
Переменные целого типа нам нужны для подсчета возможных ошибок при сохранении данных на сервер. Метод ApplyUpdates, вызванный для каждого НД, не только сохраняет данные на сервер, но и возвращает количество ошибок, если они были. Параметр 0 указывает, что сохранение данных не нужно прекращать, если при сохранении выявляются ошибки. Можете здесь указать 1. В этом случае сохранение прекратится сразу же, как будет выявлена какая либо ошибка. Если сумма переменных i и k окажется больше 0, ошибки были допущены. Иначе ошибок не было. Код кнопки «Отключиться от базы» будет таким: procedure TfMain.bUnConnectClick(Sender: TObject); begin //отключаем таблицы fDM.CDSLichData.Close; fDM.CDSTelephones.Close; //отключаем DCOMConnection fDM.DCOMC.Close; end;
Вначале мы закрываем наборы данных, после чего прерываем подключение к серверу. Теперь сохраните проект, скомпилируйте его и загрузите. Попробуйте отредактировать какую-нибудь запись. Затем нажмите «Обновить данные», либо последовательно, отключитесь от сервера и снова подключитесь к нему. Вы убедитесь, что сделанные изменения не сохранились. Это подтверждает, что 249
при редактировании мы изменяем данные в кэше, а вовсе не на сервере. Если вы снова измените запись, затем нажмете «Сохранить изменения в базу», то изменения передадутся на сервер. Повторное обновление данных убедит вас в этом.
Сохранение данных в локальный файл, и чтение из файла Теперь нам осталось написать обработчики для кнопок «Сохранить данные в файл» и «Загрузить данные из файла». Однако здесь следует сделать одно замечание. Как уже говорилось, метод SaveToFile() компонента ClientDataSet имеет два параметра: имя файла, куда нужно сохранять данные, и формат файла, который имеет тип TDataPacketFormat. Этот тип может содержать три значения (см. табл.29.2), описание этого типа находится в модуле DBClient. Этот модуль уже подключен к нашему модулю данных DM, однако он не подключен к главной форме. Следовательно, в главной форме мы не сможем реализовать сохранение данных в файл и чтение их из файла, если не подключим этот модуль в раздел uses. Чтобы дважды не подключать его к проекту, реализуем методы сохранения в файл и чтения из файла в модуле данных, а в главной форме просто вызовем их. Перейдите к модулю данных. В разделе Public опишите две процедуры: public { Public declarations } procedure SaveDataToFiles; procedure LoadDataFromFiles;
Установите курсор на одну из них и нажмите , чтобы сгенерировать обе эти процедуры. Эти процедуры будут такими: {Читаем данные из файла} procedure TfDM.LoadDataFromFiles; begin if OD1.Execute then begin CDSLichData.LoadFromFile(OD1.FileName); ShowMessage('Процедура чтения данных выполнена!'); end; //if end; //************************************** {Сохраняем данные в файл} procedure TfDM.SaveDataToFiles; begin if SD1.Execute then begin CDSLichData.SaveToFile(SD1.FileName, dfBinary); ShowMessage('Процедура сохранения данных выполнена!'); end; //if end;
Комментарии достаточно подробны и не нуждаются в дополнительных пояснениях. Код кнопки «Сохранить данные в файл» на главной форме просто вызывает нужную процедуру: procedure TfMain.bSaveToFileClick(Sender: TObject); begin fDM.SaveDataToFiles; end;
А код кнопки «Загрузить данные из файла» будет следующим: procedure TfMain.bLoadFromFileClick(Sender: TObject); begin fDM.LoadDataFromFiles; end;
Сохраните проект, скомпилируйте и загрузите его. Соединитесь с базой данных, чтобы считать данные. Сохраните их в файл и отключитесь от базы данных. Считайте данные из локального файла, не 250
подключаясь к БД. Вы видите, что сохранились данные не только главной, но и подчиненной таблицы. Фактически, через поле TTelephones, каждая запись главной таблицы содержит также связанные данные из подчиненной таблицы, поэтому нам пришлось сохранять лишь один файл. Вы реализовали «метод Портфеля». На этом наш курс «Программирование баз данных в Delphi» окончен. Желаю всевозможных успехов в программировании!
251
СОДЕРЖАНИЕ ЛЕКЦИЯ 1. ТЕОРИЯ ПРОЕКТИРОВАНИЯ БАЗ ДАННЫХ.................................................................................................. 2 ВВЕДЕНИЕ .................................................................................................................................................................................... 2 ТЕРМИНОЛОГИЯ ........................................................................................................................................................................... 2 СВЯЗИ (ОТНОШЕНИЯ) ................................................................................................................................................................... 3 ССЫЛОЧНАЯ ЦЕЛОСТНОСТЬ.......................................................................................................................................................... 5 НОРМАЛИЗАЦИЯ БАЗЫ ДАННЫХ ................................................................................................................................................... 6 ЛЕКЦИЯ 2. ADO. СВЯЗЬ С ТАБЛИЦЕЙ MS ACCESS. .......................................................................................................... 8 СРАВНЕНИЕ BDE И ADO ............................................................................................................................................................. 8 СОЗДАНИЕ БАЗЫ ДАННЫХ MS ACCESS.......................................................................................................................................... 9 ПРАКТИКА РАБОТЫ С БД MS ACCESS ИЗ DELPHI......................................................................................................................... 13 ЛЕКЦИЯ 3. ПОИСК, ФИЛЬТРАЦИЯ И ИНДЕКСАЦИЯ ТАБЛИЦ. .................................................................................. 24 ПОСЛЕДОВАТЕЛЬНЫЙ ПЕРЕБОР................................................................................................................................................... 24 МЕТОД LOCATE .......................................................................................................................................................................... 24 МЕТОД LOOKUP ......................................................................................................................................................................... 26 ФИЛЬТРАЦИЯ ДАННЫХ ............................................................................................................................................................... 27 СВОЙСТВО FILTER ...................................................................................................................................................................... 27 СОБЫТИЕ ONFILTERRECORD ....................................................................................................................................................... 28 ИСПОЛЬЗОВАНИЕ ИНДЕКСОВ ...................................................................................................................................................... 29 ЛЕКЦИЯ 4. НАБОРЫ ДАННЫХ. ОСНОВНЫЕ СВОЙСТВА, МЕТОДЫ И СОБЫТИЯ ................................................. 32 СВОЙСТВА ................................................................................................................................................................................. 32 МЕТОДЫ .................................................................................................................................................................................... 34 СОБЫТИЯ ................................................................................................................................................................................... 35 БЛОКИРОВКА ТАБЛИЦ В АРХИТЕКТУРЕ ФАЙЛ-СЕРВЕР.................................................................................................................. 36 КУРСОРЫ В НАБОРАХ ДАННЫХ ADO .......................................................................................................................................... 36 CursorLocation (положение курсора) .................................................................................................................................. 36 CursorType (тип курсора).................................................................................................................................................... 37 ЛЕКЦИЯ 5. ТАБЛИЦЫ PARADOX В ADO............................................................................................................................. 38 ПОДКЛЮЧЕНИЕ ТАБЛИЦ PARADOX 7 К ПРИЛОЖЕНИЮ ЧЕРЕЗ ADO .............................................................................................. 38 ЛЕКЦИЯ 6. ПОЛЯ (TFIELD) .................................................................................................................................................... 45 ПОДСТАНОВОЧНЫЕ (LOOKUP) ПОЛЯ ........................................................................................................................................... 45 ВЫЧИСЛЯЕМЫЕ (CALCULATED) ПОЛЯ ......................................................................................................................................... 45 ПОЛЕ ДАННЫХ (DATA) ............................................................................................................................................................... 47 СВОЙСТВО DISPLAYVALUES ....................................................................................................................................................... 47 ДРУГИЕ НАИБОЛЕЕ ВАЖНЫЕ СВОЙСТВА КЛАССА TFIELD ............................................................................................................ 48 НАИБОЛЕЕ ВАЖНЫЕ МЕТОДЫ КЛАССА TFIELD ............................................................................................................................ 49 НАИБОЛЕЕ ВАЖНЫЕ СОБЫТИЯ КЛАССА TFIELD .......................................................................................................................... 49 ОБРАЩЕНИЕ К ЗНАЧЕНИЮ ПОЛЯ ................................................................................................................................................. 49 ЛЕКЦИЯ 7. ЗАПРОСЫ.............................................................................................................................................................. 51 ЗАПРОСЫ (TQUERY, TADOQUERY)............................................................................................................................................ 51 КОМПОНЕНТ TADOQUERY ........................................................................................................................................................ 51 СВОЙСТВА КОМПОНЕНТА-ЗАПРОСА ............................................................................................................................................ 55 МЕТОДЫ КОМПОНЕНТА-ЗАПРОСА ............................................................................................................................................... 56 ЛЕКЦИЯ 8. КРАТКИЙ КУРС ЯЗЫКА ЗАПРОСОВ SQL. .................................................................................................... 57 КОМАНДА SELECT.................................................................................................................................................................... 57 КОМАНДА WHERE .................................................................................................................................................................... 58 КОМАНДА ORDER BY............................................................................................................................................................... 59 ОПЕРАТОР IN ............................................................................................................................................................................. 60 ОПЕРАТОР BEETWEEN............................................................................................................................................................. 60 ОПЕРАТОР LIKE......................................................................................................................................................................... 60 АГРЕГАТНЫЕ ФУНКЦИИ .............................................................................................................................................................. 60 КОМАНДА GROUP BY............................................................................................................................................................... 61 КОМАНДА DISTINCT | ALL....................................................................................................................................................... 62 КОМАНДА HAVING................................................................................................................................................................... 62
252
ЛЕКЦИЯ 9. ПРИЕМЫ СОЗДАНИЯ И МОДИФИКАЦИИ ТАБЛИЦ ПРОГРАММНО. ................................................... 64 BDE. ПРОСТАЯ ТАБЛИЦА. .......................................................................................................................................................... 64 BDE. ТАБЛИЦА С КЛЮЧОМ И ИНДЕКСАМИ.................................................................................................................................. 66 ADO. СОЗДАНИЕ ПРОСТОЙ ТАБЛИЦЫ ПОСРЕДСТВОМ ЗАПРОСА SQL........................................................................................... 69 ЛЕКЦИЯ 10. СОХРАНЕНИЕ ДРЕВОВИДНЫХ СТРУКТУР В БАЗЕ ДАННЫХ. ............................................................. 72 ПОДГОТОВКА ПРОЕКТА .............................................................................................................................................................. 73 СОЗДАНИЕ И СОХРАНЕНИЕ В ТАБЛИЦУ ДЕРЕВА РАЗДЕЛОВ ........................................................................................................... 74 ЧТЕНИЕ ДРЕВОВИДНОЙ СТРУКТУРЫ ИЗ ТАБЛИЦЫ ........................................................................................................................ 78 ЛЕКЦИЯ 11. ОТЧЕТЫ. QUICK REPORT. .............................................................................................................................. 82 УСТАНОВКА QUICK REPORT. ...................................................................................................................................................... 82 ПРОСТОЙ ОТЧЕТ ......................................................................................................................................................................... 82 ОТЧЕТ ИЗ СВЯЗАННЫХ ТАБЛИЦ ................................................................................................................................................... 88 ЭКСПОРТ ОТЧЕТА В ДРУГИЕ ФОРМАТЫ........................................................................................................................................ 90 ЛЕКЦИЯ 12. РАБОТА С СЕТКОЙ DBGRID........................................................................................................................... 92 СТОЛБЦЫ DBGRID ..................................................................................................................................................................... 92 ПУСТЫЕ СТОЛБЦЫ ...................................................................................................................................................................... 95 СПИСОК ВЫБОРА В СТОЛБЦЕ ...................................................................................................................................................... 97 ВЫДЕЛЕНИЕ ОТДЕЛЬНЫХ СТРОК ................................................................................................................................................. 98 ЛЕКЦИЯ 13. DBCHART. ГРАФИКИ И ДИАГРАММЫ...................................................................................................... 101 ПРОСТОЕ ПРИЛОЖЕНИЕ С ГРАФИКОМ ....................................................................................................................................... 101 ПЕЧАТЬ ГРАФИКА ..................................................................................................................................................................... 106 ОСНОВНЫЕ МЕТОДЫ И СВОЙСТВА DBCHAR ............................................................................................................................. 108 ЛЕКЦИЯ 14. ВВЕДЕНИЕ В КЛИЕНТ-СЕРВЕРНЫЕ БД. INTERBASE. ........................................................................... 110 INTERBASE ............................................................................................................................................................................... 111 РЕГИСТРАЦИЯ СЕРВЕРА ............................................................................................................................................................ 113 РЕГИСТРАЦИЯ НОВОГО ПОЛЬЗОВАТЕЛЯ .................................................................................................................................... 117 ЛЕКЦИЯ 15. ТЕХНИЧЕСКИЕ ХАРАКТЕРИСТИКИ. СОЗДАНИЕ И ПЕРЕНОС БАЗЫ ДАННЫХ............................ 119 ОСНОВНЫЕ ТЕХНИЧЕСКИЕ ХАРАКТЕРИСТИКИ ........................................................................................................................... 119 СОЗДАНИЕ БАЗЫ ДАННЫХ ........................................................................................................................................................ 119 РАЗМЕР СТРАНИЦЫ................................................................................................................................................................... 120 КОДИРОВКА ПО УМОЛЧАНИЮ................................................................................................................................................... 121 ДИАЛЕКТ.................................................................................................................................................................................. 121 РЕГИСТРАЦИЯ БАЗЫ ДАННЫХ ................................................................................................................................................... 122 ПЕРЕНОС БАЗЫ ДАННЫХ ........................................................................................................................................................... 124 ЛЕКЦИЯ 16. ТИПЫ ДАННЫХ. ДОМЕНЫ........................................................................................................................... 129 ТИПЫ ДАННЫХ ......................................................................................................................................................................... 129 ЦЕЛЫЕ ЧИСЛА .......................................................................................................................................................................... 130 ВЕЩЕСТВЕННЫЕ ЧИСЛА ........................................................................................................................................................... 132 ЧИСЛА С ФИКСИРОВАННОЙ ТОЧКОЙ ......................................................................................................................................... 132 ДАТЫ И ВРЕМЯ ......................................................................................................................................................................... 133 ТЕКСТОВЫЕ ТИПЫ .................................................................................................................................................................... 133 ТИП ДАННЫХ BLOB................................................................................................................................................................. 135 СТОЛБЦЫ – МАССИВЫ .............................................................................................................................................................. 135 ЛОГИЧЕСКИЙ ТИП .................................................................................................................................................................... 136 ДОМЕНЫ .................................................................................................................................................................................. 136 ЛЕКЦИЯ 17. СОЗДАНИЕ, МОДИФИКАЦИЯ И УДАЛЕНИЕ ТАБЛИЦ И ПРЕДСТАВЛЕНИЙ.................................. 137 CREATE TABLE..................................................................................................................................................................... 137 ВЫЧИСЛЯЕМЫЕ СТОЛБЦЫ ........................................................................................................................................................ 138 ЗНАЧЕНИЯ ПО УМОЛЧАНИЮ ..................................................................................................................................................... 138 ПАРАМЕТР NOT NULL ............................................................................................................................................................ 139 ОГРАНИЧЕНИЯ СТОЛБЦОВ ........................................................................................................................................................ 139 ОГРАНИЧЕНИЯ CHECK В ДОМЕНАХ ......................................................................................................................................... 141 ПОРЯДОК СОРТИРОВКИ COLLATE........................................................................................................................................... 141 УДАЛЕНИЕ ТАБЛИЦ .................................................................................................................................................................. 141 МОДИФИКАЦИЯ ТАБЛИЦЫ ........................................................................................................................................................ 141 ПРЕДСТАВЛЕНИЯ...................................................................................................................................................................... 142 ИЗМЕНЯЕМЫЕ ПРЕДСТАВЛЕНИЯ ............................................................................................................................................... 144
253
МОДИФИКАЦИЯ ПРЕДСТАВЛЕНИЯ ............................................................................................................................................ 144 ЛЕКЦИЯ 18. КЛЮЧИ И ИНДЕКСЫ..................................................................................................................................... 145 ПРИМЕЧАНИЕ О ТЕРМИНОЛОГИИ .............................................................................................................................................. 145 PRIMARY KEY ...................................................................................................................................................................... 146 UNIQUE.................................................................................................................................................................................. 146 FOREIGN KEY ....................................................................................................................................................................... 147 МЕХАНИЗМЫ УПРАВЛЕНИЯ ССЫЛКАМИ ВНЕШНИХ КЛЮЧЕЙ ..................................................................................................... 149 ИМЕНОВАНИЕ ССЫЛОЧНОЙ ЦЕЛОСТНОСТИ ............................................................................................................................... 149 ИНДЕКСЫ ................................................................................................................................................................................. 151 ЛЕКЦИЯ 19. ХРАНИМЫЕ ПРОЦЕДУРЫ. ........................................................................................................................... 153 ХРАНИМЫЕ ПРОЦЕДУРЫ (STORED PROCEDURES) ...................................................................................................................... 153 ТЕРМИНАТОРЫ ......................................................................................................................................................................... 154 ЗАГОЛОВОК .............................................................................................................................................................................. 154 ТЕЛО ПРОЦЕДУРЫ .................................................................................................................................................................... 155 БЛОК КОДА ПРОЦЕДУРЫ ........................................................................................................................................................... 155 ОПЕРАТОР ПРИСВАИВАНИЯ ...................................................................................................................................................... 156 УСЛОВНЫЙ ОПЕРАТОР IF… THEN … ELSE............................................................................................................................. 156 ОПЕРАТОР SELECT ................................................................................................................................................................. 156 ЦИКЛ FOR SELECT И SUSPEND ............................................................................................................................................ 157 ЦИКЛ WHILE … DO................................................................................................................................................................ 157 ОПЕРАТОРЫ INSERT, UPDATE, DELETE............................................................................................................................... 158 ОПЕРАТОР EXECUTE PROCEDURE....................................................................................................................................... 158 ИСКЛЮЧЕНИЯ .......................................................................................................................................................................... 158 СОБЫТИЯ И ОПЕРАТОР POST_EVENT ..................................................................................................................................... 159 ИЗМЕНЕНИЯ И УДАЛЕНИЯ ХРАНИМЫХ ПРОЦЕДУР ..................................................................................................................... 160 ПРИМЕРЫ СОЗДАНИЯ И ВЫЗОВА ХРАНИМЫХ ПРОЦЕДУР ............................................................................................................ 160 ЛЕКЦИЯ 20. ГЕНЕРАТОРЫ И ТРИГГЕРЫ. РЕАЛИЗАЦИЯ АВТОИНКРЕМЕНТНОГО ПОЛЯ................................ 163 ГЕНЕРАТОРЫ ............................................................................................................................................................................ 163 УВЕЛИЧЕНИЕ ШАГА ГЕНЕРАТОРА ............................................................................................................................................. 164 ТРИГГЕРЫ ................................................................................................................................................................................ 165 [ACTIVE | INACTIVE] ........................................................................................................................................................ 165 {BEFORE | AFTER} {DELETE | INSERT | UPDATE} ......................................................................................................... 165 [POSITION <число>] ......................................................................................................................................................... 166 AS........................................................................................................................................................................................ 166 ПЕРЕМЕННЫЕ NEW И OLD ...................................................................................................................................................... 166 РЕАЛИЗАЦИЯ АВТОИНКРЕМЕНТНЫХ КЛЮЧЕВЫХ ПОЛЕЙ ........................................................................................................... 167 ЛЕКЦИЯ 21. КОМАНДЫ МОДИФИКАЦИИ ДАННЫХ DML. СКРИПТЫ. ................................................................... 169 INSERT ................................................................................................................................................................................... 169 UPDATE ................................................................................................................................................................................. 170 DELETE .................................................................................................................................................................................. 171 СКРИПТЫ ................................................................................................................................................................................. 172 ЛЕКЦИЯ 22. СОЕДИНЕНИЕ С БД КЛИЕНТСКОЙ ПРОГРАММЫ. ПРОБЛЕМЫ РУССКИХ БУКВ В INTERBASE. ..................................................................................................................................................................................................... 174 BDE......................................................................................................................................................................................... 174 ПРОБЛЕМЫ РУССКИХ БУКВ В INTERBASE .................................................................................................................................. 176 DBEXPRESS ............................................................................................................................................................................... 177 INTERBASE EXPRESS (IBX)....................................................................................................................................................... 179 ЛЕКЦИЯ 23. СТАНДАРТНЫЕ ФУНКЦИИ INTERBASE. UDF. ........................................................................................ 181 СТАНДАРТНЫЕ ФУНКЦИИ INTERBASE ....................................................................................................................................... 181 AVG .................................................................................................................................................................................... 181 COUNT ............................................................................................................................................................................... 181 MAX / MIN .......................................................................................................................................................................... 182 SUM.................................................................................................................................................................................... 182 CAST................................................................................................................................................................................... 183 UPPER................................................................................................................................................................................ 183 GEN_ID .............................................................................................................................................................................. 184 UDF......................................................................................................................................................................................... 184 ЛЕКЦИЯ 24. ТРАНЗАКЦИИ................................................................................................................................................... 188 АТОМАРНОСТЬ (ATOMICITY) .................................................................................................................................................... 188
254
СОГЛАСОВАННОСТЬ (CONSISTENCY) ........................................................................................................................................ 188 ИЗОЛИРОВАННОСТЬ (ISOLATION).............................................................................................................................................. 189 УСТОЙЧИВОСТЬ (DURABILITY) ................................................................................................................................................. 189 НЕЯВНЫЙ И ЯВНЫЙ СТРАТ ТРАНЗАКЦИЙ................................................................................................................................... 189 КАК ТРАНЗАКЦИЯ РАБОТАЕТ .................................................................................................................................................... 190 УРОВНИ ИЗОЛИРОВАННОСТИ ТРАНЗАКЦИЙ ............................................................................................................................... 191 ПАРАМЕТРЫ ТРАНЗАКЦИЙ ........................................................................................................................................................ 192 ПРАКТИКА ПРИМЕНЕНИЯ ТРАНЗАКЦИЙ ..................................................................................................................................... 193 ЛЕКЦИЯ 25. АДМИНИСТРИРОВАНИЕ INTERBASE: БЕЗОПАСНОСТЬ БД. .............................................................. 198 УТИЛИТЫ КОМАНДНОЙ СТРОКИ ............................................................................................................................................... 198 ПОЛЬЗОВАТЕЛИ ........................................................................................................................................................................ 200 ПРАВА ..................................................................................................................................................................................... 201 РОЛИ ........................................................................................................................................................................................ 203 ЛЕКЦИЯ 26. АДМИНИСТРИРОВАНИЕ INTERBASE: ОБСЛУЖИВАНИЕ БД. ............................................................ 205 РЕЗЕРВНОЕ КОПИРОВАНИЕ БАЗЫ ДАННЫХ (BACKUP) ................................................................................................................ 205 BACKUP С ПОМОЩЬЮ IBCONSOLE ............................................................................................................................................ 205 BACKUP С ПОМОЩЬЮ УТИЛИТЫ КОМАНДНОЙ СТРОКИ .............................................................................................................. 206 RESTORE С ПОМОЩЬЮ IBCONSOLE........................................................................................................................................... 208 RESTORE С ПОМОЩЬЮ УТИЛИТЫ КОМАНДНОЙ СТРОКИ............................................................................................................. 209 ТЕНЕВЫЕ (SHADOW) КОПИИ ..................................................................................................................................................... 210 УТИЛИТА КОМАНДНОЙ СТРОКИ GFIX ........................................................................................................................................ 212 РЕКОМЕНДАЦИИ ПО РЕМОНТУ ПОВРЕЖДЕННЫХ БАЗ ДАННЫХ ................................................................................................... 215 ЛЕКЦИЯ 27. ПРОГРАММНОЕ АДМИНИСТРИРОВАНИЕ БАЗ ДАННЫХ INTERBASE............................................. 217 РАЗРАБОТКА ПРОГРАММЫ ADMINIB......................................................................................................................................... 217 РЕАЛИЗАЦИЯ РЕЗЕРВНОГО КОПИРОВАНИЯ ................................................................................................................................ 217 РЕАЛИЗАЦИЯ ВОССТАНОВЛЕНИЯ ИЗ РЕЗЕРВНОЙ КОПИИ ............................................................................................................ 223 РАБОТА С ПОЛЬЗОВАТЕЛЯМИ .................................................................................................................................................... 227 Добавление нового пользователя....................................................................................................................................... 232 Редактирование пользователя.......................................................................................................................................... 233 Удаление пользователя...................................................................................................................................................... 235 ЛЕКЦИЯ 28. МНОГОУРОВНЕВАЯ АРХИТЕКТУРА......................................................................................................... 236 ПРЕИМУЩЕСТВА МНОГОУРОВНЕВЫХ МОДЕЛЕЙ ........................................................................................................................ 237 СЕРВЕР ПРИЛОЖЕНИЙ .............................................................................................................................................................. 237 ЛЕКЦИЯ 29. МНОГОУРОВНЕВАЯ АРХИТЕКТУРА. СОЗДАНИЕ «ТОНКОГО» КЛИЕНТА. .................................... 244 СОЗДАНИЕ КЛИЕНТСКОГО ПРИЛОЖЕНИЯ .................................................................................................................................. 244 ОСНОВНЫЕ СВОЙСТВА И МЕТОДЫ КОМПОНЕНТА CLIENTDATASET ........................................................................................... 246 РЕАЛИЗАЦИЯ ПОДКЛЮЧЕНИЙ К СЕРВЕРНЫМ НАБОРАМ ДАННЫХ ............................................................................................... 248 СОХРАНЕНИЕ ДАННЫХ В ЛОКАЛЬНЫЙ ФАЙЛ, И ЧТЕНИЕ ИЗ ФАЙЛА ............................................................................................ 250