by Bill Todd, President, The Database Group, Inc.
for Borland Software Corporation
September 2002
Содержание
Borland dbExpress
Прошлые попытки создать универсальный интерфейс для разнообразных БД имели ряд проблем. Одни решения были очень большими, медленными и сложными для распространения, потому что они пытались взять на себя слишком много. Другие предлагали подход "наименьшего общего знаменателя", что не позволяло разработчикам использовать специфические особенности конкретной базы данных. Третьи предлагали комплексный подход с написанием драйверов, получая в результате ограниченную функциональность, низкую скорость или большое количество ошибок.
Borland dbExpress преодолевает эти проблемы, комбинируя новый подход к предоставлению общего API для разных баз данных с проверенной технологией Borland provider/resolver для управления работой с данными. В этом документе рассматривается архитектура dbExpress и механизм provider/resolver, демонстрируется пример создания приложений на компонентах dbExpress, и объясняется процесс переноса на dbExpress приложений, использующих BDE.
Архитектура dbExpress
dbExpress был разработан, чтобы решить следующие 6 задач:
- минимизировать объем и количество используемых ресурсов
- получить максимальную скорость работы
- обеспечить кросс-платформенность
- обеспечить легкость распространения
- обеспечить легкость разработки драйверов
- дать разработчику больше управления памятью и сетевым трафиком
Драйверы dbExpress небольшие по объему и быстрые, потому что они обеспечивают достаточно небольшую функциональность. Каждый драйвер выполнен в виде dll (на платформе Windows) или как so (shared library на Linux). Драйвер dbExpress предоставляет пять интерфейсов для выборки метаданных, выполнения операторов SQL и хранимых процедур, и возможность чтения записей из выборки в одном направлении (unidirectional cursor). В это же время, при использовании с DataSetProvider и ClientDataSet, dbExpress предоставляет полнофункциональную, высокопроизводительную, многопользовательскую систему для работы с SQL-серверами баз данных.
Как работает архитектура provider/resolver?
Архитектура provider/resolver использует четыре компонента для предоставления данных и их редактирования. Первый компонент – SQLConnection – предназначен для установления соединения между драйвером dbExpress и используемым сервером БД. Дальше идут компоненты, которые предоставляют доступ к данным, получаемым оператором SELECT или вызовом хранимых процедур. Третий компонент
– DataSetProvider, и четвертый
– ClientDataSet. Когда вы открываете ClientDataSet, он запрашивает данные у DataSetProvider. DataSetProvider открывает компонент, выполняющий запрос или хранимую процедуру, выбирает данные, закрывает этот компонент, и поставляет данные (и необходимые метаданные) компоненту ClientDataSet.
ClientDataSet хранит данные в памяти, пока они просматриваются и модифицируются. При добавлении, удалении или обновлении записи, в коде или через пользовательский интерфейс, компонент ClientDataSet запоминает эти операции в памяти. Для обновления базы данных нужно вызвать метод ClientDataSet.ApplyUpdates. ApplyUpdates передает изменения компоненту DataSetProvider. Провайдер стартует транзакцию, затем создает и выполняет операторы SQL, соответствующие произведенным операциям над данными ClientDataSet. Если все операторы SQL были выполнены успешно, провайдер завершает транзакцию по commit; если нет
– отменяет транзакцию по rollback. Изменения в базе данных могут не пройти, например, если изменения нарушают правила контроля данных, или если другой пользователь уже модифицировал эти данные определенным образом. При возникновении ошибки транзакция отменяется по rollback, и вызывается событие ClientDataSet.OnReconcileError, предоставляя вам возможность обработки ошибок.
Преимущества архитектуры provider/resolver
Короткое время жизни транзакций
Долгие транзакции заставляют сервер БД удерживать блокировки, которые снижают возможности многопользовательской обработки данных и отнимают ресурсы сервера. При архитектуре provider/resolver, транзакция существует только в тот момент, когда применяются обновления. Это существенно снижает требования к ресурсам и уменьшает вероятность блокировок, особенно при большом количестве пользователей сервера БД.
Дает возможность редактировать любые записи
Записи, возвращаемые многотабличными выборками, хранимыми процедурами или нередактируемыми view не могут быть изменены напрямую. Вы можете указать при помощи свойства ProviderFlags у объектов TField, какие столбцы должны обновляться, и в событии DataSetProvider.OnGetTableName
– какая именно таблица должна обновляться. При этом большинство нередактируемых данных станут редактируемыми.
Быстрая сортировка и поиск
Поскольку ClientDataSet хранит записи в памяти, они могут быть быстро отсортированы. Если сортировка в памяти работает медленно, вы можете создать индексы над данными ClientDataSet либо во время разработки, либо во время выполнения приложения. Эти индексы в памяти помогут менять порядок записей или искать записи очень быстро, без использования индексов в базе данных.
Автоматическое агрегирование
ClientDataSet может выполнять автоматически определяемые вами сложные вычисления, такие как Sum(Price)
– Sum(Cost). Вы можете группировать вычисления сумм по полю или комбинации полей. Вы также можете использовать агрегаты Min, Max, Count и Avg.
Просмотр подмножества данных
Выражения фильтрации могут использовать синтаксис WHERE для отображения подмножества записей ClientDataSet, без необходимости конструировать запрос на клиенте и отправлять его каждый раз на сервер.
Множество видов данных
Возможность "клонировать" курсор ClientDataSet позволяет просматривать одни и те же данные различными способами, одновременно. Например, можно просматривать одни и те же данные, отсортированные по разным столбцам.
Вычисляемые столбцы на клиенте
Также вы можете добавлять вычисляемые столбцы к ClientDataSet во время разработки, для того чтобы вычисляемые столбцы стали частью данных в памяти. Поскольку вычисления производятся скомпилированным Delphi или C++ кодом, они выполняются очень быстро и могут быть более сложными, чем вычисления, производимые на сервере в COMPUTED BY столбцах.
Ограничение, которого нет
Может показаться, что у хранения записей в памяти есть ограничение по количеству таких записей, с которыми можно работать. Однако вспомните обычную схему клиент-сервер – приложения разрабатываются таким образом, чтобы выбирать небольшой объем данных для минимизации сетевого трафика и загрузки сервера БД. Даже если нужно работать с необычно большим количеством записей, помните что 10 тысяч записей, каждая по 100 байт, занимают 1 мегабайт памяти. В случаях, когда объем данных действительно большой, компоненты ClientDataSet и DataSetProvider имеют свойства и события, которые позволяют выбирать часть записей, редактировать их, удалять из памяти и затем получать новую порцию записей.
Легкость распространения
Приложения, использующие dbExpress, для работы требуют две DLL. Первая dll
– драйвер dbExpress, например DBEXPINT.DLL для Interbase, и вторая
– MIDAS.DLL, библиотека поддержки ClientDataSet. Вместе эти две DLL занимают менее чем 500 килобайт. Это минимизирует размер приложения и упрощает его установку. Если вы не хотите распространять эти DLL, вы можете вкомпилировать их прямо в EXE приложения. Распространение на Linux то же самое, за исключением того, что библиотеки имеют расширение .so, а не .dll.
Легкость создания драйвера
Драйверы dbExpress должны реализовывать пять интерфейсов, которые описаны в онлайновой справке. Borland также поставляет исходный текст драйвера для MySQL в качестве примера. Это существенно снижает затраты производителей СУБД на создание высокопроизводительных драйверов. Вы можете создать свой собственный драйвер, если вы работаете с необычной или устаревшей базой данных, для которой нет доступных коммерческих драйверов.
Создание приложений на dbExpress
Перед тем как переносить существующие BDE-приложения на dbExpress, вы должны уметь работать с компонентами dbExpress. В этом разделе мы создадим приложение dbExpress, шаг за шагом, с описанием каждого используемого компонента. Этот пример создан в Delphi на Windows (или C++Builder), но шаги идентичны при работе с Kylix на Linux.
Приложение будет использовать базу данных примеров EMPLOYEE.GDB и отображать отношение один ко многим между таблицами Employee и Salary History. Приложение демонстрирует следующие возможности dbExpress:
- вложенность таблицы деталей в поле главной таблицы
- редактирование данных через SQLQuery, DataSetProvider, и ClientDataSet
- использование SQLQuery как источника данных только для чтения без DataSetProvider и ClientDataSet
- применение изменений, накопленных ClientDataSet, к базе данных
- обработка ошибок при применении изменений к базе данных
Компонент SQLConnection
Для создания простого приложения dbExpress, начнем со следующих шагов:
- Создайте новое приложение и добавим data module. Назовем модуль данных MainDm
- Используйте окно Project Options, для того чтобы убедиться, что модуль создается автоматически перед созданием главной формы
- Поместите компонент SQLConnection с закладки dbExpress на модуль данных
- Назовите компонент SQLConnection как EmployeeConnection и установите его свойство DriverName в Interbase
- Откройте редактор свойств Params и установим параметр Database в путь к EMPLOYEE.GDB
- Измените значения свойств UserName и Password на нужные
- Установите свойство LoginPrompt в False, чтобы не появлялось окно запроса имени пользователя и пароля при старте программы
- Установите свойство Connected в True для проверки соединения, и обратно в False
Компонент SQLConnection обеспечивает соединение с базой данных для любого количества компонент. Вы можете использовать несколько SQLConnection для подсоединения к нескольким базам данных одновременно.
Существует три способа осуществить соединение с базой данных: использовать существующий алиас, создать новый "алиас" или записать параметры соединения прямо в SQLConnection.Properties.
Для создания нового соединения сделайте двойной клик мышью на компоненте SQLConnection, при этом откроется окно Connection Editor. С левой стороны показываются созданные ранее алиасы. Список выбора Driver Name позволяет отфильтровывать нужные алиасы по типам серверов. Сетка Connection Settings показывает список параметров для текущего соединения (они могут отличаться в зависимости от типа драйвера). Все алиасы, которые вы создаете, хранятся в файле
dbxconnections.ini.
Внимание! Если база данных на сервере имеет кодировку WIN1251, то ее нужно указать в свойстве ServerCharSet. Если на сервере в базе данных установлена поддержка кодировки KOI8R, то для Kylix более естественным будет указать ServerCharSet=KOI8R (даже если база данных имеет кодировку WIN1251).
Файл алиасов
Если вы используете именованные соединения (алиасы), то файл dbconnections.ini можно распространять со своим приложением.
Свойство Params компонента SQLConnection
При создании нового алиаса нужно выбрать тип сервера в DriverName. Список серверов хранится в файле dbxdrivers.ini. Выбор нужного сервера в DriverName установит свойства LibraryName и VendorLib в значения, соответствующие данному серверу. LibraryName содержит имя драйвера dbExpress, а VendorLib – имя клиентской библиотеки сервера.
После ввода параметров информация о соединении будет хранится внутри приложения. Если вы хотите дать возможность пользователям приложений менять эту информацию, то параметры можно сохранять в своем файле конфигурации, и редактировать его в приложении или отдельной утилитой конфигурации. Это сделает приложение самодостаточным.
У SQLConnection есть методы StartTransaction, Commit и Rollback для явного управления транзакциями. Если нужно вызывать операторы SQL, которые не возвращают набор данных (например, операторы DDL), то можно воспользоваться методами Execute или ExecuteDirect. При этом компоненты для работы с SQL не нужны. Если нужен доступ к метаданным, то вызовите методы GetTableNames, GetFieldNames и GetIndexNames компонента SQLConnection.
Компоненты DataSet
dbExpress предоставляет четыре компонента: SQLDataSet, SQLQuery, SQLStoredProc и SQLTable. SQLDataSet является универсальным для любых приложений. Установкой свойства CommandType можно выполнять операторы SQL, вызывать хранимые процедуры, или выбирать записи из запросов. Остальные компоненты DataSet созданы для имитации функциональности BDE, насколько это возможно. Использование этих компонент значительно облегчает переход с BDE.
Компонент SQLQuery
Свойства и методы компонента SQLQuery очень похожи на свойства компонента TQuery BDE. Поскольку SQLQuery возвращает только однонаправленный набор записей, свойства и методы, используемые в BDE для редактирования данных, отсутствуют. Вы можете использовать SQLQuery для выполнения SQL как операторов DML, так и DDL. Для тех операторов, которые возвращают набор записей, можно вызвать метод Open или установить свойство Active в True. Для остальных операторов используйте метод ExecSQL. Продолжим создавать наше приложение:
- Поместите три компонента SQLQuery на модуль данных
- Установите их свойство SQLConnection в EmployeeConnection
- Назовите первый компонент EmployeeQry, второй HistoryQry, и третий – DeptQry
- Установите свойство SQL компонента EmployeeQry в
select * from employee
where dept_no = :dept_no
order by last_name
- Установите свойство SQL компонента HistoryQry в
select * from salary_history
where emp_no = :emp_no
- Установите свойство SQL компонента DeptQry в
select dept_no, department from department
order by department
- Поместите компонент DataSource на модуль данных, установите его имя в EmpLinkSrc, а свойство DataSet – в EmployeeQry
- Установите свойство компонента HistoryQry.DataSource в EmpLinkSrc
- Вернитесь к EmployeeQry, откройте редактор свойства Params, и установите значение параметра dept_no в XXX. Поскольку это неверное значение, в результате не будет отображено ни одной записи, пока пользователь не введет правильный номер отдела.
- Сделайте двойной клик на EmployeeQry, для того чтобы открыть Fields Editor. Добавьте все поля (в меню по правой кнопке add all fields)
- Выберите столбец emp_no, свойство ProviderFlags, и установите pfInKey в True. Установка этого флага указывает на то, что столбец emp_no является первичным ключом. Компоненту DataSetProvider (который мы добавим позже) нужна эта информация для генерации операторов SQL, модифицирующих данные.
- Выберите столбец full_name, свойство ProviderFlags, и установите pfInUpdate и pfInWhere в False. Столбец full_name является вычисляемым полем, поэтому не может быть обновлен и не нужен в условии where SQL операторов, генерируемых DataSetProvider.
- Установите свойство Active компонента EmployeeQry в True
- Сделайте двойной клик на HistoryQry, и в FieldEditor добавьте все поля (add all fields)
- Выберите поле emp_no и установите pfInKey в True. Повторите то же самое для полей change_date и updater_id, т. к. все эти три поля являются первичным ключом.
- Поле new_salary также является вычисляемым, поэтому для него тоже нужно установить pfInUpdate и pfInWhere в False
- Установите свойство Active компонента EmployeeQry в False
- Сделайте двойной клик на компоненте DeptQry, добавьте все поля (add all fields)
- Установите свойство Connected компонента EmployeeConnection в False
Компонент SQLTable
Этот компонент очень похож на TTable из BDE. Как и в BDE, этот компонент возвращает все столбцы и все записи из таблицы, поэтому он не очень подходит для приложений клиент-сервер. Его единственное преимущество только в том, что его использование позволяет быстро перевести на dbExpress приложения, которые используют TTable.
Для использования SQLTable нужно установить свойство SQLConnection в имя компонента SQLConnection, затем указать в TableName имя нужной таблицы, и открыть компонент методом Open или установкой свойства Active в True.
Компонент SQLStoredProc
Этот компонент предназначен для вызова хранимых процедур, и может быть использован, так же как и компонент TStoredProc BDE. Процедуры можно вызывать и компонентом SQLDataSet (и SQLQuery).
Для использования компонента установите свойство SQLConnection в имя соответствующего SQLConnection, и укажите в StoredProcName имя процедуры. Если процедура возвращает набор записей, то открыть ее можно методом Open или установкой свойства Active в True. Если процедура не возвращает набор записей, выполнить ее можно методом ExecProc.
Компонент SQLDataSet
Для использования SQLDataSet подсоедините его к выбранному SQLConnection. Далее, установите свойство CommandType в один из трех вариантов – ctQuery, ctStoredProc или ctTable. Чаще всего вы будете использовать ctQuery. Значение свойства CommandText зависит от установленного CommandType. Если CommandType равен ctQuery, то CommandText должно содержать текст выполняемого запроса SQL. При ctStoredProc CommandText должен содержать имя хранимой процедуры. При ctTable CommandText должен содержать только имя таблицы. Свойство Params используется для параметризованных запросов или процедур. Свойство DataSource предназначено для связывания с другими SQLDataSet, например, для организации связки мастер-деталь.
Если запрос возвращает набор записей, то SQLDataSet открывается методом Open или установкой Active в True. Если нет – запрос выполняется методом ExecSQL.
Компонент SQLDataSet обеспечивает перемещение по записям только в одном направлении. Если этого достаточно, например, для печати отчета, то можно использовать SQLDataSet с DataSource. Если же нужно обеспечить возможность навигации по записям как вниз, так и вверх, или редактировать записи, то нужно добавить DataSetProvider или использовать SimpleDataSet, как это описано дальше в этом документе.
При необходимости получения более подробной информации о метаданных, чем ее предоставляет SQLConnection, можно использовать метод SQLDataSet.SetSchemaInfo. Этот метод принимает три параметра – SchemaType, SchemaObject и SchemaPattern. SchemaType может быть stNone, stTables, stSysTables, stProcedures, stColumns, stProcedureParams, или stIndexes. Этот параметр определяет тип информации, которую будет выводить SQLDataSet при открытии. Если выполняется обычный запрос, то всегда указано stNone. Остальные типы схем открывают выборку с соответствующими полями и возвращаемой информацией. SchemaObject – имя хранимой процедуры или таблицы, когда идет запрос информации об этом объекте. SchemaPattern – маска для фильтрации результата запроса. Например, если SchemaType = stTables и SchemaPattern = 'EMP%', то набор данных будет содержать информацию только о тех таблицах, имя которых начинаются с 'EMP'.
Компонент SimpleDataSet
Для просмотра и редактирования записей в BDE достаточно использовать компонент TQuery. В dbExpress необходимо использовать комбинацию SQLQuery или SQLDataSet, DataSetProvider и ClientDataSet, соединенных вместе. Есть два способа сократить время на добавление этих трех компонент и установку их свойств.
Компонент SimpleDataSet введен в Delphi 7. Он комбинирует в себе SQLDataSet, DataSetProvider и ClientDataSet. Если вам нужен компонент для просмотра и редактирования данных одной таблицы, то просто добавьте компонент SimpleDataSet на модуль данных, и установите свойства CommandType и CommandText. Однако SimpleDataSet имеет ряд ограничений:
- Вы не можете использовать его в многозвенных приложениях. Если в дальнейшем потребуется конвертировать приложение в многозвенное, используйте отдельные компоненты
- Невозможно подсоединить дочерний DataSet
- События встроенного DataSetProvider не экспортированы
- Свойство Options встроенного DataSetProvider не экспортировано. Вы не можете устанавливать параметры провайдера во время разработки или выполнения
- Вы не можете определять поля встроенного DataSet в момент разработки. Это означает, что вы не можете установить свойство ProviderFlags во время разработки. Вам придется делать это в коде приложения
- Если вы используете Borland MyBase (база данных встроенная в ClientDataSet), и не работаете с сервером баз данных, то лучше использовать ClientDataSet отдельно, для уменьшения используемых ресурсов
- Свойства и методы встроенного SQLDataSet не эквивалентны свойствам TQuery. Поэтому использование SimpleDataSet при переносе проектов с BDE могут потребовать больше изменений в коде
Если нужно больше функциональности, чем предлагает ClientDataSet, то поместите компоненты SQLQuery, DataSetProvider и ClientDataSet на форму или модуль данных. Установите свойство DataSetProvider.DataSet на SQLQuery. Установите ProviderName на ClientDataSet. Выберите все три компонента, и в главном меню среды выберите
– Component | Create Component Template. Укажите имя класса, компонента и палитры для нового шаблона. Теперь можно бросить три компонента на модуль данных так же легко как один компонент.
SQLMonitor
Этот компонент предназначен для оптимизации производительности ваших приложений. SQLMonitor контролирует все операторы SQL, передаваемые компонентом SQLConnection серверу баз данных. Информация может быть сохранена в файл, компонент TMemo, или обработана любым другим способом.
Компоненты доступа к данным
При необходимости прокрутки записей и обновления данных, нужно использовать компоненты DataSetProvider и ClientDataSet.
DataSetProvider
Этот компонент связывается с компонентами DataSet через одноименное свойство. DataSetProvider поставляет данные компоненту ClientDataSet и генерирует операторы SQL DML для обновления базы данных, используя лог изменений, накапливаемый ClientDataSet.
Вернемся к модулю данных нашего приложения и проделаем следующие шаги:
- Добавьте DataSetProvider с палитры DataAccess на модуль данных
- Его свойство DataSet установите в EmployeeQry, а имя (name) в EmployeeProv
Свойство UpdateMode позволяет управлять, каким образом провайдер определяет, изменена ли запись другим пользователем. Когда провайдер генерирует операторы SQL для обновления базы данных, каждый оператор UPDATE или DELETE включает условие WHERE для идентификации записи. Если UpdateMode установлен в upWhereAll, оригинальное значение каждого не-блоб поля в записи будет включено в условие WHERE, за исключением тех полей, у которых в ProviderFlags установлено pfInWhere=False. Это означает, что выполняемый оператор UPDATE или DELETE выдаст сообщение об ошибке, если на сервере любое из полей данной записи было изменено. upWhereChanged включает в WHERE только измененные поля. При upWhereKeyOnly генерируется WHERE, включающий только поля первичного ключа.
Свойство Options содержит много флагов, позволяющих управлять процессом provide/resolve. Если poCascadeDeletes установлено в True, то провайдер не будет генерировать операторы SQL для удаления записей в таблице деталей при удалении записей в мастер-таблице. Провайдер будет предполагать, что сервер баз данных поддерживает каскадное удаление, и удалит записи деталей самостоятельно. poCascadeUpdate обеспечивает аналогичную функциональность при изменении первичного ключа мастер-таблицы.
Если SQLQuery, поставляющий данные провайдеру, содержит запрос с ORDER BY, и вы хотите сохранить этот порядок в ClientDataSet, установите флаг poRetainServerOrder в True. Если нужно иметь возможность менять текст запроса SQLQuery путем изменения свойства ClientDataSet.CommandText – установите poAllowCommandText в True.
Если используется обработчик событий BeforeUpdateRecord, и он может изменить значение поля в записи, перед тем как обновление будет отправлено на сервер, установите poPropogateChanges в True. Теперь провайдер будет отсылать изменения обратно в ClientDataSet для обновления записей, хранимых в памяти.
Из множества полезных событий компонента DataSetProvider стоит отметить наиболее важные - OnGetTableName и BeforeUpdateRecord. В многотабличных запросах, хранимых процедурах или нередактируемых view невозможно модифицировать записи напрямую. DataSetProvider предоставляет три инструмента для обработки таких ситуаций.
Если запись включает столбцы из одной таблицы, например возвращаемые хранимой процедурой, единственной проблемой является то, что DataSetProvider не в состоянии определить, какую таблицу надо обновлять. Решение кроется в создании обработчика OnGetTableName, чтобы это событие возвращало имя таблицы.
Другая возможность есть для обновления многотабличных выборок, где надо обновлять записи только одной таблицы. Вначале, установите ProviderFlags конкретных полей для указания, что именно эти поля надо обновлять. Потом создайте OnGetTableName, возвращающий имя нужной таблицы. После этого провайдер будет генерировать операторы SQL для обновления таблицы автоматически. Если нужно обновлять много таблиц для каждой записи, то добавьте событие BeforeUpdateRecord, в котором можно создавать операторы SQL для каждой таблицы и выполнять их.
Обработчик BeforeUpdateRecord также дает возможность проверять запись перед ее обновлением, и изменять значения полей. Можно вообще заблокировать изменение путем вызова excepton. Это делает BeforeUpdateRecord хорошим местом для применения бизнес-правил.
ClientDataSet
ClientDataSet подсоединяется к DataSetProvider через свойство ProviderName. Он получает данные из DataSetProvider, буферизирует данные в памяти, регистрирует все изменения данных, и отправляет лог изменений в DataSetProvider при вызове ClientDataSet.ApplyUpdates.
Продолжим работу над примером приложения:
- Поместите два компонента ClientDataSet на модуль данных
- Установите свойство ProviderName первого компонента на EmployeeProv, и назовите его EmployeeCds
- Сделайте двойной клик на EmployeeCds, чтобы открыть FieldEditor. Выполните Add all fields. Последнее поле имеет имя HistoryQry. Это поле вложенного DataSet, которое содержит историю зарплат (salary history) для каждой записи о сотруднике (employee). Записи, возвращаемые HistoryQry видны как вложенные данные внутри данных EmployeeQry. Это происходит потому, что свойство HistoryQry.DataSource установлено в EmpLinkSrc, который подсоединен к EmployeeQry, и получает значение параметра запроса из записи EmployeeQry.
- Щелкните правой кнопкой мыши на EmployeeCds и выберите пункт меню FetchParams, чтобы ClientDataSet обновил список параметров в соответствии с компонентом EmployeeQry.
- Назовите второй ClientDataSet как HistoryCds и установите его свойство DataSetField в EmploueeCdsHistoryQry. Теперь HistoryCds будет получать данные из поля вложенного набора данных EmployeeCds
- Сделайте двойной клик на HistoryCds, и в Field Editor добавьте все поля (add all fields)
- Поместите два компонента DataSource на модуль данных. Назовите их EmployeeSrc и HistorySrc.
- Установите свойство DataSet у этих компонент – у EmployeeSrc в EmployeeCds, и у HistorySrc в HistoryCds.
Теперь можно установить свойство Active в True у компонент EmployeeCds и HistoryCds. Модуль данных при этом будет выглядеть следующим образом:
Вложенные наборы данных – весьма мощная вещь, поскольку они исключают непонимание порядка обновлений данных мастер- и деталь-таблицы. Например, если добавлены несколько записей в таблицу-мастер и несколько записей в таблицу деталей, то INSERT в таблицу-мастер должен быть отправлен на сервер БД раньше, чем INSERT в таблицу деталей. И наоборот, если удаляются записи деталей, а затем записи мастера, то оператор DELETE должен выполняться именно в таком порядке. При вложенных наборах данных DataSetProvider обеспечивает правильный порядок следования запросов на обновление, что исключает ошибки на сервере БД.
ClientDataSet имеет ряд полезных свойств. Свойство Aggregates позволяет определить агрегатные столбцы, которые автоматически будут считать сумму, минимум, максимум, количество или среднее значение любого числового столбца набора данных. Агрегаты могут быть сгруппированы по любому индексу. Свойство AggregatesActive позволяет управлять состоянием агрегатов в момент выполнения.
Для сортировки записей в нужном порядке, укажите в свойстве IndexFieldNames имена полей сортировки, разделенные точкой с запятой. Для сортировки по убыванию или для ускорения сортировки создайте индекс по нужным полям, и укажите имя этого индекса в свойстве IndexName.
Используйте свойство CommandText для изменения запроса SQL в компоненте, который поставляет данные DataSetProvider. Закройте ClientDataSet, назначьте новый запрос в CommandText, и откройте ClientDataSet для получения результата нового запроса. Также можно назначать новые значения параметрам в источнике записей, используя свойство ClientDataSet.Params.
Почему желательно использовать CommandText и Params вместо обращения к конкретным свойствам компонента, поставляющего данные (например, SQLQuery)? Дело в том, что при таком написании кода его не придется менять, если впоследствии потребуется переписать приложение на многозвенную архитектуру (Borland DataSnap).
Свойство PacketRecords позволяет управлять количеством записей, которое провайдер выбирает с сервера за одно обращение. Значение по умолчанию -1 указывает провайдеру выбирать все записи, возвращаемые компонентом, выполняющим SQL-запрос. Обычно это нормально, но если нужно обработать большое количество записей, то может потребоваться выбирать их небольшими группами.
Одно из самых больших отличий BDE и архитектуры provider/resolver в том, что вы должны вызывать ClientDataSet.ApplyUpdates для применения изменений к базе данных. Используйте свойство ChangeCount для проверки наличия неотправленных изменений. ApplyUpdates имеет один параметр, который указывает допустимое количество ошибок, после которого процесс обновления будет прерван, и транзакция будет отменена по rollback. Обычно передается 0, поэтому процесс обновления прерывается при первой же ошибке. Передача -1 указывает на необходимость отправки всех изменений, независимо от количества возникающих ошибок.
Если при обновлении данных возникает ошибка, происходит событие ClientDataSet.OnReconcileError. Для обработки ошибок в нашем приложении-примере сделайте следующее:
- Добавьте в проект Reconcile Error Dialog с закладки Dialogs репозитария объектов
- Добавьте имя нового модуля в оператор USES модуля данных.
- В свойствах проекта убедитесь, что модуль Reconcile Error Dialog не создается автоматически при запуске приложения
- Создайте обработчик OnReconcile для EmployeeCds, и добавьте туда строку
Action := HandleReconcileError(DataSet, UpdateKind, E);
Если в момент применения изменений возникнет ошибка, то будет показан диалог Reconcile Error, в нем отображены записи, приведшие к сообщению об ошибке, и набор кнопок, которые дают пользователю возможность обработать ошибку.
Чтобы завершить приложение, проделайте следующее:
- Добавьте два компонента Panel, два DBGrid и два DBNavigator на главную форму приложения, так как это показано на картинке
- Добавьте имя модуля данных в USES главной формы
- Установите свойство DataSource верхнего DBGrid и DBNavigator в EmployeeSrc
- Установите свойство DataSource нижнего DBGrid и DBNavigator в HistorySrc
- Добавьте Label и ComboBox
- Создайте событие OnChange для ComboBox со следующим текстом
procedure TMainForm.DeptComboChange(Sender: TObject);
begin
with MainDm.EmployeeCds do
begin
if Active then CheckBrowseMode;
Close;
Params.ParamByName('DEPT_NO').asString :=Copy(DeptCombo.Text, 1, 3);
Open;
end; //with
end;
- Добавьте кнопку (Button) на верхнюю панель, и установите ее надпись в "Сохранить".
- Добавьте следующий код по OnClick для этой кнопки
procedure TForm1.SaveBtnClick(Sender: TObject);
begin
with MainDm do
begin
if EmployeeCds.ChangeCount > 0 then
begin
if HistoryCds.Active then
HistoryCds.CheckBrowseMode;
if EmployeeCds.Active then
EmployeeCds.CheckBrowseMode;
EmployeeCds.ApplyUpdates(0);
EmployeeCds.Refresh;
end; //if
end; //with
end;
- Добавьте метод к форме MainForm для заполнения списка выбора номеров отделов
procedure TMainForm.LoadDeptCombo;
begin
with MainDm do
begin
DeptQry.Open;
while not DeptQry.Eof do
begin
DeptCombo.Items.Add(DeptQryDept_No.asString +
' ' + DeptQryDepartment.asString);
DeptQry.Next;
end; // while
DeptQry.Close;
end; // with
end;
- Добавьте обработчик OnCreate для главной формы
procedure TMainForm.FormCreate(Sender: TObject);
begin
MainDm.EmployeeCds.Open;
LoadDeptCombo;
end;
Приложение готово, можно запускать.
BDE против dbExpress
В таблице приведено сравнение BDE и dbExpress по ряду параметров, для того чтобы привести список отличий. Вторая таблица показывает компоненты dbExpress, соответствующие компонентам BDE.
Функциональность |
BDE |
dbExpress |
буферизация записей |
BDE сам определяет количество записей, хранимых в памяти |
Вы можете управлять количеством записей, хранимых в памяти |
контроль сетевого трафика |
BDE сам определяет, сколько записей выбирать с сервера |
Вы управляете как количеством, так и моментом выборки записей с сервера |
управление транзакциями |
Есть автоматический режим и ручное управление транзакциями. Транзакции активны в момент редактирования данных. |
Есть автоматический режим и ручное управление транзакциями. Транзакции активны в момент передачи изменений на сервер. |
распространение приложений |
Большое количество файлов (объемом около 9 мегабайт). Установка и конфигурирование достаточно сложны и требуют редактирования registry. |
Для распространения нужно две dll, занимающие менее 500 килобайт, которые могут быть вкомпилированы в EXE. Конфигурация хранится в dbxconnections.ini и dbxdrivers.ini |
кросс-платформенность |
Только Windows |
Windows и Linux |
Драйверы от третьих фирм |
Сложно разрабатывать, практически нет |
Легко разрабатывать. Доступно много разных драйверов. За информацией по драйверам обращайтесь на edn.embarcadero.com |
Следующая таблица показывает компоненты dbExpress, которые соответствуют компонентам BDE. Обратите внимание, что в dbExpress нет аналога компоненту BatchMove из BDE.
BDE |
dbExpress |
TDatabase |
TSQLConnection |
TQuery |
TSQLQuery |
TStoredProc |
TSQLStoredProc |
TTable |
TSQLTable |
нет аналога |
TSQLDataSet |
TBatchMove |
нет аналога |
утилита SQL Monitor |
TSQLMonitor |
TSession |
нет |
TUpdateSQL |
нет |
NestedDataSet |
нет |
BDEClientDataSet |
SimpleDataSet |
Механизм TSession отсутствует в dbExpress, и как таковой в dbExpress не нужен. TUpdateSQL используется только при включении CachedUpdates компонента TQuery. в dbExpress, возможности ClientDataSet заменяют cached updates. Возможность обработки вложенных наборов данных встроена в DataSetProvider и ClientDataSet.
Перенос клиент-серверного приложения BDE на dbExpress
Перенос приложений BDE на dbExpress требует выполнения ряда шагов. Многие приложения клиент-сервер не используют компонент TTable. Поскольку локальные приложения BDE интенсивно используют TTable, эта тема описана в следующем разделе.
- Замените компонент TDatabase на компонент SQLConnection
- Замените компоненты TQuery и TStoredProc на компоненты SQLQuery и SQLStoredProc
- Добавьте DataSetProvider и ClientDataSet к каждому SQLQuery или SQLStoredProc, для которых требуется двунаправленная прокрутка или редактирование данных
Замена TDatabase на SQLConnection
Каждый компонент BDE TDatabase должен быть заменен на SQLConnection.
Установка параметров соединения
Если вы используете алиасы BDE, то будет нужен соответствующий алиас для SQLConnection. Наиболее удобный вариант – это хранить параметры коннекта в ini-файле. При этом нужно соответствующим образом запрограммировать чтение параметров из ini-файла в SQLConnection.Params при старте приложения.
Управление транзакциями
Одно из ключевых решений, которые нужно принять перед конвертированием приложений, это как управлять транзакциями. Если ваше приложение BDE никак не управляло транзакциями, т.е. полагалось на автоматический контроль транзакций, то все что это нужно делать – обеспечить вызовы ApplyUpdates в тех местах, где нужно сохранять данные. dbExpress будет управлять транзакциями, так же как и BDE. Если пользователь продолжит работу с тем же набором данных после применения обновления, нужно вызвать метод ClientDataSet.Refresh. Это приведет к повторному выполнению запроса, и помещению новых данных в ClientDataSet. Это позволит пользователю видеть чужие изменения в базе данных.
Если ваше приложение использовало явное управление транзакциями, т. е. вызывало методы StartTransaction, Commit и Rollback, выбор становится более сложным. Один вариант это убрать весь код, управляющий транзакциями, и заменить каждый вызов Commit вызовом ApplyUpdates соответствующих ClientDataSet. При этом dbExpress будет управлять транзакциями самостоятельно, и упростит код. Однако это не будет работать, если изменения двух ClientDataSet надо применить в одной транзакции. Явные вызовы Rollback надо заменить на вызов ClientDataSet.CancelUpdates. CancelUpdates отменяет накопленные в логе изменения ClientDataSet.
Другой вариант – оставить операторы явного управления транзакциями. Это наиболее сложный вариант по трем причинам:
- Вы должны добавить вызовы ApplyUpdates перед каждым Commit (и CancelUpdates при Rollback)
- Вы должны перенести место вызова StartTransaction если хотите получить преимущества коротких транзакций (возможно, перед соответствующими ApplyUpdates)
- При каждом вызове StartTransaction, Commit и Rollback нужно передавать параметр TTransactionDesc
Как видите, меньше работы по удалению кода, явно управляющего транзакциями, чем по его модификации. Давайте посмотрим на эти три пункта подробнее.
После старта транзакции и перед каждым Commit вам нужно добавить вызовы ApplyUpdates всех ClientDataSet, которые участвуют в транзакциях. До вызова ApplyUpdates никакие изменения в базу данных не попадут, поэтому делать Commit не имеет смысла.
В клиент-серверных приложениях BDE управление транзакциями выглядит следующим образом:
- Старт транзакции
- Редактирование данных пользователем
- Commit или Rollback
В этой модели транзакция активна все время, пока пользователь редактирует данные. Одно из главных достоинств dbExpress это короткое время жизни транзакций. Для использования этого достоинства управление транзакциями должно быть изменено на следующую схему:
- Пользователь редактирует данные в ClientDataSet
- Старт транзакции
- Вызов ApplyUpdates
- Commit или Rollback
Для изменения приложения в соответствии с этой схемой, нужно переместить каждый вызов StartTransaction в место, где будет выполняться соответствующий Commit. Затем между ними вставить вызов ApplyUpdates.
Компоненты TDatabase и SQLConnection имеют методы StartTransaction, Commit и Rollback. В отличие от BDE dbExpress позволяет работать с несколькими транзакциями, активными в один и тот же момент. Для поддержки такого режима методы StartTransaction, Commit и Rollback принимают параметр TTransactionDesc. Он объявлен как:
TTransactionDesc = packed record
TransactionID : longword;
GlobalID : longword;
IsolationLevel : TTransactionIsolationLevel;
CustomIsolation: longword;
end;
Для каждой отдельной транзакции вы должны объявить переменную типа TTransactionDesc и установить TransactionId в число, которое должно быть уникально между всеми активными транзакциями. Поле GlobalId используется только в Oracle. IsolationLevel может быть xilDirtyRead, xilReadCommitted или xilRepeatableRead. Поле CustomIsolation пока не поддерживается.
Поскольку BDE не поддерживает более одной одновременной транзакции, все, что вам нужно это объявить одну переменную типа TTransactionDesc в интерфейсной части модуля, который используется всеми остальными модулями приложения (которые используют явные транзакции). В коде, обрабатываемом при старте приложения, установите TransactionID этой переменной в 1, а IsolationLevel в xilReadCommitted или xilRepeatableRead. Далее, измените все вызовы StartTransaction, Commit и Rollback на вызовы, принимающие эту переменную как параметр.
Замена всех компонент DataSet
Самая большая работа – это замена всех компонент DataSet при конвертации приложения. Вы должны заменить все TQuery на SQLQuery и все TStoredProc на TSQLStoredProc. Процесс усложняется тем, что компоненты BDE имеют ряд свойств, которых нет у компонент dbExpress. Все ссылки на отсутствующие свойства и методы в коде должны быть убраны.
Для каждого SQLQuery и SQLStoredProc, требующего двунаправленной прокрутки и возможности редактирования данных, вы должны добавить DataSetProvider и ClientDataSet. Подсоедините обработчики событий, которых нет у SQLQuery и SQLStoredProc, к ClientDataSet. Для минимизации изменений в коде именуйте компоненты ClientDataSet как заменяемые ими TQuery в BDE.
Далее, вернитесь к компонентам SQLQuery и SQLStoredProc, и инициализируйте их столбцы, используя Fields Editor. Установите необходимые ProviderFlags для полей первичного ключа (pdInKey) и обновляемых (или не обновляемых) полей (pfInUpdate и pfInWhere).
Могут быть случаи, когда добавление DataSetProvider и ClientDataSet не требуется. Например, у вас есть запрос, записи которого читаются один раз от первой до последней записи, и записываются в ComboBox или ListBox. В этом случае, поскольку вы сканируете записи в одном направлении, и ничего кроме доступа только для чтения не требуется, вы можете использовать SQLQuery или SQLStoredProc самостоятельно.
Если вы используете CachedUpdates в BDE, удалите все компоненты UpdateSQL. CachedUpdates не нужны в dbExpress, поскольку ClientDataSet и DataSetProvider обеспечивают ту же самую функциональность.
И, наконец, не забудьте добавить вызовы ClientDataSet.ApplyUpdates как это описано в предыдущей части этого документа.
Отображение типов данных
dbExpress использует два новых типа данных, с которыми вы не сталкивались при работе с BDE. Все числовые значения, которые не вмещаются в double precision, будут помещаться в объект TFMTBCDField. Объект TFMTBCDField хранит данные как тип TBCD, являющийся настоящим числом BCD (binary coded decimal) с максимальной точностью в 32 цифры. Для выполнения математических операций над значением в этом типе столбца используйте свойство asVariant.
dbExpress использует TSQLTimeStampField для хранения данных даты и времени. Этот тип хранит данные в переменной типа TSQLTimeStamp. Это запись Delphi, с отдельными полями для года, месяца, дня, часов, минут, секунд и миллисекунд. TSQLTimeStamp позволяет хранить информацию о дате и времени без потерь.
TSQLTimeStampField имеет методы as.... для конвертации даты и времени в другие типы данных.
Замена TBatchMove
В dbExpress нет аналога компонента BDE BatchMove. Если вы используете этот компонент в своих приложениях, то его придется заменить вручную написанным кодом.
Перенос локальных приложений BDE на dbExpress
Если вы переносите на dbExpress приложения, работающие с локальными форматами данных Paradox или dBase, вы столкнетесь с рядом проблем.
Конвертация данных
Поскольку dbExpress не поддерживает таблицы Paradox или dBase, вам придется установить SQL-сервер, поддерживаемый dbExpress, и перенести туда все данные. Есть три способа сделать это:
- Используйте утилиту DataPump, входящую в поставку Delphi или C++Builder
- Используйте утилиты переноса данных третьих фирм (kdv – например, IBDataPump)
- Напишите свою утилиту, переносящую данные из одного формата в другой
Используемый SQL сервер может не поддерживать все типы данных Paradox или dBase. Например, в Paradox есть тип boolean, который есть не во всех SQL-серверах (kdv – boolean есть в
Interbase 7). Также нужно убедиться, что утилита копирования данных правильно соотносит типы данных источника и приемника.
Многие SQL-серверы поддерживают как CHAR, так и VARCHAR, в то время как в настольных СУБД поддерживается один строковый тип. Проверьте, может ли утилита конвертирования указывать на стороне приемника нужный тип для строк.
Вам может потребоваться изменить типы данных по другим причинам. Например, Paradox хранит числа с плавающей точкой только в double. Большинство серверов SQL поддерживают также фиксированный формат, такой как NUMERIC или DECIMAL. В отличие от чисел с плавающей точкой числа с фиксированной точкой не имеют проблем с округлением, однако теряют точность при вычислениях, если таковая требуется более определенной. Опять же, проверьте утилиту конвертирования данных, может ли она выбирать на стороне приемника типы для хранения числовых данных.
Безопасность сервера
Вы не можете получить доступ к данным SQL-сервера, пока не введете правильное имя пользователя и пароль. Возможно, для вашего приложения все пользователи будут иметь одно и то же имя пользователя и пароль, или наоборот, пользователи будут указывать свое имя при старте приложения. Во втором случае придется в код приложения добавлять обработку прав доступа.
Многие настольные форматы данных поддерживают шифрование файлов данных с целью исключения возможности кражи данных. Однако многие SQL-серверы этого не позволяют. Вместо этого они ориентируются на функциональность операционной системы, обеспечивающей безопасность доступа к файлам. Обычно пользователь, которому требуется доступ ко всем данным, является администратором. Для этой цели в SQL-серверах существуют специальные учетные записи. Если же ваше приложение будет работать в "однопользовательском" режиме, например на ноутбуке, убедитесь, что выбранная СУБД обеспечивает нужный уровень безопасности.
Концепция "набора данных"
Обычные настольные приложения позволяют пользователям работать с таблицами целиком, через компонент Table. Этот подход может вызвать серьезные проблемы с производительностью при переходе на SQL-сервер, потому что в этом случае вся таблица должна быть выбрана с сервера в клиентское приложение. Компонент Table также вызывает большой трафик, принимая с сервера различные метаданные, именно поэтому Table и может самостоятельно конструировать запросы на выборку и обновление данных.
Хранение записей в памяти ClientDataSet не является проблемой в правильно спроектированном приложении client-server, поскольку приложение выбирает небольшое количество записей за один раз с сервера. В любом случае, перенос приложения, позволяющего пользователям просматривать таблицы целиком, может вызвать большой расход памяти на клиентской стороне, т.к. вся таблица будет храниться в памяти ClientDataSet.
Когда вы конвертируете приложение, использующее компонент Table, у вас есть три возможности:
- заменить каждый Table компонентами SQLTable, DataSetProvider и ClientDataSet
- заменить каждый Table компонентами SQLQuery, DataSetProvider и ClientDataSet. Выбирать все столбцы и все записи из таблицы.
- заменить каждый Table компонентами SQLQuery, DataSetProvider и ClientDataSet. Переписать приложение для работы в client-server.
Вариант 1 является наиболее простым, т. к. свойства SQLTable почти целиком повторяют свойства Table. Если таблицы не очень большие, то производительность будет достаточной. Недостаток этого варианта в том, что если вы захотите выбирать часть данных с сервера вместо всех данных, вам придется заменить компонент SQLTable на SQLQuery.
Второй вариант более правильный. Даже если вы будете выбирать все столбцы и все записи, ваше приложение теперь будет выполнять указанные вами запросы. Все что нужно будет сделать в дальнейшем, это изменить приложение на работу с наборами записей.
Третий вариант потребует изменения части кода. Приложения, которые созданы для работы в client-server, обычно заставляют пользователя вводить ряд ограничений (фильтров) перед тем как показывать данные. Условия выборки указываются в опции WHERE оператора SELECT, что позволяет выбирать небольшие наборы данных и не нагружать сервер SQL.
Итог
Перенос ваших приложений с BDE на dbExpress дает много преимуществ:
- более короткие транзакции
- больше контроля над сетевым трафиком и использованием ресурсов
- уменьшение размера программы и используемых ею ресурсов
- повышенная производительность
- легкое распространение приложения
- небольшой набор файлов для распространения
- кросс-платформенная разработка (Windows и Linux)
dbExpress минимизирует усилия по переносу приложений, т. к. он включает набор компонент, содержащих свойства и события, похожие на компоненты BDE настолько, насколько это возможно. Это позволяет вам перейти на качественную технологию работы с данными при минимуме изменения исходного кода ваших приложений.
Об авторе
Bill Todd является президентом The Database Group, Inc, обеспечивающей консультации и разработку. Он является соавтором четырех книг по программированию на СУБД и более 90 статей, является членом TeamBorland, предоставляя техническое сопровождение в news-конференциях Borland. Представил более двух десятков документов на Borland Developer Conference. Bill также известен как преподаватель по Interbase и Delphi. Email –
bill@dbinc.com.
Перевод осуществлен с разрешения Borland.
Впервые опубликовано на www.ibase.ru.