12 шагов по переводу приложения Visual FoxPro в архитектуру клиент-сервер

Джим Фалино (Jim Falino)

Приходилось ли вам слышать вопрос: "Сколько времени займет перевод этого приложения в архитектуру клиент-сервер с доступом к SQL Server?" Джиму приходилось. В этой статье, первой из двух, он рассматривает приемы по переводу приложения для обеспечения доступа практически к любому ODBC-источнику. На самом деле, при надлежащем планировании и дизайне, ваше приложение сможет работать и с табли­цами Visual FoxPro. Это даст вам возможность создать и отработать прототип, который затем будет развернут для реальной работы с таблицами сервера баз данных.

Вопрос, вынесенный в аннотацию статьи, в первый раз мне пришлось услышать примерно три года назад. К счастью, вопрос был задан до того как я начал писать код. И это было очень хорошо. Из плохих новостей могу сказать, что тот же самый код должен был работать как с "родными" таблицами Visual FoxPro, так и с таблицами более дорогих серверов баз данных типа SQL Server. Может показаться, что такое требование трудновыполнимо, но, будучи предупрежден, учитывая гибкость Visual FoxPro как средства разработчика, я смог удовлетворить требованиям заказчика.

Основное в таком проекте - это понимание принципов работы приложений архитектуры клиент-сервер. Если понимание достигнуто, остальное довольно просто: вам нужно обеспечить работу кода в обоих сценариях. Учитывая вышесказанное, хочу отметить, что целью настоящей статьи не является рассказ о том, как Visual FoxPro обеспечивает доступ к таблицам SQL Server. Скорее, я постараюсь разобрать проблемы, возникающие при попытке перевести классическое приложение, использующее таблицы на файл-сервере, в архитектуру клиент-сервер.

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

У кого-то может возникнуть недоумение, почему я не рассматриваю использование базы данных самого Visual FoxPro с доступом через ODBC-драйвер. Я пытался идти и таким путем и обнаружил, что ограничения драйвера делают подобные усилия практически бессмысленными. При работе с таблицами, расположенными на файл-сервере, вы, по крайней мере, не ставите себя в рамки ограничений, налагаемых драйвером.

Все дело в представлениях и оболочках

Проблемы, с которыми вы можете столкнуться, я разместил в 12 категорий (все так делают, почему бы и мне не попробовать?) Через все 12 категорий проходит основная тема: представления и оболочки (view & wrapper). Если постоянно помнить об этих двух концепциях, основная часть проблем уйдет. Кроме того, 12 шагов, соответствующих 12 категориям, расположены без определенного порядка. В этом номере мы рассмотрим шаги с первого по шестой, а остальные оставим на другой месяц.

Как вы знаете, представления - это всего лишь SQL-выражения, хранящиеся в базе данных. Они могут быть локальными и удаленными. Локальные представления работают только с таблицами самого Visual FoxPro, тогда как удаленные - с любым ODBC-совместимым источником. То, как это реализовано в Visual FoxPro, позволяет работать с удаленными представлениями практически также, как с локальными. Как вы увидите из дальнейших рассуждений, освоение удаленных представлений - ключ к успеху в разработке приложений, не зависящих от конкретной СУБД.

Еще одним важным моментом является использование оболочек. Под оболочкой я подразумеваю технику программирования, при которой функция или команда, например SKIP, "упакована" в некоторый защитный код и помещена в пользовательскую функцию, так что при каждом использовании вам не придется переписывать обработку ошибок. Например, функция SafeSkip может выглядеть так:

FUNCTION SafeSkip

  LPARAMETERS cAlias

  llMoved = .F.

  * Позволяет избежать возникновения

  * ошибки "EOF Encountered"

  IF !EOF(cAlias)

    SKIP IN (cAlias)

    IF EOF(cAlias)

      * Возвращаемся на одну запись

      SKIP -1 IN (cAlias)

    ELSE

      llMoved = .T.

    ENDIF

  ENDIF

  RETURN llMoved

Я полагаю, что любители тонкого анализа кода могут представить приведенный выше пример как ненужное украшательство. Как бы то ни было, создание связок вместо жесткого кодирования должно стать обязательным правилом, иначе у вас возникнут проблемы при изменении принципов работы с сервером. Возможно, вам придется писать код, обрабатывающий условную логику, и чем больше источников данных вы планируете охватить, тем больше понадобится такого кода. Это дает следующие преимущества:  

