ФЕДЕРАЛЬНОЕ АГЕНТСТВО ПО ОБРАЗОВАНИЮ ГОСУДАРСТВЕННОЕ ОБРАЗОВАТЕЛЬНОЕ УЧРЕЖДЕНИЕ ВЫСШЕГО ПРОФЕССИОНАЛЬНОГО ОБРАЗОВАНИЯ «ВОРОНЕЖСКИЙ ГОСУДАРСТВЕННЫЙ УНИВЕРСИТЕТ» В.Г. Рудалев ТЕХНОЛОГИЯ ADO И ДОСТУП К ДАННЫМ MS SQL SERVER Учебно-методическое пособие для вузов Издательско-полиграфический центр Воронежского государственного университета 2008 Утверждено научно-методическим советом факультета прикладной математики, информатики и механики 14 марта 2008 г., протокол № 7 Рецензент зав. каф. ПО и АИС ВГУ проф. М.А. Артемов Учебное пособие подготовлено на кафедре технической кибернетики и автоматического регулирования факультета прикладной математики, информатики и механики Воронежского государственного университета. Пособие рекомендуется для студентов факультета ПММ Воронежского государственного университета, сдающих экзамен по курсу «Технологии проектирования информационных систем». Для специальности: 010501 – Прикладная математика и информатика 2 Введение MS SQL Server – семейство серверов БД от фирмы Microsoft. В настоящее время наиболее широко распространена версия MS SQL Server 2000. По совокупности показателей и функциональных возможностей при очень скромных системных требованиях SQL Server 2000 превосходит InterBase, уступая Oracle. Постепенно набирает популярность версия SQL Server 2005, приближающаяся по своим характеристикам к Oracle, доступна для скачивания пробная версия SQL Server 2008. Существует также «облегченная» бесплатная версия SQL Server 2005 Express Edition. Тем не менее, мы будем рассматривать более старую версию SQL Server 2000, которая, будучи удобнее в эксплуатации, при гораздо более низких системных требованиях не имеет (по кругу базовых вопросов, рассматриваемых в данном пособии) существенных отличий от последних версий. По утверждениям разработчиков, приведенным в онлайновой документации, SQL Server 2000 может обслуживать базы данных терабайтных объемов при доступе тысяч пользователей. Однако главным достоинством MS SQL Server (а также его главным недостатком) является тесная интеграция с Windows и сопутствующими программными продуктами Microsoft (Back Office, Visual Studio .NET, IIS). Общими являются модель защиты, базирующаяся на защите Windows, консоль администрирования, набор программных интерфейсов и др. Важным фактором, сдерживающим распространение MS SQL Server, является отсутствие его версий для альтернативных операционных систем. По данным опроса посетителей сайта www.sql.ru SQL Server 2000 является наиболее популярной СУБД в РФ. На втором месте – Oracle, на третьем – InterBase. Мировая статистика объемов продаж (в денежном выражении) свидетельствует о безусловном лидерстве Oracle. 3 Доступ к базам данных сервера возможен из программ на любых языках программирования через универсальные интерфейсы ADO, ADO.NET, ODBC, JDBC. 1. Технология ADO. Общая характеристика Для доступа к данным SQL Server из клиентских приложений, написанных на языке Delphi для платформы Win32 можно применять хорошо известные универсальные технологии Borland Database Engine (BDE) и DBExpress, разработанные фирмой Borland. Однако рекомендуемый подход – использование технологии Microsoft ActiveX Data Objects (ADO), оптимизированной для SQL Server. Технология ADO основана на возможностях СОМ, а именно интерфейсов OLE DB. Базовый набор интерфейсов OLE DB предустановлен во всех версиях Microsoft Windows, поэтому при переносе приложения на другой компьютер для его работоспособности достаточно лишь правильно настроить провайдер OLE DB. Провайдер OLE DB представляет собой СОМ-сервер, предоставляющий набор интерфейсов для доступа к данным, и «скрывающий» особенности конкретных источников данных. Провайдеры OLE DB разработаны для большинства СУБД (MS Access, Oracle, Interbase и др.) и многих других источников данных. Часть провайдеров (например, OLE DB Provider for SQL Server) уже установлена в системе, другие доступны для скачивания в Internet. Технология ADO – надстройка над интерфейсом OLE DB, облегчающая его использование прикладными программистами. Технология ADO и интерфейсы OLE DB предоставляют приложениям единый способ доступа к источникам данных различных типов. Приложение, использующее ADO, может однотипно работать с данными, хранящимися на сервере SQL, с электронным таблицами и локальными СУБД (MS Access). Согласно терминологии ADO, любой источник данных (база 4 данных, электронная таблица, файл) называется хранилищем данных, с которым при помощи провайдера взаимодействует приложение. В результате приложение обращается не напрямую к источнику данных, а к объекту OLE DB, который представляет данные в виде таблицы БД или результата выполнения запроса SQL. Такая архитектура позволяет сделать набор объектов и интерфейсов открытым и расширяемым. Набор объектов и соответствующий провайдер могут быть созданы для любого хранилища данных без изменения исходной структуры ADO. При этом существенно расширяется само понятие данных. Можно разработать набор объектов и интерфейсов и для не табличных данных, например, графических данных, древовидных структур, данных CASE-инструментов и др. [4]. В Delphi на странице ADO палитры компонентов расположены компоненты доступа к данным, инкапсулирующие технологию ADO. Общая методика их использования построена по тем же принципам, что и у остальных компонентов доступа к данным (BDE, IBExpress, DBExpress и др.), однако внутренняя организация совсем другая. Это удобно, так как программист может с успехом использовать ранее имевшиеся навыки и опыт работы с другими СУБД. Например, компоненты ADO поддерживают навигацию, работу с наборами данных, кэшируемые изменения (здесь они называются пакетными обновлениями), управление транзакциями. Наиболее серьезное препятствие здесь – научиться мыслить категориями архитектуры клиент-сервер, и не пытаться переносить методы и приемы создания персональных баз данных в многопользовательскую клиентсерверную среду. С другой стороны, с помощью свойств и методов упомянутых компонентов при необходимости легко обратиться к дополнительным возможностям ADO для более «тонкой» настройки приложения (см. п. 2.8). Более подробную информацию можно получить в [4]. 5 2. Компоненты ADO в Delphi 2.1. Наборы данных ADO Предполагая, что читатель уже знаком с компонентной методикой создания приложений БД [6], рассмотрим основные особенности компонентов ADO для доступа к данным. На странице ADO Палитры компонентов Delphi расположены компоненты, инкапсулирующие набор данных ADO и приспособленные для работы с ADO соединениями. Это пять компонентов: ● TADODataSet — универсальный набор данных; ● TАDOTаblе — таблица БД; ● TADOQuery — запрос SQL; ● TADOStoredProc — хранимая процедура; ● TADOCommand – команда ADO. 2.2. Компонент TADOTable Компонент ТАDOTаblе предназначен для использования таблиц БД, подключенных через провайдеры OLE DB. По своим функциональным возможностям и применению он подобен стандартному компоненту BDE TTable и возвращает на клиентский компьютер все записи таблицы, формируя в адресном пространстве приложения набор данных. В клиентсерверной среде обычно требуется доставлять клиенту ограниченное число записей, поэтому от ТАDOTаblе по возможности следует отказываться и использовать компоненты TADOQuery, TADODataSet или TADOCommand и встраивать в них оператор SELECT с ограничительным условием WHERE. Имя таблицы БД задается свойством property TableName: WideString; 6 Свойство property Readonly: Boolean; позволяет включить или отключить для таблицы режим «только для чтения». Набор данных открывается методом Open и закрывается методом Close. Также можно использовать свойство property Active: Boolean; Дополнительно для наборов данных можно сделать активным фильтр (с помощью свойств Filter: String и Filtered: Boolean). Фильтр ограничивает видимые записи на клиентской машине, поэтому на загрузку сети он не влияет. Для установки соединения используется свойство Connection: TADOConnection, в котором хранится имя компонента-соединения. Также допускается непосредственная запись параметров соединения в свойство property СonnectionString: WideString;. 2.3. Компонент TADOQuery Компонент TADOQuery предназначен для выполнения запросов языка SQL. Текст запроса хранится в свойстве property SQL: TStrings; Параметры передаются в запрос с помощью свойства property Parameters: TParameters; Если запрос должен возвращать набор данных, для его открытия используется свойство property Active: Boolean; или метод procedure Open; В противном случае достаточно использовать метод function ExecSQL: Integer; Число обработанных запросом записей возвращает свойство property RowsAffected: Integer; 7 2.4. Компонент TAdoDataSet Универсальный компонент TADODataSet предназначен для представления набора данных ADO и может применяться несколькими способами, в зависимости от типа команды ADO и ее текста. Например, получать данные из таблиц, запросов SQL, хранимых процедур и т. д. В терминах ADO к текстовым командам ADO относятся операторы DML (в том числе, SELECT), DDL, операторы вызова исполняемых хранимых процедур. Для управления командами ADO используются свойства CommandText: WideString и CommandType: TCommandType. Например, можно задать CommandType = cmdText и занести в свойство CommandText текст запроса SQL. Здесь можно использовать только оператор SELECT. Если задать тип команды cmdTable, то в CommandText следует занести имя таблицы и т. п. Соединение с данных базой задается свойством Connectionstring или Connection. Набор данных открывается и закрывается свойством Active или методами Open и Close. 2.5. Компонент TADOStoredProc Компонент TADOStoredProc позволяет использовать хранимые процедуры. Имя хранимой процедуры определяется свойством property ProcedureName: WideString; Для определения входных и выходных параметров используется свойство property Parameters: TParameters; Если процедура будет применяться без изменений многократно, для ускорения работы полезно заранее подготовить ее выполнение на сервере. Для этого свойству property Prepared: Boolean присваивается значение True. 8 2.6. Команды ADO Компонент TADOCommand соответствует команде ADO. Это упрощенный компонент, предназначенный для выполнения операций, которые не возвращают наборы данных (например, модификация данных, исполняемые хранимые процедуры). Поэтому у TADOCommand нет необходимости работать c наборами записей, а его непосредственным предком является класс TComponent, к функциональности которого добавлены механизм соединения с БД через ADO и средства представления команды. Команда передается в хранилище данных ADO через собственное соединение или через компонент TADOConnection аналогично другим компонентам ADO. Текстовое представление выполняемой команды должно содержаться в свойстве property CommandText: WideString; Если для выполнения команды необходимо задать параметры, используется свойство property Parameters: TParameters; Команда выполняется методом Execute. 2.7. Настройка соединения Для установки соединения с сервером предназначен компонент TADOConnection. Компонент многофункционален, но чаще всего он используется для настройки соединения и управления транзакциями. Остальные компоненты содержат ссылку на TADOConnection и обращаются к серверу через одно соединение. Перед открытием соединения необходимо задать его параметры. Для этого предназначено свойство TADOConnection property ConnectionString: WideString; Набор параметров зависит от типа провайдера и записывается в это свойство как вручную, так и при помощи специального редактора параметров соединения, который вызывается двойным щелчком на компоненте 9 TADOConnection, перенесенным на форму, или щелчком на кнопке в поле редактирования свойства ConnectionString в Инспекторе объектов. Подробно методика настройки рассматривается далее на примере в разделе 3. Здесь отметим некоторые особенности и дополнительные возможности. Параметры соединения могут храниться в файле с расширением udl (радиокнопка Use Data Link File в окне настройки). Файл UDL представляет собой обычный текстовый файл, в котором указываются название параметра и через знак равенства его значение. Параметры разделяются точкой с запятой: [oledb] Everything after this line is an OLE DB initstring Provider=SQLOLEDB.1;Integrated Security=SSPI;Persist Security Info=False;Initial Catalog=test;Use Procedure for Prepare=1; Auto Translate=True;Packet Size=4096;Workstation ID=COMP; Use Encryption for Data=False; Если файл параметров соединения отсутствует, настройку осуществляется вручную. Для этого следует нажать кнопку Build. В результате появляется диалоговое окно Data Link Properties. Первая страница «Поставщик данных» (Provider) этого окна позволяет выбрать провайдер OLE DB для конкретного типа источника данных из числа провайдеров, установленных в системе. Состав элементов управления следующих страниц зависит от типа источника данных. На странице «Подключение» для случая SQL Server задается имя сервера, имя базы данных, способ аутентификации пользователя (в зависимости от способа – имя и пароль пользователя). Страница «Дополнительно» задает дополнительные параметры соединения. В зависимости от типа хранилища данных некоторые элементы этой страницы могут быть недоступны. В поле «Время ожидания подключения» можно задать время ожидания соединения в секундах. По истечении этого времени процесс прерывается. 10 Список «Права доступа» задает права доступа к отдельным видам выполняемых операций. Последняя страница «Все» позволяет просмотреть и при необходимости изменить все сделанные настройки для выбранного провайдера. Соединение с хранилищем данных ADO открывается и закрывается при помощи свойства property Connected: Boolean; При открытии соединения необходимо вводить имя пользователя и его пароль. Появление стандартного диалога подавляется, если установить свойство property LoginPrompt: Boolean; в False. Если для SQL Server использовалась интегрированная аутентификация Windows, то такой диалог не требуется. В любом случае параметры входа можно записать в свойство Connectionstring. 2.8. Класс TCustomADODataSet Общим предком наборов данных ADO является класс TDataSet, предоставляющий базовые функции управления наборами данных. Непосредственным потомком TDataSet (и предком ADO компонентов) является класс TCustomADODataSet, инкапсулирующий всю специфику ADO. На уровне TCustomADODataSet определены также новые (для программистов, работавших ранее с технологиями BDE и IBExpress) очень полезные свойства и методы для управления блокировками и курсорами из приложений, которые мы сейчас и рассмотрим. Перед открытием набора данных необходимо установить тип используемой при редактировании записей блокировки. Для этого применяется свойство property LockType: TADOLockType; значениями которого могут быть следующие константы. 11 ltUnspecified Блокировка задается источником данных (сервером), а не компонентом ltReadOnly Набор данных откроется в режиме только для чтения ltPessimistic Редактируемая запись блокируется на все время редактирования от ее чтения до момента сохранения в хранилище данных ltOptimistic Запись блокируется только при сохранении изменений в хранилище данных ltBatchOptimistic Запись блокируется на время сохранения в хранилище данных при вызове метода UpdateBatch Для того чтобы установка блокировки подействовала, свойство LockType должно быть обязательно модифицировано до открытия набора данных. Напомним, что блокировки применяются для организации многопользовательской работы с БД. При оптимистической блокировке предполагается, что конфликт, т. е. доступ к одной и той же записи со стороны сразу нескольких транзакций маловероятен. При пессимистической блокировке считается, что конфликт неизбежен, поэтому блокировка налагается на больший период времени (см. таблицу), что замедляет работу с данными. Оптимистическая блокировка короче, поэтому она задана в компоненте по умолчанию. Однако, если же оптимизм не оправдается и конфликт все-таки произойдет, транзакцию при оптимистической блокировке необходимо повторить заново. Для набора данных ADO необходимо выбрать тип и местоположение используемого курсора, с помощью которого набор данных будет формироваться. Курсор – это указатель на набор строк, возвращаемых SQL-запросом. Местоположение курсора задается свойством 12 type TCursorLocation = (clUseServer, clUseClient); property CursorLocation: TCursorLocation; Курсор может находиться на сервере (clUseServer) или на клиенте (clUseClient по умолчанию). Серверный курсор используется при работе с большими наборами данных, которые нецелесообразно пересылать клиенту целиком. При этом несколько снижается скорость работы клиентского набора данных. Работа с клиентским курсором происходит намного быстрее, но такой курсор лучше использовать для небольших наборов данных, не загружающих канал связи с сервером. При использовании клиентского курсора необходимо дополнительно установить свойство для управления обменом данных с сервером: property MarshalOptions: TmarshalOption, где TMarshalOption = (moMarshalAll, moMarshalModifiedOnly); По умолчанию задано значение moMarshalAll, разрешающее возврат серверу всех записей набора данных. При плохом соединении с сервером для ускорения работы компонента применяется свойство moMarshalModifiedOnly, обеспечивающее возврат только модифицированных клиентом записей. Тип курсора определяется свойством CursorType со следующими значениями ctUnspecified Курсор не задан, тип курсора определяется возможностями источника данных ctOpenForwardOnly Однонаправленный курсор, допускающий перемещение только вперед; используется при необходимости быстрого одноразового прохода по всем записям набора данных 13 ctKeyset Двунаправленный локальный курсор, не отображающий добавленные и удаленные другими пользователями записи ctDynamic Двунаправленный курсор, отображает все изменения. Требует наибольших затрат ресурсов ctStatic Двунаправленный курсор, полностью игнорирует изменения, внесенные другими пользователями Замечание: если курсор расположен на клиенте (CursorType = clUseClient), то для него доступен только один тип ctStatic, другие значения курсора при открытии НД будут автоматически заменены значением ctStatic. После передачи клиенту записи набора данных размещаются в локальном буфере, размер которого определяется свойством property CacheSize: Integer; Значение свойства есть число записей, помещаемых в локальный буфер, и оно не может быть меньше 1. Очевидно, что при достаточно большом размере буфера компонент будет обращаться к серверу не так часто, но при этом большой буфер замедлит открытие набора данных. При использовании клиентского курсора значение этого свойства ни на что не влияет, так как набор данных уже буферизован и находится в памяти клиентской машины. Также можно ограничить максимальный размер набора данных. Свойство property MaxRecords: Integer задает максимальное число записей набора данных. По умолчанию свойство имеет значение 0 и набор данных не ограничен. Общее число записей набора данных на этот момент property RecordCount: Integer; 14 возвращает свойство 2.9. Пакетные обновления (Batch updates) При использовании механизма пакетных обновлений любые изменения, вносимые пользователем, накапливаются в памяти клиентской машины. Позже полный пакет этих изменений может быть перенесен в базу данных за одну операцию. Очевидно, такой подход не нагружает сеть, пользователь может редактировать данные, даже будучи отключенным от сервера. Недостаток этого метода состоит в недоступности изменений другим пользователям, пока изменения находятся на клиенте. В других технологиях Borland (BDE, IBExpress, DBExpress) вместо термина «пакетные обновления» используется термин «кэширование изменений». Для перевода набора данных ADO в режим пакетных обновлений необходимо выполнить следующие действия: ● в наборе данных установить клиентский курсор: CursorLocation := clUseClient; ● задать оптимистическую блокировку: LockType := ItBatchOptimistic; В результате все измененные клиентом записи будут храниться в специальной области памяти (дополнительном буфере, области delta [3]). Набор данных будет выглядеть так, как будто данные были изменены, однако на самом деле эти изменения хранятся в памяти клиента, а не на сервере. В режиме пакетных обновлений метод Post компонентов наборов данных изменяет только содержимое набора данных, но не влияет на таблицы сервера. Чтобы перенести изменения из памяти в базу данных, необходимо вызвать метод procedure UpdateBatch(AffectRecords: TAffectRecords = arAll); Используемый в методах тип TAffectRecords позволяет задать тип записей, к которым применяется данная операция: 15 tAffectRecords = (arCurrent, arFiltered, arAll, arAllChapters); arCurrent — операция выполняется только для текущей записи; arFiltered — операция выполняется для записей из работающего фильтра; arAll — операция выполняется для всех записей; arAllChapters – операция выполняется для всех записей текущего набора данных (включая невидимые из-за включенного фильтра), а также для всех вложенных наборов данных. Вызов метода без параметров UpdateBatch() эквивалентен использованию arAll. Для отмены всех сделанных, но не сохраненных методом UpdateBatch изменений применяется метод procedure CancelBatch(AffectRecords: TAffectRecords = arAll); 2.10. Управление транзакциями В ADO допускаются два способа управления транзакциями – неявный и явный. При неявном управлении каждый оператор, модифицирующий данные (например, метод Post, UpdateBatch) рассматривается как отдельная транзакция. В случае успеха выполнения оператора транзакция применяется, в случае неудачи транзакция откатывается и генерируется исключительная ситуация. В случае модификации нескольких взаимосвязанных таблиц данный способ неизбежно приведет к нарушению целостности БД. Поэтому при модификации нескольких таблиц необходимо использовать явное управление транзакциями, при котором границы транзакции задаются явно. 16 Для явного управления транзакциями применяются методы компонента TADOConnection. Методы function BeginTrans: Integer; procedure CommitTrans; procedure RollbackTrans; означают старт, фиксацию и откат транзакции, соответственно. Свойство property IsolationLevel: TIsolationLevel; позволяет задать уровень изоляции транзакции (см. таблицу). ilUnspecifled Уровень изоляции не задается ilReadUncommitted Незафиксированные изменения других транзакций видимы ilChaos Изменения более защищенных транзакций не перезаписываются данной транзакцией ilBrowse Незафиксированные изменения других транзакций видимы ilCursorStability Изменения других транзакций видны только после фиксации ilReadCommitted Изменения других транзакций видны только после фиксации ilRepeatableRead Изменения других транзакций не видимы, но доступны при обновлении данных ilSerializable Транзакция выполняется изолированно от других транзакций ilIsolated Транзакция выполняется изолированно от других транзакций 17 Примерная схема транзакции в задаче отпуска товара со склада выглядит следующим образом: var otpusk, ostatok: integer; … Otpusk := NewValue; //Новое значение из строки редактирования // Выбираем значение поля Rest текущей записи таблицы Product Ostatok := tProductRest.value; if Otpusk <= Ostatok then begin // Запускаем транзакцию AdoConnection.BeginTrans; try // Изменяем новый пустой заказ // Переводим НД для таблицы Orders в режим вставки tOrders.Append; tOrdersProdCount.Value:=Otpusk; tOrders.Post; // Передаем изменения на сервер // Уменьшаем остаток в таблице Product. tProduct.Edit; tProductRest.Value:=Ostatok-Otpusk; tProduct.Post; // Подтверждаем транзакцию AdoConnection.CommitTrans; except // Откатываем транзакцию AdoConnection.RollbackTrans; ShowMessage('Ошибка транзакции!'); end; end //if Otpusk <= Ostatok else begin ShowMessage ('На складе нет нужного количества!'); end; 18 2.11. Компоненты управления данными Данная группа универсальных компонентов сосредоточена на палитре Data Controls и предназначена для отображения и редактирования записей набора данных. Эти компоненты не зависят от выбранной технологии доступа (ADO, IBExpress и др.) и подробно описаны в литературе [3–6]. Поэтому, не углубляясь в детали, отметим лишь несколько важных фактов и принципиальных моментов. Навигатор вызывает методы набора данных (класса TDataSet) для работы с текущей записью, автоматически контролируя состояние набора данных (свойство State: TDataSetState). Если состояние не позволяет вызвать метод, кнопка навигатора не доступна. Напомним, что методы First, Prior, Next, Last перемещают указатель текущей записи, метод Append вставляет пустую запись и переводит НД в состояние dsInsert, Delete удаляет текущую запись, Edit переводит НД в состояние dsEdit, что позволяет редактировать текущую запись. Метод Post передает измененную запись на сервер (если не активирован режим пакетных обновлений) и переводит НД в состояние dsBrowse (только просмотр). Post можно вызвать, если НД находился в состоянии dsInsert или dsEdit. Метод Cancel отменяет изменения и переводит НД в состояние dsBrowse. Метод Refresh заполняет НД обновленными значениями с сервера без переоткрытия набора данных с сохранением позиции текущей записи. Без использования навигатора вызов методов и проверку состояния НД надо производить вручную. Но этот «недостаток» оборачивается преимуществами, так как появляется возможность реализовывать алгоритмы, проверять исключительные ситуации и т. п. Всегда ли необходимо явно вызывать Post и Edit? Нет. При работе с DBGrid метод Post вызывается автоматически при выходе из ячейки. Вызов метода Edit зависит от значения свойства AutoEdit компонента 19 DataSource, связанного с компонентом управления данными (DBGrid, DBEdit и других). Если AutoEdit=True (по умолчанию), то Edit вызывается автоматически, как только строка редактирования получит событие от клавиатуры с допустимым кодом символа. Символы, не допустимые для данного типа столбца, блокируются. Если AutoEdit=False, то метод Edit надо вызывать явно или с помощью кнопки навигатора. Иногда это бывает полезно для предотвращения случайного ввода данных. Рассмотрим теперь группу компонентов, предназначенных для редактирования текущей записи. В отличие от DBGrid , они позволяют редактировать данные не в виде таблиц, а в виде формы. Таблицы (иногда их называют «сетки») применяются для просмотра группы записей, а в форме, напротив, отображается только одна текущая запись, но в более удобном виде. Попробуйте, например, представить себе сетку с 10 колонками. Все рассматриваемые компоненты имеют свойство DataSource: TDataSource, в котором хранится имя источника данных и DataField: String, в котором записывается имя поля текущей записи НД, отображаемое в компоненте. Компонент DBText отображает поле текущей записи, указанное в его свойстве DataField , в режиме «только для чтения». DBEdit позволяет это поле редактировать и сохранять вызовом метода Post НД. Компоненты DBMemo и DBImage предназначены для отображения и редактирования многострочных текстовых и графических полей соответственно. Компоненты DBListBox (список) и DBComboBox (комбинированный список) подобны стандартным компонентам Delphi со схожим названием. Их задачей является ввод в поле текущей записи предопределенных значений, которые выбираются из списка, но не хранятся в базе дан20 ных. Список представлен свойством Items: TStrings. Например, в таблице Products есть поле Warranty (гарантия), которое может принимать только значения 6 мес., 12 мес., 24 мес. Занесите их в Items программно или с помощью инспектора объектов, после чего эти три значения будут выбираться из списка при редактировании. Компоненты DBLookUpListBox и DBLookUpComboBox предназначены для заполнения текущей записи дочерней таблицы данными, выбранными из родительской таблицы (см. п. 3.3.5). Свойства ListSource и ListField указывают на таблицу, откуда берутся данные, DataSource и DataField – на таблицу, куда будут подставляться данные. KeyField – поле родительской таблицы (ListSource), которое будет подставляться в поле DataField . Компонент DBCheckBox удобен, когда надо вводить одно из двух значений. Если компонент включен (Checked), то в связанное поле НД подставляется значение, указанное в свойстве ValueChecked, иначе – в ValueUnchecked. Компонент DBRadioGroup похож на DBListBox. В зависимости от положения переключателя в поле подставляются значения, заданные в свойстве Values: TStrings. Обозначения значений хранятся в свойстве Items: TStrings. 3. Пример программирования 3.1. Модель предметной области Рассмотрим упрощенную модель продаж. Сформулируем основные бизнес-правила. Каждый заказчик может сделать несколько заказов. Анонимные заказы допускаются. Постоянный заказчик, в отличие от анонимного, пользуется скидкой. Заказ может включать в себя несколько товаров. В заказе хранится сумма стоимостей всех включенных в него товаров. Каждый товар может войти в несколько 21 заказов. Один товар не может повторно войти в один и тот же заказ. Товар продается поштучно, причем нельзя продать товар, которого нет на складе. Наименование товара уникально. При продаже товара остаток товара на складе уменьшается. Заказчики, заказы и товары идентифицируются уникальным «дружественным» номером, пригодным для использования в отчетных документах. Воспользуемся системой Case-проектирования Erwin и в соответствии c перечисленными бизнес-правилами создадим логическую модель «Сущность-связь» предметной области (рис. 3.1). Products Orders Ord_ID Prod_ID Cust_ID (FK) SaleDate Total OrdNum ProductName Price Rest CountryName Category Customers Cust_ID CustName Address OrderDetails Prod_ID (FK) Ord_ID (FK) ProdCount Amount Рис. 3.1 Сущность – это объект предметной области, информация о котором должна изучаться и накапливаться. Сущность характеризуется атрибутами. Уникальный идентификатор сущности (первичный атрибут) отображается в верхней части прямоугольника, изображающего сущность, остальные атрибуты – в нижней части прямоугольника. Таким образом, наша логическая модель состоит из четырех сущностей – Customers, Orders (заказы), OrderDetails (детали заказа), Products. 22 Родительская сущность Customers связана с дочерней сущностью Orders неидентифицирующей необязательной связью «один-ко-многим» (пунктирная линия с жирной точкой на стороне дочерней сущности). При создании неидентифицирующей связи первичный ключ родительской сущности копируется в состав неключевых атрибутов дочерней сущности и становитcя внешним ключом (FK). Этим реализуется бизнес-правило, допускающее несколько заказов у одного заказчика. Прозрачный ромб на стороне родительской сущности обозначает необязательность связи, т. е. атрибут CUST_ID может быть пустым. В данном случае необязательность реализует бизнес-правило, допускающее анонимность заказчика. Сущности Orders и Products, с одной стороны, и дочерняя по отношению к ним сущность OrderDetails, с другой, связаны идентифицирующей связью «один-ко-многим» (сплошная линия). При идентифицирующей связи первичные ключевые атрибуты родительских сущностей копируются в состав первичных ключевых атрибутов дочерней сущности. Дочерняя сущность становится зависимой и обозначается прямоугольником с закругленными углами. Зависимая сущность не может существовать вне контекста родительской сущности, что для сущности OrderDetails (детали заказа) следует из ее названия. В сущности OrderDetails первичный ключ является составным (состоящим из двух внешних ключей ORD_ID и PROD_ID). Наличие сущности OrderDetails с составным первичным ключом позволило реализовать бизнес-правила «Заказ может включать в себя несколько товаров» и «Один товар не может повторно войти в один и тот же заказ». Первичный ключ Ord_ID, который имеет тип UniqueIdentifier (т. е. GUID), используется для связи между сущностями Orders и OrderDetails, но он не является номером заказа. Для описания «дружественного» номера заказа вводится дополнительный атрибут OrdNum. 23 Оставшаяся часть бизнес-правил реализуется средствами СУБД. После построения логической модели предметной области (Logical Model) следующим этапом разработки в среде Erwin является построение Physical Model, более тесно привязанной к конкретной СУБД. Для этого в главном меню ERWin выберем пункт меню Tools – Derive New Model, а в открывшемся окне в качестве Target Database укажем SQL Server 2000. Проверим и еще раз отредактируем типы атрибутов (столбцов SQL Server), которые Erwin подобрал самостоятельно. Далее требуется создать структуру БД на основе полученной модели. Здесь удобнее воспользоваться скриптом на языке SQL. В меню Tools среды ERWin выберем пункт Forward Engineer/Schema Generation (прямое проектирование/генерация схемы). В окне отметим объекты схемы, которые мы доверяем Erwin’у создавать (оставив только таблицы, домены и ограничения), и нажмем кнопку Preview для просмотра скрипта. Сущности преобразуются в определения таблиц БД, а атрибуты – в определения столбцов. Сохраним скрипт в файле на диске. Запустим инструмент SQL Query Analyzer, соединимся с пустой БД, загрузим и выполним скрипт. Перед выполнением скрипт рекомендуется тщательно проверить и, при необходимости, отредактировать. Некоторые названия сущностей и атрибутов, которые мы могли использовать в модели, неприменимы для названий таблиц и столбцов СУБД. Например, нельзя использовать слова Order, Group, так как они совпадают со служебными словами языка SQL. 3.2. Серверная часть приложения Итак, таблицы и ограничения БД мы создали с помощью скрипта, полученного в ERWin. Но некоторые объекты серверной части приложения (например, триггеры и хранимые процедуры) в целом более тесно привязаны к особенностям текущей версии входного языка сервера, поэтому для их создания предпочтительнее использовать инструмент SQL Server Enterprise Manager. 24 Прежде всего, уточним спецификацию первичных ключей. Для заполнения значения первичных ключевых полей допустимы два подхода. Например, можно сделать эти поля автоинкрементным. Для этого измените в свойствах столбца значения следующих атрибутов: Identity – Yes, Identity Seed – 1, Identity Increment – 1. Автоинкрементные поля – широко распространенный, но не самый лучший способ заполнения первичных ключей. Здесь возможны серьезные проблемы. Например, если мы объединяем несколько баз данных из разных филиалов фирмы в одну централизованную базу данных, нельзя гарантировать, что автоинкрементные номера заказов во всех филиалах будут различаться, что нарушит требование уникальности первичных ключей в объединенной БД. Кроме того, автоинкрементные поля сервер заполняет автоматически. Для получения значения такого поля клиент должен переоткрыть набор данных, что сопровождается дополнительной нагрузкой на сеть, особенно при открытии таких потенциально огромных таблиц, как Orders и OrderDetails. Поэтому мы будем применять атрибут Identity только для столбцов Cust_ID, Prod_ID, OrdNum. При создании таблицы Orders используем альтернативный подход и объявим для первичного ключа Ord_ID тип UniqueIdentifier. Значениями этого типа являются GUID – глобальные уникальные идентификаторы, совпадение которых возможно лишь теоретически. Генерировать GUID будем не на сервере, а в клиентском приложении. Разумеется, идентификатор GUID громоздок и не нагляден, поэтому в таблице присутствует дополнительный автоинкрементный номер заказа OrdNum, не являющийся первичным ключом. В итоге скрипт для создания таблиц имеет вид CREATE TABLE CUSTOMERS ( Cust_ID CustName int IDENTITY, varchar(20) NOT NULL, 25 ) go Address PRIMARY KEY varchar(20) NULL, (Cust_ID) CREATE TABLE ORDERS ( Ord_ID Cust_ID SaleDate Total uniqueidentifier NOT NULL, int NULL, datetime NOT NULL, money NOT NULL CHECK (Total > 0), OrdNum int IDENTITY, PRIMARY KEY (Ord_ID), FOREIGN KEY (Cust_ID)REFERENCES CUSTOMERS ) go CREATE TABLE PRODUCTS ( Prod_ID ProductName Rest ) go int IDENTITY, varchar(20) UNIQUE NOT NULL, int NOT NULL CHECK (Rest >= 0), Price money NOT NULL CHECK (Price > 0), CountryName varchar(20) NULL, Category int NULL, PRIMARY KEY (Prod_ID) CREATE TABLE ORDERDETAILS ( Prod_ID int NOT NULL, Ord_ID uniqueidentifier NOT NULL, ProdCount int NOT NULL, Amount money NOT NULL CHECK (Amount > 0), PRIMARY KEY (Prod_ID, Ord_ID), FOREIGN KEY (Ord_ID) REFERENCES ORDERS, FOREIGN KEY (Prod_ID) REFERENCES PRODUCTS ) go Ключевыми для приложения являются: ● хранимая процедура AddOrder (добавление нового заказа и получение его «дружественного» номера) CREATE PROCEDURE AddOrder @OrdID UniqueIdentifier, @CustID int, @Total money, @SaleDate DateTime, 26 @OrdNum int OUTPUT AS INSERT INTO Orders (Ord_ID, Cust_ID, VALUES (@OrdID, Total, SaleDate) @CustID, @Total, @SaleDate) SELECT @OrdNum = OrdNum FROM ORDERS WHERE Ord_ID = @OrdID GO ● триггер OnOrders для таблицы OrderDetails (уменьшение остатка на складе при вставке нового заказа) CREATE TRIGGER OnOrders ON [dbo].[OrderDetails] INSTEAD OF INSERT AS DECLARE @ost AS int, @ID AS int SELECT @ost=products.rest - i.prodcount, @ID= i.Prod_Id FROM products, inserted i WHERE products.Prod_id=i.Prod_id IF @ost >= 0 BEGIN INSERT INTO OrderDetails (Ord_ID, Prod_ID, ProdCount, Amount) SELECT Ord_ID, Prod_ID, ProdCount, Amount FROM inserted UPDATE Products SET Rest = @ost WHERE products.Prod_id = @id END ELSE BEGIN RAISERROR ('На складе нет нужного количества!', 16, 1) END GO 3.3. Клиентская часть приложения. Компоненты доступа к данным Работа будет состоять из четырех этапов: ● настройка компонентов доступа; 27 ● настройка логических полей наборов данных; ● настройка компонентов управления данными; ● реализация бизнес-правил. Для настройки доступа к данным нам потребуются следующие компоненты: Компонент Страница Назначение палитры компонентов TADOConnection ADO Соединение с сервером TADOStoredProc ADO TADODataSet ADO TDataSource Data Access Выполнение хранимой процедуры AddOrder Доступ к таблицам Customers, Productss, OrderDetails Связь между TADODataSet и компонентами управления данными Общепринятый стиль в создании программ для баз данных требует, в частности, разделения кода для настройки доступа к данным и кода для управления данными. Компоненты доступа к данным разместим в модуле данных (рис. 3.2). Рис. 3.2 Расположим в модуле данных и настроим сначала компонент conn: TADOConnection. Двойной щелчок по нему вызывает окно редактирования строки соединения (рис. 3.3). 28 Рис. 3.3 Для формирования строки соединения удобнее воспользоваться мастером, запускаемым по кнопке Build. На первой вкладке выбираем имя поставщика данных – Microsoft OLE DB Provider for SQL Server (рис. 3.4). На следующей вкладке (рис. 3.5) указываем имя сервера (можно использовать кнопку Обзор), способ входа в сервер (учетные записи Windows NT) и в ниспадающем списке – имя базы данных на сервере. Проверяем подключение, чтобы убедиться, что все сделано правильно и нажимаем OK. Рис. 3.4 29 Рис. 3.5 Дополнительно, чтобы отключить запрашивание имени и пароля (их мы уже вводили при входе в Windows) укажем для TADOConnection свойство LoginPrompt = False. Свойства adsProducts и adsCust настраиваются однотипно в соответствии со следующими таблицами. Свойство Name Значение adsProducts Комментарий Имя компонента Connection CursorType LockType Conn ctStatic ltOptimistic Имя компонента соединения Статический курсор Оптимистическая блокировка без пакетных обновлений По умолчанию SQL оператор Текст оператора CursorLocation clUseClient CommandType cmdText CommandText Select * from Products 30 Свойство Name Значение adsCust Комментарий Имя компонента Connection CursorType LockType Conn ctStatic ltOptimistic Имя компонента соединения Статический курсор Оптимистическая блокировка без пакетных обновлений По умолчанию SQL оператор Текст оператора CursorLocation clUseClient CommandType cmdText CommandText Select * from Customers Типичная ошибка программиста, привыкшего к разработке персональных БД, состоит в применении такой же методики по отношению к таблицам Orders и OrdersDetails, хотя даже для таблиц Products и Customers она далеко не всегда пригодна. Дело в том, что таблицы Orders и OrdersDetails, являющиеся дочерними, содержат потенциально огромный объем текущей информации, которая оператору для оформления заказа не требуется. В архитектуре клиент-сервер формирование НД из таких таблиц методами Select * from Orders оказывает крайне негативное влияние на производительность информационной системы в целом. Поэтому для набора данных adsOrdersDetails сделаем так, чтобы первоначально он был пуст, т. е. отражал содержание вновь вставляемого заказа в таблицу Orders. Для этого в приведенной ниже таблице используем запрос с параметром: select * from OrderDetails where Ord_ID = :OrdID В качестве значения параметра :OrdID в запрос будем передавать идентификатор GUID нового заказа, сгенерированный в приложении. На сервер должен отправляться уже полностью сформированный заказ, поэтому использование пакетных обновлений здесь вполне уместно. В итоге компонент adsOrdersDetails конфигурируется следующим образом: 31 Свойство Name Значение adsOrders Комментарий Имя компонента Connection conn CommandType CommandText cmdText select * from OrderDetails where Ord_ID = :OrdID ctStatic ltBatchOptimistic Имя компонента соединения SQL оператор Select Текст оператора. CursorType LockType CursorLocation clUseClient Статический курсор Оптимистическая блокировка c пакетными обновлениями По умолчанию Замечание. Если таблицы Products и Customers могут содержать большой объем информации, то для выборки данных из них также необходимо использовать выборку с ограничением, например, по ценовой группе или категории товара, по адресу заказчика и т. п. Необходимые изменения в проект внесите самостоятельно. Чтобы настроить доступ к хранимой процедуре, разместим на форме компонент spAddOrder: TAdoStoredProc, соединим его с объектом AdoConnection. В раскрывающемся списке свойства ProcedureName выберем имя хранимой процедуры AddOrder. Выбрав свойство Parameters, удостоверимся, что компонент включает все параметры хранимой процедуры, и запомним их названия. Для дальнейшей работы настроим компоненты-посредники TDataSource, связывающие наборы данных с визуальными компонентами: dsCust: Свойство Name DataSet Значение dsCust adsCust dsProducts: Свойство Значение Name dsProducts DataSet adsProducts 32 dsOrderDetails: Свойство Значение Name dsOrderDetails DataSet adsOrderDetails 3.4. Настройка логических полей наборов данных Следующий этап – настройка логических полей наборов данных adsCust, adsOrderDetails, adsProducts. Наборы данных создаются в оперативной памяти клиентской машины «по образу и подобию» таблиц базы данных. Логические поля наборов данных соответствуют физическим полям реальной таблицы, но могут по-другому (более удобно) отображаться, а некоторые логические поля могут быть вычисляемыми и не иметь соответствующих физических полей. Наконец, иногда бывает полезно не все физические поля отображать в логические. Для настройки логических полей вызовем так называемый редактор полей, два раза щелкнув по компоненту adsCust. Первоначально окно редактора пусто – логических полей нет. Но в контекстном меню выберем Add All Fields (добавить все поля), после чего настроим все поля из списка. Укажем для них свойство DisplayLabel – осмысленные заголовки в таблице. Например для поля CustName присвоим для DisplayLabel – «Фамилия» и т. д. Рис. 3.6 Для каждого созданного поля будет автоматически создан и занесен в форму компонент TNNNField, где NNN – тип поля. Имя компонента строится так: 33 имя набора данных + имя поля, например, для поля Address набора данных adsCust будет создан компонент adsCustAddress: TWideStringField; Для набора данных adsProducts логические поля создадим аналогично. Но для набора данных adsOrderDetails проделаем более сложную и более полезную операцию – создание поля просмотра. Очевидно, при вставке нового заказа неразумно заставлять оператора вручную прописывать код покупателя и код товара. Намного удобнее, когда из ниспадающего списка выбирается название товара из таблицы товаров, а его код подставляется в таблицу заказов автоматически. Такое поле называется полем просмотра. Для создания поля просмотра в редакторе полей выберем New Field, после чего появится диалоговое окно. В окне введем поле Prod Name, которое должно хранить название товара, и которого нет в таблице заказов. Тип поля сделаем строковым. Отметим переключатель Lookup (просмотр) – это будет поле просмотра. Далее в этом же окне определим ключевое поле Key Fields – оно выбирается из таблицы OrderDetails, куда будет происходить вставка. Key Fields должно совпадать с полем Lookup Keys из таблицы Products, откуда будет выбираться значение и подставляться в Orders. В качестве DataSet выбирается таблица просмотра adsProducts, а в качестве ResulField – поле из таблицы товаров, которое будет отображаться среди полей таблицы заказов. Очевидно, это поле – ProductName (см. рис. 3.7) и тип его должен совпадать с типом поля выбора ProdName. 34 Рис. 3.7 Если все было сделано правильно, после заполнения данных в этом диалоговом окне будет создано логическое поле выбора adsOrderDetailsProdName, имеющее следующие свойства (их можно отредактировать с помощью инспектора объектов). Свойство Значение Комментарий FieldName ProdName Название поля FieldKind fkLookUp Поведение поля (поле просмотра) DisplayLabel Товар Имя для отображения KeyFields Prod_ID Ключевое поле, общее для двух таблиц LookupKey- Prod_ID Fields LookupDataset Поле, значение которого будет выбираться adsPro- Таблица, из которой происходит ducts выбор LookupResult- Product- Поле из adsProducts, значение Field Name которого будет подставлено в поле выбора 35 Чтобы потребовать обязательности ввода количества товаров, установим для компонента-поля adsOrdersProdCount свойство Required в True. Для ввода покупателей поля просмотра создавать не будем, а сконфигурируем компонент TDBLookUpComboBox, выполняющий аналогичную функцию (см. п. 3.5). Так как согласно бизнес-правилам безымянные заказы допускаются, то в дальнейшем предусмотрим лишь опциональную возможность открытия НД adsCust. 3.5. Взаимодействие с пользователем Главную форму приложения создадим в соответствии с рис. 3.8. Рис. 3.8 В главном меню формы предусмотрим пункты «Файл- Соединение/Выход» и «Операции-Заказчики/Заказы/Товары». Продублируем их кнопками на форме. Функциональность программы будет обеспечивать форма TabForm, с расположенным на ней компонентом PC:TPageControl, разбитым на три вкладки TabSheet – tsCust, tsOrders, tsProducts, с соответствующими заголовками. Каждая закладка будет автоматически отображаться при открытии формы, в зависимости от нажатой кнопки, например: procedure TMainForm.sbProductsClick(Sender: TObject); begin TabForm.PC.ActivePage := TabForm.tsProducts; TabForm.ShowModal; end; 36 Рассмотрим логику формирования заказов. На закладке tsOrders расположены сетка DBGrid и навигатор, связанные через свойство DataSource с компонентом adsOrderDetails. Благодаря наличию поля просмотра в сетке отображается текстовое название товара, см. рис. 3.9. Рис. 3.9 Сетка специально настроена с помощью Columns Editor, чтобы отображать только нужную оператору информацию – название, количество и сумму товара. Остальные столбцы таблицы OrderDetails заполняются автоматически и в сетке не показываются. Отметим, что в состав типизированных полей обычно включаются все столбцы таблицы, а в состав столбцов сетки – только те, которые необходимы оператору. 37 В данном случае «лишними» для оператора являются идентификатор покупателя, идентификаторы заказа и товара Ord_ID, Prod_ID в НД asOrders. Чтобы их убрать из «сеток», следует два раза щелкнуть по компоненту TDBGrid и в появившемся редакторе столбцов (он немного напоминает ранее использовавшийся редактор полей) добавить в «сетку» только необходимые столбцы. Для отображения остатка и цены выбранного товара над сеткой расположены два DBEdit, связанные через свойство DataSource с компонентом adsProducts. Заказ будем формировать в два приема: занесение одной строки с первичным ключом Ord_ID в таблицу Orders и занесение группы строк с внешним ключом Ord_ID в таблицу OrderDetails. Первая операция выполняется с помощью хранимой процедуры AddOrder, вторая – отправкой на сервер пакета изменений в НД adsOrderDetails. В качестве первичного ключа будем использовать значение глобального уникального идентификатора (GUID), хранимое в поле формы OrdID: TGUID. Генерация GUID и занесение его в параметр запроса происходят при открытии формы: procedure TTabForm.tsOrdersShow(Sender: TObject); begin // Показываем текущую дату lbDate.Caption := DateToStr(Date); // Получаем уникальный номер для нового заказа CreateGUID(OrdID); // Открываем пустой список деталей заказа dm.adsOrderDetails.Close;; dm.adsOrderDetails.Parameters.ParamByName('OrdID').Value := GUIDToString(OrdID); dm.adsOrderDetails.Open; end; 38 При редактировании элементов заказа в сетке неизбежно возникнет проблема, связанная с отсутствием значения внешнего ключа OrdID в НД adsOrderDetails (он является частью первичного ключа). Чтобы этого не происходило, будем своевременно подставлять сгенерированное нами значение OrdID в обработчике BeforePost НД: procedure TDM.adsOrderDetailsBeforePost(DataSet: TDataSet); begin dm.adsOrderDetailsORD_ID.value:=GUIDToString(TabForm.OrdID); end; Пересчет суммы элемента заказа в денежном выражении (столбец Amount в таблице OrderDetails) при изменении количества товара нуждается в автоматизации. Для этого в модуле данных два раза щелкнем по компоненту adsOrderDetails, в редакторе полей выделим поле ProdCount и создадим для него обработчик для события OnChange (изменение значения): procedure TDM.adsOrderDetailsProdCountChange(Sender: TField); begin if not adsOrderDetailsProdName.IsNull then if dm.adsOrderDetailsPRODCOUNT.Value > dm.adsProductREST.Value then begin ShowMessage ('На складе нет нужного количества!'); dm.adsOrderDetails.Cancel; end else adsOrderDetailsAmount.Value:= adsProductprice.Value * adsOrderDetailsPRODCOUNT.value; end; Расчет суммы заказа выполняется в обработчике щелчка кнопки со значком ∑ : procedure TTabForm.bCalcClick(Sender: TObject); begin 39 lbTotal.Caption := FloatToStr (GetTotal(dm.adsOrderDetails)); ChangeAccess; end; Здесь используется процедура расчета по данным, еще не переданным на сервер. function GetTotal (ds: TDataSet): Currency; // Подсчет суммы заказа begin // Так как заказ еще не перенесен на сервер, // суммируем Amount из набора данных Result:=0; ds.First; while not ds.Eof do begin Result := Result + ds.FieldByName ('Amount').Value; ds.Next; end; end; Метод TTabForm.ChangeAccess используется для блокировки кнопок, использование которых не допускается состоянием НД: procedure TTabForm.ChangeAccess; var Ins: Boolean; begin // Разрешаем доступ к кнопкам в зав-ти от состояния НД Ins := dm.adsOrderDetails.State in [dsBrowse]; bOrderApply.Enabled := Ins; bOrderCancel.Enabled := Ins; bCalc.Enabled := Ins; end; Если сумма и содержание заказа покупателя устраивают, то нажимается кнопка «Применить», стартующая транзакцию. procedure TTabForm.bOrderApplyClick(Sender: TObject); begin 40 dm.conn.BeginTrans; // Старт транзакции try // В рамках одной транзакции сначала записываем // заказа в родительскую таблицу Orders. // Для этого вызываем хранимую процедуру AddOrder dm.spAddOrder.Parameters.ParamByName('@OrdID').Value := GUIDToString(OrdID); if cbCust.Checked then dm.spAddOrder.Parameters.ParamByName('@CustID').Value := dm.adsCustCust_ID.Value; dm.spAddOrder.Parameters.ParamByName('@Total').Value := GetTotal(dm.adsOrderDetails); dm.spAddOrder.Parameters.ParamByName('@SaleDate').Value := date; // Дата dm.spAddOrder.ExecProc; // Вызов хранимой процедуры // Получение с сервера "дружественного" номера заказа lbNum.Caption := dm.spAddOrder.Parameters.ParamByName('@OrdNum').Value; // Затем пересылаем пакетом содержание заказа // в дочернюю таблицу OrderDetails, // остаток на складе уменьшается в триггере dm.adsOrderDetails.UpdateBatch; dm.Conn.CommitTrans; // Фиксация транзакции except ShowMessage ('Ошибка транзакции!'); dm.adsOrderDetails.CancelBatch; dm.conn.RollbackTrans; // Откат транзакции end; ChangeAccess; end; Здесь для реализации бизнес-правила уменьшения остатка используется триггер, приведенный ранее в п. 3.2. При попытке передачи на сервер неправильного заказа будет выдано сообщение: 41 Рис. 3.10 Кнопка «Отмена» откатывает транзакцию: procedure TTabForm.bOrderCancelClick(Sender: TObject); begin dm.adsOrderDetails.CancelUpdates; if dm.conn.InTransaction then dm.conn.RollbackTrans; ChangeAccess; end; Дополнительно в обработчике OnClick навигатора запишем procedure TTabForm.nOrderDetailsClick(Sender: TObject; Button: TNavigateBtn); begin ChangeAccess; end; Согласно сформулированными нами бизнес-правилам анонимные заказы допускаются, т. е. ключ Cust_ID в таблице Orders заполнять необязательно. Поэтому для экономии трафика при оформлении заказа набор данных с заказчиками adsCust по умолчанию не открывается. Чтобы его все-таки открыть, следует отметить CheckBox, расположенный рядом со списком выбора заказчика в верхней части формы, после чего последний список будет заполнен данными: procedure TTabForm.cbCustClick(Sender: TObject); begin if cbCust.Checked then if dm.adsCust.Active then dm.adsCust.Refresh else dm.adsCust.Open else dm.adsCust.Close; end; 42 Компонент dblcCust: TDBLookUpComboBox служит для занесения в заказы кода заказчика, выбираемого из таблицы клиентов, и настраивается следующим образом: Свойство Значение Примечание DataField Cust_ID В какое поле и DataSource DM.dsOrders в какой НД записывается KeyField Cust_ID Что записывается ListField CustName;Address Что отображается в списке ListSource DM.dsCust Откуда берется При попытке закрытия формы проверяется активность транзакции и выдается окно с предупреждением. procedure TTabForm.FormCloseQuery(Sender: TObject; var CanClose: Boolean); begin if dm.conn.InTransaction then if MessageDlg('Имеются незавершенные транзакции. Завершить их?', mtConfirmation, [mbYes, mbNo], 0) = mrYes then begin try dm.conn.CommitTrans; except dm.conn.RollbackTrans; ShowMessage('Ошибка транзакции!'); end; end else begin dm.conn.RollbackTrans; end; CanClose:=True; end; 43 3.6. Форма соединения Очевидный недостаток программы – соединение жестко «зашито» в код. При изменении названия, местоположения сервера или способа аутентификации программу придется изменять и перекомпилировать. Поэтому добавим форму соединения, которая будет вызываться при старте программы (рис. 3.11). На форме имеется кнопка bBrowse: TButton, при нажатии которой будет запускаться стандартный диалог настройки параметров соединения. Рис. 3.11 После конфигурации строка соединения отображается в компоненте ConnectionString: TEdit, где ее можно подправить вручную. Далее при нажатии кнопки bConnect: TBitBtn делается попытка соединения с сервером. После успешного соединения строка соединения сохраняется в системном реестре. При повторных запусках программы строка считывается из реестра, а диалог конфигурации будет запускаться только при неудачном соединении. Таким образом, настройка соединения будет происходить, как правило, только при первом запуске программы. Необходимый для реализации данного алгоритма код выглядит следующим образом: uses dmUnit, Registry; {$R *.dfm} var Reg: TRegistry; KeyName: string; DBS: string; 44 procedure TfrmConnect.btnBrowseClick(Sender: TObject); begin if EditConnectionString(dm.Conn) then ConnectionString.Text := dm.Conn.ConnectionString; end; procedure TfrmConnect.FormShow(Sender: TObject); begin DBS:=''; Reg := TRegistry.Create; // Создаем объект реестр try // Устанавливаем корневой раздел Reg.RootKey := HKEY_CURRENT_USER; KeyName := 'Software\AdoMinSklad\Path'; // Ищем ключ Path в подразделе Software\IbSklad Reg.OpenKey(KeyName, True); // если его нет, параметр true заставляет создать DBS := Reg.ReadString('Path'); // Читаем значение ключа Reg.CloseKey; finally Reg.Free; end; ConnectionString.Text := DBS; end; procedure TfrmConnect.bConnectClick(Sender: TObject); begin with dm do begin Conn.Connected:=false; try dm.Conn.ConnectionString := ConnectionString.Text; dm.Conn.Connected := True; // Сохраняем параметры соединения if dm.Conn.Connected then 45 begin Reg := TRegistry.Create; // Создаем объект реестр try // устанавливаем корневой раздел Reg.RootKey := HKEY_CURRENT_USER; KeyName := 'Software\AdoMinSklad\Path'; // Ищем ключ Path в подразделе Software\IbSklad if Reg.OpenKey(KeyName, False) then begin // если есть Reg.WriteString('Path', ConnectionString.Text); // Записываем в ключ путь к БД Reg.CloseKey; end; finally Reg.Free; end; end; except MessageDlg('Не удается создать соединение. имя и пароль.', mtError, Проверьте [mbOK], 0); end; end; end; Теперь очистим свойство ConnectionString в компоненте AdoConnection (оно будет считываться из реестра) и запишем в обработчик события выбора пункта меню «Соединение» следующий код: procedure TMainForm.miConnectClick(Sender: TObject); var isConnected: boolean; begin if FrmConnect.ShowModal=mrOK then begin isConnected := dm.conn.Connected; 46 // Кнопки доступны только после успешного соединения miOper.Enabled := isConnected; sbCust.Enabled := isConnected; sbOrders.Enabled := isConnected; sbProducts.Enabled := isConnected; end; end; В заключение рассмотрим еще два способа доступа к данным MS SQL Server – обращение к хранимым процедурам и использование компонентов, чувствительных к данным. 3.7. Дополнительные функции Функциональность вкладок tsCust и tsProducts реализуйте самостоятельно. Первая вкладка должна включать поиск покупателя по начальным буквам и добавление покупателя в базу, если его там нет. Вторая вкладка должна включать просмотр товаров на складе и добавление товара на склад. Добавление товара должно происходить с учетом категории товара, хранящейся в дополнительной таблице Category со столбцами CatID и CategoryName. Если товар с указанным именем уже имеется в таблице Products, то при добавлении необходимо увеличить его остаток, если нет – добавить в таблицу строку c новым товаром. Подобные бизнес-правила лучше всего реализовывать в виде хранимых процедур, выполняемых на сервере. Примерный код хранимой процедуры: CREATE PROCEDURE dbo.Add_Product @NewName varchar(20), @NewPrice money, @newRest int, @CountryName varchar(20), @CatID int, @CurrentID int OUTPUT /* Входными параметрами являются новые название товара, 47 */ /* цена, количество, производитель, идентификатор категории */ /* Выходным параметром, помечаемым служебным словом OUTPUT, */ /* является идентификатор добавленного товара */ AS DECLARE @count as int /* Поиск товара */ SELECT @count=Count(*) FROM dbo.Product WHERE ProductName = @NewName IF @Count = 0 /* Если не найден, добавить */ INSERT INTO dbo.Product (ProductName, Price, Rest, CountryName, Category) VALUES(@NewName, @NewPrice, @newRest, @CountryName, @CatID) ELSE /* Иначе увеличить количество */ UPDATE dbo.Product SET Rest = Rest+ @NewRest c WHERE ProductName=@NewName /* Выбрать идентификатор (ключ ProdName уникален) */ SELECT @CurrentID = Prod_ID FROM dbo.Product WHERE ProductName=@NewName Вызов хранимой процедуры: procedure tsProducts.bApplyClick(Sender: TObject); begin with DM.spAddProduct do begin try Parameters.ParamByName('@NewName').Value := eProdName.Text; Parameters.ParamByName('@NewPrice').Value := StrToFloat(ePrice.Text); Parameters.ParamByName('@NewRest').Value := StrToInt(eRest.Text); Parameters.ParamByName('@CountryName').Value := eCountry.Text; 48 Parameters.ParamByName('@CatID').Value := tCategoryCatID.Value; ExecProc; // Выполнение хранимой процедуры except MessageDlg('Ошибка записи!', mtError, [mbOk],0); end; end; end; Указание. Для выбора данных из таблицы Category используйте компонент DBLookUpComboBox. В качестве задания повышенного уровня сложности предусмотрите хранение в таблице Category дерева категорий и выборку листа дерева для занесения в таблицу Products. Задания для самостоятельной работы Решение каждой задачи должно включать ER-модель указанной предметной области, серверную и клиентскую часть приложения. 1. Библиотека. 2. Видеотека. 3. Информационно-поисковая система. 4. Музыкальные альбомы. 5. Отдел кадров. 6. Распределение учебной нагрузки. 7. Бронирование авиабилетов. 8. Гостиница. 9. Прокуратура. 10. Факультет. 11. ИТУ. 12. Кулинарные рецепты. 49 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. Народные депутаты. Регистратура лечебного учреждения. Автозапчасти. Заказы на сборку компьютеров. Автосалон. F1 Word Grand Prix. Биржа труда. Турагентство. Аренда помещений. Риэлтерская фирма. CIA. Финансовая организация. Управление госимуществом. Ордена и награды. Охранное агентство. Hollywood. Российский футбол. Городские достопримечательности. Дома на продажу. Альпинистский клуб. Управление проектами. Катера и яхты. Ресторан. Заповедник. 50 Приложение. Типы данных SQL Server BIT INT, INTEGER Целое число, равное 0 или 1 32-битное целое число в диапазоне от –2,147,483,648 до 2,147,483,647 SMALLINT 16-битное целое число в диапазоне от 32,768 до 32,767 TINYINT 8-битное целое число в диапазоне от 0 до 255 DECIMAL[(P[,S])], Десятичное число с фиксированной точноNUMERIC, стью в диапазоне от –10 38 –1 до 1038 – 1. P – DEC максимальное количество знаков в числе. S – количество знаков после запятой MONEY Денежный тип данных. Целое 64-битное число, младшие 4 разряда которого отведены под дробную часть. Может хранить числа в диапазоне от –922,337,203,685,477.5808 до 922,337,203,685,477.5807. SMALLMONEY Аналогичен типу Money, но 32-разрядный и ограничен диапазоном от –214,748.3648 до 214,748.3647 FLOAT, Число с плавающей точкой в диапазоне от DOUBLE PRECISION –1.79E + 308 до 1.79E + 308. REAL Число с плавающей точкой в диапазоне от –3.40E + 38 до 3.40E + 38 DATETIME Дата и время в диапазоне от 1 января 1753 г. до 31 декабря 9999 г. с точностью до 3.33 миллисекунды SMALLDATETIME Дата и время в диапазоне от 1 января 1900 г. до 6 июня 2079 г. с точностью до 1 минуты TIMESTAMP Уникальный в пределах БД идентификатор. Этот тип данных не содержит времени и гарантирует лишь, что поле этого типа уникально в рамках базы данных UNIQUEIDENTIFIER Глобальный уникальный идентификатор. Статистически уникальное значение. Над этим типом данных определены операции =, <>, IS NULL и IS NOT NULL CHAR[(N)], Строка фиксированной длины. N – длина CHARACTER строки. Максимальная длина – 8000 символов VARCHAR[(N)], CHARAC- Строка переменной длины. N – длина строTER VARYING(N) ки. Максимальная длина – 8000 символов TEXT Строка произвольной (до 2,147,483,647 символов) длины 51 NCHAR[(N)], NATIONAL CHARACTER Строка фиксированной длины в формате UNICODE. N – длина строки. Максимальная длина – 4000 символов NVARCHAR[(N)], Строка переменной длины в формате UNINATIONAL CHARACTER CODE. N – длина строки. Максимальная VARYING(N) длина – 4000 символов NTEXT, Строка произвольной (до 1,073,741,823 симNATIONAL TEXT волов) длины BINARY[(N)], Двоичные данные фиксированной длины, до VARYING VARBINARY 8000 байт. N – длина данных VARBINARY[(N)] Двоичные данные переменной длины, до 8000 байт. N – длина данных IMAGE Двоичные данные произвольной (до 2,147,483,647 байт) длины BIGINT 64-битное целое число SQL_VARIANT Может хранить данные произвольного типа Литература 1. Кренке Д. Теория и практика построения баз данных / Д. Кренке.– СПб. : Питер, 2005. – 859 с. 2. Вьейра Р. SQL Server 2000. Программирование : в 2 ч. / Р. Вьейра. – М. : БИНОМ. Лаборатория знаний, 2004. – Часть 1. – 735 с. 3. Кэнту М. Delphi 7 для профессионалов / М. Кэнту. – СПб. : Питер, 2004. – 1101 с. 4. Дарахвелидзе П.Г. Разработка WEB-служб средствами Delphi / П.Г. Дарахвелидзе, Е.П. Марков. – СПб. : BHV-Петербург, 2003. – 672 с. 5. Рудалев В.Г. Клиент-серверные приложения баз данных : учебное пособие для вузов / В.Г. Рудалев, С.С. Пронин. – Воронеж : ИПЦ ВГУ, 2007. – 82 с. 6. Рудалев В.Г. Создание приложений для MS SQL Server: учебнометодическое пособие для вузов / В.Г. Рудалев. – Воронеж : ИПЦ ВГУ, 2006. – 58 с. 52 СОДЕРЖАНИЕ Введение ................................................................................................ 3 1. Технология ADO. Общая характеристика ..................................... 4 2. Компоненты ADO в Delphi .............................................................. 6 2.1. Наборы данных ADO ................................................................ 6 2.2. Компонент TADOTable ........................................................... 6 2.3. Компонент TADOQuery ........................................................... 7 2.4. Компонент TAdoDataSet ...................................................... 8 2.5. Компонент TADOStoredProc ............................................... 8 2.6. Команды ADO............................................................................ 9 2.7. Настройка соединения .............................................................. 9 2.8. Класс TCustomADODataSet ............................................... 11 2.9. Пакетные обновления (Batch updates) ................................... 15 2.10. Управление транзакциями.................................................... 16 2.11.Компоненты управления данными....................................... 19 3. Пример программирования............................................................ 21 3.1. Модель предметной области .................................................. 21 3.2. Серверная часть приложения ................................................. 24 3.3. Клиентская часть приложения. Компоненты доступа к данным ……………………………………………………..27 3.4.Настройка логических полей наборов данных...................... 33 3.5. Взаимодействие с пользователем .......................................... 36 3.6. Форма соединения................................................................... 44 3.7. Дополнительные функции...................................................... 47 Задания для самостоятельной работы .............................................. 49 Приложение. Типы данных SQL Server ........................................... 51 Литература........................................................................................... 52 53 Учебное издание Рудалев Валерий Геннадьевич ТЕХНОЛОГИЯ ADO И ДОСТУП К ДАННЫМ MS SQL SERVER Учебно-методическое пособие для вузов Редактор И.Г. Валынкина Подписано в печать 22.05.08 Формат 60×84/16. Усл. печ. л. 3,1. Тираж 50 экз. Заказ 978. Издательско-полиграфический центр Воронежского государственного университета. 394000, г. Воронеж, пл. им. Ленина, 10. Тел. 208-298, 598-026 (факс) http://www.ppc.vsu.ru; e-mail: [email protected] Отпечатано в типографии Издательско-полиграфического центра Воронежского государственного университета. 394000, г. Воронеж, ул. Пушкинская, 3. Тел. 204-133. 54