Шаг 1 - Формы ввода данных на основе представлений

При обращении к серверу вы можете использовать либо сквозной (pass-through) запрос, представляющий собой просто строку с командой SQL, передаваемую ODBC-драйверу, либо удаленное представление, которое описывается и хранится в базе данных Visual FoxPro и при использовании создает временную таблицу, по своей функциональности очень похожую на обычную таблицу. Результат исполнения сквозного запроса помещается в локальный курсор, что требует программной привязки объектов формы к полям курсора (например, ThisForm.txtCusto­mer.Value = SQLResult.Customer). Соответственно, для внесения модификаций в базу данных на сервере вам придется вручную создавать необходимые SQL-предложения - довольно непростой и потенциально чреватый ошибками процесс. Так обычно поступают программисты на Visual Basic, разработчики на Visual FoxPro имеют более удобные варианты.

С другой стороны, удаленные представления очень просты в работе. Все что нужно - это создать представление с такой же структурой, что и основная таблица, открыть его (при этом исполняется SQL SELECT) и работать с результатом также, как с обычной таблицей Visual FoxPro. По окончании работы вызовите функцию TABLEUPDATE(), которая автоматически создает необходимые команды SELECT, INSERT или UPDATE и посылает их на сервер через диспетчер драйверов ODBC. Драйвер преобразует команду в синтаксис, понятный серверу. Вот и все. Для форм ввода данных я рекомендую использовать удаленные представления по целому ряду причин:

Тем не менее, сквозные запросы остаются важным компонентом приложений, работающих в архитектуре клиент-сервер, более подробно я поговорю о них на шаге 3. Дополнительную информацию вы сможете найти в документации по Visual FoxPro.

Шаг 2 - все команды, связанные с данными должны проходить через одну функцию

Чтобы максимально осложнить себе задачу перевода приложения в архитектуру клиент-сервер, используйте явно прописанные команды типа ZAP, PACK, REINDEX, SEEK и т. д. Иными словами, не найдется ли у вас чего-нибудь подобного?

FUNCTION PurgeOrders

  * Эта функция удаляет заказы,

  * отправленные до указанной даты

  LPARAMETER dShipDate

  locl lcMsg

  SELECT COUNT(*) AS OrderCnt FROM Orders ;

    WHERE ShipDate <= dShipDate INTO CURSOR tcPurge

  IF _TALLY = 0

    lcMsg = "Нет заказов"

  ELSE

    DELETE FROM Orders WHERE ShipDate <= dShipDate

    lcMsg = TRIM(STR(_TALLY)) + ;

      " заказов удалено из системы"

  ENDIF

  MESSAGEBOX(lcMsg)

ENDFUNC

Указанные команды SQL хороши только для доступа к таблицам, которые могут быть найдены в текущем списке каталогов (path). В мире клиент-серверных приложений вы общаетесь с источником данных через указатель (handle) на текущее соединение. Удаленные представления используют описание источника данных (через DSN или строку соединения) в определении самого представления. Функция SQLExec() использует указатель соединения. Функция возвращает -1, если во время исполнения была получена ошибка, 1, если все прошло нормально, и 0, если запрос исполняется асинхронно и исполнение еще не закончено. Вот как можно получить курсор со списком продаж для указанного заказчика с использованием сквозного запроса:

 llSuccess = SQLExec(goEnv.nHandle, ;

   "Select * From ORDERS Where Customer = ?cCustomer",;

   "tcOrders") > 0 

Если вы используете функцию-оболочку для исполнения SQL-выражений в функции PurgeOrders, то представленная выше команда может быть использована для получения данных из базы SQL Server, данных на файл-сервере или таблиц на локальном компьютере. Можно создать набор классов или просто функцию, которые сделают ваши запросы независимыми от местоположения информации. Вот как функция PurgeOrders должна выглядеть в мире клиент-сервер:

 Function PurgeOrders

 * Функция удаляет заказы, отправленные

 * до указанной даты

 LParameter dShipDate

 Local lcSQLStr

 lcSQLStr = "Select Count(*) as OrderCnt " + ;

   "from Orders " + ;

   "Where ShipDate <= ?dShipDate"

 This.SQLExecute(lcSQLStr, "tcPurge")

 If Reccount("tcPurge") = 0

   lcMsg = "There are no orders to purge."

 Else

   lcSQLStr = "Delete from Orders ;

     Where ShipDate <= ?dShipDate"

   This.SQLExecute (lcSQLStr)

   lcMsg = Trim(Str(Reccount("tcPurge"))) ;

   + " orders were purged from the system."

 Endif

 MessageBox(lcMsg)

 EndFunc

 

 Function SQLExecute

 * Оболочка для функции SQLExec

 LParameters cExecuteStr, cCursor

 Local llSuccess

 cCursor = Iif(PCount() = 1, "SQLResult", cCursor)

 llSuccess = .T.

 If goEnv.lRemote

   llSuccess = (SQLExec(goEnv.nHandle, ;

     cExecuteStr, cCursor) > 0)

 Else  && Локальное исполнение, используем макро для cExecuteStr

   * Добавим специфическое для Visual FoxPro

   * предложение "Into Cursor..."

   If Upper(Left(cExecuteStr,6)) = "SELECT"

     cExecuteStr = cExecuteStr + ;

       " Into Cursor " + cCursor + " NoFilter"

   Endif

   * Здесь следует поместить обработчик ошибок, для

   * перехвата результата llSuccess

   &cExecuteStr

 Endif

 Return llSuccess

 EndFunc

Вы должны заметить, что вместо _TALLY я использую RECCOUNT(). Курсоры, создаваемые при исполнении сквозных запросов, не обновляют значение переменной _TALLY, так что вам придется забыть об ее использовании. Однако не стоит беспокоиться насчет удаленных записей, попавших в результат, возвращаемый RECCOUNT(), так как серверы баз данных не используют двухэтапное удаление записей (DELETE + PACK). Если же вы обратились к БД Visual FoxPro, применение предложения NOFILTER создаст реальный курсор, пригодный для дальнейшей работы, вместо фильтра на реальную таблицу.

Вторая ошибка, которая может привести к проблемам при переводе системы в архитектуру клиент-сервер, - внедрение функций Visual FoxPro в команды SELECT. Если у вас имеется оболочка, подобная описанной выше, вы сможете разобрать команду и привести ее в понятный серверу вид. Так, функция Transact-SQL Convert() используется для преобразования типов данных. Возможно, вы захотите поменять ваши STR() и VAL() на Convert(). Если делать это в коде, то функция STRTRAN() позволит разобрать строку команды и выполнить замену. Еще один вариант - удалить все функции Visual FoxPro из команд SELECT и использовать их на локальных курсорах, полученных в результате исполнения SELECT.

Шаг 3 - использование хранимых процедур или параметризированных запросов

Если рассматривать скорость исполнения, быстрее всего исполняются хранимые процедуры. Так как они уже прошли компиляцию и живут на одном сервере с таблицами, что еще может сравниться с ними по эффективности? Так как синтаксис передачи параметров может отличаться для разных серверов, использование оболочки снова окажется весьма кстати. Например, обратите внимание на отличия синтаксиса в передаче параметров для Visual FoxPro и SQL Server:

SQLExec ("usp_GetOrders ('Acme')") && VFP

SQLExec ("usp_GetOrders 'Acme'") && SQL Server

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

Если же вы не хотите идти по пути использования хранимых процедур, можно использовать параметризированные запросы. Это позволит избежать беспокойства по поводу специфических для сервера вызовов хранимых процедур или переписывания кода самих процедур для каждого типа сервера. При этом потеря производительности может быть компенсирована простотой сопровождения.

Вы уже встречались с параметризированными запросами - это всего лишь команда в стиле сквозного запроса:

 llSuccess = SQLExec(goEnv.nHandle, ;

   "Select * From Orders Where Customer ;

     = ?cCustomer", "tcOrders") > 0

Обратите внимание на условие выборки: Customer = ?cCustomer. Этот синтаксис очень важен, так как большинство серверов поддерживает указанную нотацию и вам не придется встраивать параметры в специфическую для сервера команду:

 Case lSQLServer

   "...Customer = '" + cCustomer + "' And ;

     shipdate <= '" + DTOS(dShipdate) + "'"

 Case VFP

   "...Customer = '" + cCustomer + "' And ;

     shipdate <= {" + DTOS(dShipdate) + "}"

Еще одним преимуществом является то, что вы не встретитесь с проблемой вложенных кавычек. Такая проблема возникает, когда вы пытаетесь построить команду, а параметр имеет вложенную кавычку (типа "Jim's"). Так как значение параметра раскрывается до того, как строка отправлена на сервер, вы получаете итоговую команду с неправильным набором кавычек и сообщение об ошибке исполнения. Как видите, использование параметров значительно сокращает объем кода, и я настоятельно рекомендую этот подход.

Если вы планируете использовать произвольные запросы, я рекомендую сквозной запрос вместо представления, особенно если вы не планируете модифицировать данные на сервере. Представления часто создают для использования в отчетах, но для крупной системы накладные расходы по использованию представлений перевешивают их удобства. Вместо представления можно использовать сквозной запрос или хранимую процедуру (кроме случаев, когда вам необходимо предоставить пользователю данные из представления на сервере). При использовании представлений клиентская база данных (DBC Visual FoxPro) может оказаться заполненной ненужной информацией, так как в данном случае представление не требует дополнительных настроек.

Шаг 4 - Постарайтесь уменьшить объем данных, получаемых с сервера

При работе в режиме клиент-сервер информация, передаваемая на рабочую станцию, должна быть минимальна. Это значит, что формы следует открывать без предварительной привязки к данным. Такой подход позволит ускорить загрузку и самой формы, даже после того как объем базы данных перерастет возможности Visual FoxPro.

Если подумать, то наиболее разумным подходом будет передача пользователю только той информации, которая ему действительно нужна. Зачем выводить в Grid 10,000 заказов, если пользователь все равно работает только с одним? Мне кажется, что скорость исполнения утилит, обеспечивающих поиск с уточнением, испортила наш стиль программирования. Для небольших систем такой подход еще годится, но по мере того как объем данных и число пользователей растет, он превратится в основную причину замедления.

Параметризированные представления

Итак, ваши формы открываются без предварительной привязки к данным. Как же заполнить их информацией? Необходимо предоставить пользователям средства определения критериев фильтрации. Эти значения будут использованы в качестве параметров предложения WHERE в определении представления. Подобная функциональность может быть реализована несколькими способами, от очень простых до весьма сложных. Простой способ заключается в привязке формы к параметризированному представлению. Когда пользователь входит в режим поиска, вы выводите вспомогательный диалог для ввода значения параметра. Например:

 

 * Создайте это представление при создании формы

 Create SQL View vOrders As Select * From Orders 

   Where Customer = ?cCustomer

 * Откройте представление (без данных) в окружении данных

 * методе Load при запуске формы

 Use vOrders NoData

 * Получите значение параметра в режиме поиска

 cCustomer = thisform.txtCustomer.Value

 Requery()  && получите данные с сервера

Другой способ, предоставляющий больше гибкости, использует технологию Query By Form (запрос в форме). Это означает, что в режиме поиска вы предоставляете пользователю возможность ввести критерии поиска во все (или некоторые) поля формы. Затем значения полей используются для формирования предложения WHERE, которое может применяться по-разному:

********** Tip ***************

Если вы обратитесь по адресу www.hallogram.com/qbfbuilder  или www.stonefield.comто сможете найти два инструмента, позволяющие создавать запросы в форме.

****************************

Обработка объектов формы, привязанных к справочным таблицам  

Ограничение объема данных, передаваемых по сети, можно организовать и на уровне индивидуальных объектов формы. Какой смысл ограничивать список заказчиков единственным наименованием, если одновременно вам приходится заполнять несколько справочных списков информацией с сервера? В приложении клиент-сервер нет места спискам с тысячами строк. Обойти указанную проблему можно, по крайней мере, двумя путями. Первый - не использовать списки, привязанные к большим таблицам, а заменить их полями ввода. При нажатии соответствующей комбинации клавиш или кнопки вы запускаете еще одну форму с Grid, где размещена нужная информация.

Если вы настаиваете на использовании списков, можно обратиться ко второму пути - не заполняйте список до тех пор, пока не сработает метод GotFocus. Позднее связывание сэкономит время на заполнение объектов, которыми редко пользуются. В зависимости от размера справочной таблицы можно использовать комбинацию двух подходов и управлять поведением формы через свойства или таблицу настроек.

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

Формы со связанными таблицами

Формы, отображающие данные из таблиц, связанных в отношении один-ко-многим, следует организовать следующим образом. Для родительской таблицы следует использовать ту технику, которую вы применяете для обычных форм. Информация из дочерних таблиц должна отображаться средствами представлений, параметризированных по уникальному идентификатору записи из родительской таблицы. В результате, при перемещении от одной записи родительской таблицы к другой, представление из дочерней таблицы следует обновлять. Такой подход позволяет ограничить передачу по сети только тех записей дочерней таблицы, которые соответствуют текущей записи "у родителя".

Шаг 5 - Храните представления в отдельной БД, чтобы не зависеть от сервера

Если для каждого набора представлений, работающих с определенным сервером, создавать отдельную базу данных, переход от одного сервера к другому будет менее болезненным. Иными словами, при переходе от одного сервера к другому вам следует сохранить как можно больше компонентов приложения неизменными. Если вы используете в качестве источника данных таблицы Visual FoxPro, то размещение представлений в самостоятельном контейнере, отдельно от таблиц, позволит перейти к другому источнику просто и быстро. Реализация может быть такой: 

Возможно, вам стоит добавить в файл инициализации имя контейнера базы данных, но, скорее всего, он и так напрямую прописан в коде программы. Разделение представлений по самостоятельным базам данных позволит лучше управлять размером контейнера. Если приложение обращается к таблицам на файл-сервере, то подключение нового источника удвоит число объектов в контейнере, и для большого числа объектов команда MODIFY Database будет исполняться очень долго. Нужно также отметить, что контейнер, в котором хранятся описания представлений, всегда расположен на локальном компьютере, а не на сервере с многопользовательским доступом. Использование описанной технологии обеспечивает высокую степень гибкости, как я покажу позднее.

Шаг 6 - Для поиска используйте LOCATE, а не SEEK

Так как доступ к данным в системах архитектуры клиент-сервер организуется только средствами SQL-предложений, вы теряете возможность использования SEEK для поиска нужной записи. Но утрите слезы, пользуйтесь LOCATE. В нормально спроектированной системе клиент-сервер локальные наборы данных относительно малы. Это значит, что команда LOCATE вполне пригодна для поиска в локальном курсоре или временной таблице.

Более того, результат исполнения локального представления на самом деле представляет собой временную таблицу. Откройте любое представление и выполните команду: ? DBF(). В результате вы получите нечто вроде E:\TEMP\4F1W000O.TMP. Это уже нечто материальное. Если же "оно" материальное, мы можем построить индекс. При наличии индекса команда SEEK будет в вашем распоряжении. Тем не менее, я склонен использовать LOCATE. Эта команда также оптимизируется Rushmore и она не перестает работать при отсутствии индексов. Разницы в скорости вы, скорее всего, не заметите.

Заключение

Ну вот, мы добрались до середины. Запомните два слова - "представления" и "оболочки" - это два ключевых компонента разработки приложений клиент-сервер. Их использование обеспечит вам гибкость и масштабируемость решений. В следующем номере я рассмотрю следующие шесть шагов и завершу изложение примером формы, способной работать как с таблицами Visual FoxPro, так и с базой данных SQL Server. После этого вы начинаете самостоятельную жизнь. До встречи в следующем номере.

Джим Фалино - счастливый разработчик на инструментах семейства Fox с 1991 года, когда он начал использовать FoxBASE+. Джим имеет сертификацию Microsoft Certified Professional по Visual FoxPro и вице-президент Профессиональной ассоциации разработчиков баз данных в регионе New York. В течение последних трех лет он был менеджером проекта по разработке очень крупного приложения, использующего в качестве клиентской части Visual FoxPro. Его адрес - jim@garpac.com.

 


Возврат к списку статей

Возврат на главную страницу

© Edel Ltd. Все права защищены. 1999 г.