Новости :

Как сделать exe файл

Как сделать exe файл

Содержание
1.Как скомпилировать EXE
1.1 Технология компиляции
1.2 Сопутствующие файлы
2.Что нужно включить в EXE


1.Как скомпилировать EXE
1.1 Технология компиляции
Эпиграф:
"Ай да Пушкин, ай да..."
А.С.Пушкин


Технология компиляции EXE в Visual FoxPro довольно проста. Мы не будем отвлекаться на такие экстремальные методы как ввод в командном окне команд типа BUILD PROJECT, BUILD APP, BUILD EXE, а ограничимся удобствами, предоставляемыми графическим интерфейсом VFP.
Итак, для создания EXE нам понадобится нажать в менеджере проекта кнопку "BUILD...". Выбрав в открывшемся диалоговом окне опцию Win32 executable / COM server (EXE)давим OK. ВСЕ. ;-)
"Как все?"-- спросите Вы -- "А указать под каким именем сохранить файл?"
Согласен... Надо указать обязательно, иначе все насмарку.
Кстати, думаю, что нелишним будет напомнить о том, что для того чтобы процесс компиляции прошел успешно необходимо менеджер проекта заполнить файлами Вашей программы, один из которых обязательно должен быть как main (в менеджере проектов имя этого файла выделено жирным шрифтом), т.е. файл который будет исполнен первым. По умолчанию как main-программа устанавливается первый включенный в проект программный файл (*.prg) или файл формы (*.scx).
Вобщем все довольно просто! Просто..?
Теперь о грустном...

1.2 Сопутствующие файлы
Эпиграф:
"... а включаешь - не работает!"
М.Жванецкий


Все перепетии этапа компиляции приложения уже позади, и Вы, гордый от сознания собственного превосходства над простыми смертными, семимильными шагами несетесь к заказчику, устанавливаете свое приложение, попутно расписывая ему(заказчику) как теперь у него все будет круто, и подсчитывая в уме какой винт нужно купить, что бы осталось на пиво, вдруг натыкаетесь на то, что Ваша супер-пупер прога не фурычит. Грустно. Обидно. А главное не будет у Вас нового вичестера или Чего Вы Там Себе запланировали купить с гонорара...

Все дело в том, что скомпилированный нами .exe не есть самодостаточным исполняемым файлом. Для нормальной работы этому псевдо-исполняемому файлу необходимы библиотеки поддержки (.dll). Я не буду сильно вдаваться в рассуждения об этом, а просто приведу названия файлов, необходимых для нормальной работы приложения.
В принципе до 6-й версии включительно для переноса приложения на клиентскую машину можно использовать штатный Setup Wizard, который самостоятельно все прописывает, а в последующих --InstallShield(или другой инсталятор).
Все приведенные ниже файлы есть минимально-необходимый набор файлов для различных версий VFP (по материалам http://www.foxclub.ru):

VFP 8.0          msvcr70.dll, vfp8r.dll, vfp8rrus.dll, gdiplus.dll
VFP 7.0                msvcr70.dll, vfp7r.dll, vfp7rrus.dll
VFP 6.0                vfp6r.dll, vfp6renu.dll , vfp6rrus.dll
VFP 5.0                vfpole50.dll, vfpodbc.dll, vfp500.dll, vfp5rus.dll, Foxpro.int

Указанный выше набор библиотек необходимо разместить в каталоге программы или в системном каталоге Windows. Для различных версий Windows эти каталоги различны:

Win9x, WinMe                 c:\Windows\SYSTEM
WinNT,Win2000,WinXP            c:\Windows\SYSTEM32



2.Что нужно включить в EXE
Эпиграф:
"To include or not to include?"
пьяный электрик


Все изложенное ниже является личным мнением автора, которым Вы можете пренебречь. Но делая по-своему помните: биться головой в бетонную стену гораздо больнее, чем воспользоваться проломом сделаным до Вас.
По умолчанию в готовый .exe включаются все файлы, необходимые для работы приложения - программы, формы, меню, библиотеки, классы и т.д. кроме файлов баз данных и таблиц (такие файлы можно узнать по перечеркнутому кружочку возле имени файла в менеджере проекта). Все включенные в .exe файлы после компиляции станут немодифицируемыми (доступными только для чтения). Понятно, что файлы таблиц (*.dbf) включать не стоит поскольку они подвергаются постоянным операциям изменения-дополнения данных, но иногда бывают случаи когда необходимо скрыть какие-либо статическиеданные от пользователя, но обеспечить доступ к ним самой программы. Лучшего места чем "внутренности" exe-шника не придумать.
Так же довольно нередкая ситуация, когда нужно "находу", или временно, изменить файл отчета, или когда имеем целый набор отчетов, уникальных для каждого пользователя. Ну в самом деле представьте: десять операторов и каждому по десять отчетов и все это в *.exe. единственным выходом в этой ситуации будет исключить эти отчеты из конечного файла и поставлять их вместе с приложением.
В любом случае решение о том какие файлы исключить, а какие оставить за Вами.

Все, пиво кончилось, надо идти в гастроном.

С уважением, Владислав Кулак www.foxclub.ru

Комментарии: (1) | FoxPro | 2006-06-09

Быстродействие программ на VFP

Быстродействие программ на VFP

Важное замечание
Пользовательский интерфейс
Rushmore
Навигационный и реляционный подходы
Оптимизация запросов
Нормализация БД
Алгоритм
Массивы (Arrays)
Работа с таблицами
Скорость выполнения отдельных команд
Пример
Как успокоить пользователя
Тестирование
Благодарности

Здесь вы можете скачать тексты демонстрационных программ.

Важное замечание

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

Пользовательский интерфейс

Если пользователи жалуются на слишком медленную работу форм, вы можете предпринять следующие действия:

  • Загружайте элементы интерфейса только тогда, когда в них появится необходимость. Это могут быть элементы на неактивных закладках PageFrame, скрытые узлы Treeview, или невидимые формы в FormSet. Практически любой набор объектов можно поместить в контейнер, сохранить его, как класс, а в нужный момент создать экземпляр этого класса.

  • Можно поступить "с точностью до наоборот" - при загрузке программы создать самые тяжелые и часто используемые объекты, и сделать их невидимыми до определенного момента. Обращали внимание, как долго загружается программа 1С? Зато потом скорость работы вполне приемлемая. Это пример такого подхода.

  • Если в большой таблице наложить фильтр, установить активный индекс, и отобразить результат через Browse или Grid, то переход между строками будет происходить очень медленно. Более подходящий источник для отфильтрованных и отсортированных данных - Local View.

  • Если вам не нужны все возможности Grid-а, замените его на Listbox. Этот контрол намного легче и быстрее. Кроме того, в нем нет проблем с подсветкой строки, которые имели место до 8-й версии VFP.

  • Установите последние обновления для сети и операционной системы. В моей практике было, когда для первоначального открытия десяти таблиц на сервере Novell требовалось полминуты. После установки на рабочих станциях патча для работы с Novell открытие таблиц стало происходить мгновенно.

Rushmore

Как использовать индексы для оптимизации Rushmore, описано в разделе HELP "Using Rushmore Query Optimization to Speed Data Access". В этой же статье вы найдете список команд, которые могут быть оптимизированы.

Оптимизация не будет работать, если:

  • Не совпадает Set Collate (в индексах таблицы используется Russian, а системная установка - Machine)

  • Индекс содержит условие FOR ... или ключевое слово UNIQUE

  • Индексное выражение включает условие .NOT., например Index on .NOT. DELETED()

  • Выражение для поиска не совпадает с индексным. Часто допускают такую ошибку. Предположим, у таблицы есть индекс по UPPER(cField1). Для поиска пустых полей используют условие EMPTY(cField1) = .T., что не совпадает с индексным выражением. Для того, чтобы поиск был оптимизированным, выражение должно выглядеть как UPPER(cField1) = SPACE(N), где N - длина поля cField1.

Навигационный и реляционный подходы

В FoxPro сосуществуют два подхода к обработке данных - навигационный и реляционный.Навигационные команды - это Locate, Seek, Set Relation, Set Order, GoTo. Основная реляционная команда - Select SQL. Скорость выполнения запросов SQL весьма высока, однако во многих случаях навигационный подход работает еще быстрее.

Пример поиска максимального значения поля различными способами:

*** поиск с помощью Select SQL
Select MAX(Field1) as MaxField From Table1 Into Cursor _Temp
? _Temp.MaxField

*** а так выполнится значительно быстрее (необходимо наличие индекса по полю Field1)
Select Table1
Set Order To Field1
Go Bottom
? Field1

Оптимизация запросов

Запросы на FoxPro могут выполняться долго, если в них участвует больше трех-четырех таблиц. Речь идет именно о запросах к родным БД на dbf, а не о запросах, отправляемых на сервер. Как правило, увеличить скорость путем разбиения большого запроса на несколько последовательно выполняемых подзапросов не удается. Можно выйти из положения, исключая из запроса справочные таблицы (например, ФИО сотрудников), и привязывая их к результату через Set Relation, смешивая таким образом "реляционный" и "навигационный" подходы.

Использование Outer join снижает скорость выполнения запроса. Если нет прямой необходимости в этой опции, лучше переписать запрос без нее. Как вариант - если у вас есть строки, не имеющие соответствия в связанной таблице, и потому содержащие в поле для связи .NULL., добавьте в связанную таблицу строку с нулевым кодом, а в основной - замените пустые значения на этот нулевой код.

Подзапросы вида Where Field1 in (Select ... ) или Where Exists (Select ... ) могут замедлить скорость выполнения запроса (хотя это зависит от многих параметров). Если такой запрос выполняется слишком медленно - попробуйте переписать его с использованием простого объединения.

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

Для анализа оптимальности SQL-запроса воспользуйтесь функцией Sys(3054). Имейте в виду, что если у таблиц нет индексов по Deleted(), эта функция будет показывать частичную оптимизацию, даже если фактически они вносят отрицательный вклад в скорость выполнения запроса. Этот эффект подробно описан Владимиром Максимовым.

Нормализация БД

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

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

Другой вариант - предварительные расчеты. Например, с помощью триггеров можно заранее пересчитывать суммарные данные о продажах за день в момент добавления или корректировки нового счета. К примеру, пусть счета хранятся в таблице Invoices, а данные о продажах за день - в таблице DailySales. Тогда в триггеры таблицы Invoices нужно добавить примерно такой код:

*** для триггера Insert
Update DailySales Set TolalSum = TolalSum + Invoices.InvoiceSum Where DailySales.SalesDate = Invoices.InvoiceDate

*** для триггера Update
Update DailySales Set TolalSum = TolalSum + Invoices.InvoicesSum - OldVal(Invoices.InvoiceSum) Where DailySales.SalesDate = Invoices.InvoiceDate

*** для триггера Delete
Update DailySales Set TolalSum = TolalSum - Invoices.InvoiceSum Where DailySales.SalesDate = Invoices.InvoiceDate

Естественно, сам код триггеров будет намного сложнее. Кроме приведенных строк там необходимы проверки, обработка ошибок и т.д.

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

Алгоритм

Как правило, стройный и понятный алгоритм выполняется быстрее всего, хотя бывают исключения. С другой стороны, применяя "обходные маневры" вы можете потерять возможность исправить что либо в коде, когда условия изменятся.

Вот несколько общих советов:

  • Вынесите за пределы циклов однократно выполняемые установки типа Set deleted, Set exact, и т.д.

  • Поместите проверки в начало подпрограммы. Это позволит не выполнять лишние действия, если проверки окончатся неудачно.

  • Освобождайте память от объектов и таблиц, которые вам больше не понадобятся.

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

Массивы (Arrays)

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

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

Работа с таблицами

Как уже говорилось, основная задача VFP - работа с таблицами. Здесь перечислены приемы, которыми вы можете воспользоваться для ускорения обработки данных:
  • Поиск записей с помощью команды Seek (или ее аналога функции SEEK( ) ) выполняется примерно в 5-10 раз быстрее, чем с помощью команды Locate.

  • Команда Locate позволяет указывать несколько условий, например Locate for ClientID = X AND InvoiceDate = Y. Для максимально быстрого поиска по нескольким полям заведите сложный индекс, в данном случае - STR(ClientID, 10, 0) + DTOS(InvoiceDate), и осуществляйте поиск с помощью Seek STR(X, 10, 0) + DTOS(Y).

  • "Быстрая" команда Seek может работать с любыми индексами, в том числе содержащими ключевое слово UNIQUE или условие FOR.... Однако имейте ввиду, что сложные индексы затрудняют читабельность и модификацию кода.

  • Если нужно только проверить наличие строк, удовлетворяющих условию, по которому есть индекс, но нет необходимости перемещать указатель на эти строки, воспользуйтесь функцией IndexSeek() (она появилась в VFP начиная с 6-й версии).

  • Команда Goto ... выполняется практически мгновенно. Если вам нужно вернуться к какой-либо записи в таблице - запомните текущее значение RECNO(). Возврат по номеру записи отработает быстрее, чем поиск по идентификатору с помощью Seek или Locate.

  • Цикл Scan...EndScan оптимизирует перебор записей таблицы даже без индекса. Там где это возможно, используйте его вместо конструкции Do While not EOF() ... Skip ... EndDo.

  • Выполнение команд Locate, Scan For.., Set Filter to.. замедлится, если у таблицы установлен активный индекс. Отключите индекс командой Set Order to без параметров, если порядок перебора строк неважен.

  • Команда Set Filter to.. входит в число Rushmore-оптимизируемых команд, и в "лабораторных условиях" скорость ее работы вполне удовлетворительна. Тем не менее, эта команда очень чувствительна к "внешним факторам", например, к установке активного индекса. Поэтому в большинстве случаев использование фильтров негативно сказывается на быстродействии.

  • Если вам необходимо просканировать некоторый диапазон значений в таблице, и по этим значениям есть индекс, вы можете воспользоваться приемом, известным как "Set Order + Seek + Scan while".
    Set Order to Field1
    Seek StartValue && устанавливаем указатель на начало диапазона
    Scan While Field1 =< EndValue && перебираем строки до конца диапазона
    ...
    EndScan
    
    Этот код работает быстрее, чем Scan for Between( Field1, StartValue, EndValue ) ... EndScan, но проигрывает ему в читабельности, и кроме того, требует установки активного индекса.

  • Если какой-либо индекс в таблице используется чаще других, во время технического обслуживания БД физически отсортируйте записи в таблице в порядке этого индекса. (Я никогда не использовал этот прием на практике, однако такой совет есть в HELP, в статье "Optimization of Tables and Indexes").

Скорость выполнения отдельных команд

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

  • Там, где это возможно, вместо макроподстановки & используйте выражения имени ( ) и функцию EVALUATE(). Выражения имени можно применять там, где Fox ожидает увидеть имя переменной или поля. Функция EVALUATE() может быть использована в операциях присвоения и вычислениях.
    X = "Var1"
    Y = "Var2"
    
    Store EVALUATE(X) to (Y) && так быстрее
    
    &Y = &X && эта команда сделает то же, что и предыдущая, но существенно медленнее
    
  • Если команда состоит из несколько макроподстановок, лучше собрать части команды в одну переменную, и выполнить единой макроподстановкой
    *** медленный вариант
    Select &FieldsList From &TablesList Where &JoinConditions
    
    *** так быстрее
    cSQL = "Select " + FieldsList + " From " + TablesList + " Where " + JoinConditions
    
    &cSQL
    
  • Стандартные функции VFP хорошо оптимизированы и выполняются достаточно быстро. Например, условиями задачи требуется найти цифру, входящую в текстовую строку. Можно по очереди перебирать символы:
    For I = 1 to LEN(cText)
    
           If ISDIGIT( SUBSTR(cText, m.I,1) )
                   ? m.I
                   Exit
           EndIf
    
    EndFor
    
    Более быстрый вариант с использованием стандартных функций:
    cText1 = STRTRAN(cText, "123456789", "0") ? AT(cText1, "0")
    
  • Скорость обращения к переменным выше, чем к свойствам объекта или полям таблицы. Если значение какого-то свойства или поля используется многократно, присвойте это значение переменной.

  • Подавление системных сообщений с помощью команд Set Talk off, Set Notify off (в последних версиях еще и Set Notify Cursor off) может существенно ускорить процесс.

  • Сложение текстовых строк происходит быстрее, если к длинной строке добавлять короткую, а не наоборот. То есть
    *** этот код выполнится быстро
    x = ""
    For i = 1 to 10000
           x = x + Sys(3) && функция Sys(3) играет роль "короткого текстового выражения"
    EndFor
    
    *** а этот - медленно
    x = ""
    For i = 1 to 10000
           x = Sys(3) + x
    EndFor
    
  • Для поиска "узких мест" воспользуйтесь командой Set Coverage to. Эта команда создаст текстовый файл, в котором будет записано время выполнения каждой строки. Вы можете сохранить этот файл в таблицу, и проанализировать ее. Либо воспользоваться Coverage profiler из дистрибутива Visual FoxPro.

Пример

Предположим, вам необходимо всем клиентам, у которых есть заказ в предыдущем месяце, предоставить скидку 10% на будущие заказы. Данные о заказах хранятся в таблице Invoices, а информация о скидке - в таблице Clients.

New_Discount = 0.10 && скидка, которую мы хотим предоставить

PrevDate = GoMonth(Date(), -1)
PrevMonth = Month(PrevDate) && предыдущий месяц
PrevYear = Year(PrevDate) && год, в котором был предыдущий месяц


Go top in Clients
Do while not EOF("Clients") && перебор записей в таблице клиентов

       Go top in Invoices

       Do while not EOF("Invoices") && перебор записей в таблице счетов

               If Invoices.ClientID = Clients.ClientID AND ;
               Year(Invoices.InvoiceDate) = PrevYear AND ;
               Month(Invoices.InvoiceDate) = PrevMonth

                       Replace Clients.Discount with New_Discount
                       Exit

               EndIf

               Skip in Invoices

       EndDo

       Skip in Clients

EndDo

Приведенный код логически верен, но абсолютно не оптимизируем.

Попробуем переписать этот код:

Month_End = Date() - Day(Date()) && последний день предыдущего месяца
Month_Start = Month_End - Day(Month_End) + 1 && первый день предыдущего месяца

Select Clients

Scan
       Select Invoices
       Locate for ClientID = Clients.ClientID AND Between(InvoiceDate, Month_Start, Month_End)

       If Found("Invoices")
               Replace Clients.Discount with New_Discount
       Endif
EndScan

Перебор записей таблицы Clients осуществляется командой Scan. Поиск в таблице Invoices может быть оптимизирован с помощью индексов по полям ClientID и InvoiceDate. Этот вариант будет выполняться существенно быстрее.

Для реализации следующего варианта необходимо, чтобы у таблицы Invoices существовал особый индекс:

Index on STR(ClientID, 10,0) + DTOS(InvoiceDate) tag MIXED
...
...
...
Set Exact OFF

LastMonth  = LEFT(DTOS( GOMONTH(DATE(), -1) ), 6) && год и месяц в формате "YYYYMM"

Select Clients
Replace Clients.Discount with New_Discount For IndexSeek( STR(ClientID, 10,0) + m.LastMonth, .F., "Invoices", "MIXED" )

Поиск в таблице Invoices осуществляется по индексу, поэтому этот пример отработает еще быстрее. Но индекс весьма специфический, и не может быть применен для других вычислений. Кроме того, даже автору это кода этого потребуется немало времени, чтобы спустя полгода разобраться, что именно он делает :-)

Решение с использованием команд SQL:

Update Clients Set Discount = New_Discount Where Exists ;
(Select * From Invoices Where ;
Invoices.ClientID = Clients.ClientID AND ;
Invoices.InvoiceDate BETWEEN Month_Start AND Month_End)

Возможно, данный вариант не выиграет с точки зрения скорости по сравнению с предыдущим, но он гораздо понятнее. Если время выполнения запроса находится в разумных пределах, я бы остановился именно на нем. К сожалению, такой синтаксис команды SQL-Update возможен только в VFP9.

По этой ссылке вы можете скачать исходные тексты примеров

Как успокоить пользователя, пока выполняется программа

Если программа в течение минуты не будет подавать признаков жизни, у пользователей возникает жгучее желание нажать Ctrl+Alt+Del, и прервать процесс. Вам необходимо информировать пользователя о состоянии дел. Вот варианты.

Если расчеты многоступенчатые, перед каждым этапом выводите либо в статус-бар, либо в окно сообщение

Wait window "Этап 1: Проверка данных" nowait
...
Wait window "Этап 5: Суммирование данных за год" nowait
...
Wait window "Этап 10: Окончательное форматирование" nowait
...
Wait clear

При сканировании большой таблицы выводите номер обрабатываемой записи. Имейте ввиду, что вывод на экран - очень медленный процесс. Поэтому выводите сообщения только на каждой десятой или даже сотой строке:

Set Talk Off
...
Scan
       ...
       ...
       If MOD(RECNO(), 100) = 0
               Application.StatusBar = "Обработано "  + TRANSFORM( RECNO() ) + " строк из " + TRANSFORM( RECCOUNT() )
       EndIf

EndScan

Очень полезным может оказаться следующий прием. Запоминайте, сколько времени длился расчет в прошлый раз, и используйте эти данные для текущего расчета. Сохранять эту информацию нужно локально, так как скорость вычислений зависит от возможностей данной рабочей станции.

Wait window "В прошлый раз расчет длился " + TRANSFORM(Duration) + " секунд" + CHR(13) + ;
               "Этот расчет начался в " + TIME() + CHR(13) + ;
               "и продлится примерно до " + TTOC( DATETIME() + Duration , 2 ) nowait
Если длительность расчета зависит от каких-то параметров, вы можете экстраполировать старые показания пропорционально новым. Например, если параметрами для вашей процедуры является диапазон дат, разделите длительность процесса на количество дней в диапазоне, и получите примерную длительность в расчете на один день. В следующий раз, когда пользователь укажет другие даты, умножьте сохраненный результат на новое количество дней, и получите экстраполяцию, сколько будут длиться вычисления. Время, естественно, будет очень приблизительным, но это в любом случае поможет пользователю оценить, сколько чашечек кофе он успеет выпить :-)

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

Тестирование

Советы уважаемых гуру, и даже выдержки из HELP не дают гарантий, что именно ваш случай не будет редким, но исключением из общих правил. Скорость работы может зависеть от сервера и сети, от активности пользователей, от объема и частотного распределения данных. Самый хороший способ определить, насколько быстро будет выполняться программа - провести тесты, максимально приближенные к реальности. Использование тестов только кажется сложным. Нет ничего сверхъестественного в том, чтобы сгенерировать табличку, близкую по размерам к максимальному ожидаемому размеру реальной БД, и попробовать поработать. В генерации тестовых данных вам помогут функции RAND(), SYS(3) и SYS(2015), а так же любой осмысленный текст достаточно большого размера. В тестах для данной статьи я использовал kladr - классификатор адресов России. Для большей достоверности постарайтесь, чтобы данные не были физически отсортированы по какому-либо полю.

*** Генерация тестовой таблицы ***

=RAND(-1) && HELP рекомендует перед началом работы вызвать RAND() с отрицательным параметром

For nCount = 1 to BigNumber && число добавляемых записей

       *** пример генерации случайного числа от 1 до 1000
       nRandomValue = CEILING(1000 * RAND())

       *** а так можно заполнять почтовые индексы и номера телефонов
       cZipCode = PADR(CEILING(999999 * RAND()), 6, "0")

       *** поиск случайной записи в связанной таблице
       Goto CEILING(RECCOUNT("ChildTable") * RAND() ) in ChildTable
       RelatedTableValue = ChildTable.Field1

       *** пример генерации случайного адреса
       Goto CEILING(RECCOUNT("Towns") * RAND() ) in Towns
       Goto CEILING(RECCOUNT("Streets") * RAND() ) in Streets

       cAddress = ALLTRIM(Towns.Name) + ", " + ALLTRIM(Streets.Name) + ", дом " + TRANSFORM( CEILING(200 * RAND()) )

       *** Вставляем полученные данные в таблицу
       Insert into BigTable (...) values (nRandomValue, cZipCode, cAddress, RelatedTableValue)

EndFor

Регулярное проведение тестов имеет еще одну приятную особенность. Пока выполняется тест, вы имеете полное право откинуться на спинку стула, и несколько минут наслаждаться бездельем ;-)

Благодарности

В работе над статьей принимали участие члены Фокс-Клуба: Владимир Максимов, Игорь Королев, Alex Shustikov, is

Игорь Ильин www.foxclub.ru

Комментарии: (0) | FoxPro | 2006-06-09

Как прочитать значение поля со свойством IDENTITY

Как прочитать значение поля со свойством IDENTITY

Что такое IDENTITY
Недостаток использования полей со свойством IDENTITY как первичного ключа
Как определить значение поля со свойством IDENTITY в новой записи
Как работать с полями со свойством IDENTITY в FoxPro
      Прямое использование Pass-Through технологии через функцию SQLEXEC()
      Использование Remote View
      Использование Cursor Adapter

Что такое IDENTITY

IDENTITY - это не тип данных. Это некоторое дополнительное свойство, ограничение, накладываемое на целочисленные типы данных в MS SQL сервере. Т.е. это свойство может быть применено к полям следующего типа: tinyint, smallint, int, bigint, decimal(p,0), или numeric(p,0)

Аналогом этого свойства в FoxPro является тип данных Integer-AutoIncrement. Только не надо считать, что Integer-AutoIncrement это и есть поле со свойством Identity. Вовсе нет. Это именно аналог. В основном они похожи, но имеют ряд отличий. В данной статье речь пойдет о свойстве IDENTITY в MS SQL сервере.

Поля со свойством IDENTITY обладают следующими особенностями:

  • В одной таблице допустимо существование только одного поля со свойством IDENTITY
  • Поле со свойством IDENTITY нельзя редактировать. Они имеет свойство "только для чтения".
  • Значение полю со свойством IDENTITY присваивается автоматически в момент создания новой записи.

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

Новое значение - это последнее использованное значение плюс некоторая фиксированная величина. Обратите внимание, новое значение опирается не на максимальное значение в существующих записях, а на последнее использованное значение. Это значит, что записи с последним использованным значением физически может не существовать, тем не менее, это значение будет использовано.

Другими словами, в последовательности значений поля со свойством IDENTITY вполне допустимы "дыры". Список значение вовсе не непрерывный

Как правило, в качестве шага приращения указывают 1, но это может быть и любое целое число. В том числе и отрицательное.

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

Следует иметь в виду, что свойство IDENTITY никак не контролирует уникальность данных. Например, если изначально поле имело тип INTEGER, и в него были введены ряд значений. А затем была изменена структура таблицы, и на данное поле было наложено свойство IDENTITY, то новые записи вполне могут иметь те же данные, что уже были введены ранее в эту таблицу. Поэтому, если поле со свойством IDENTITY используется в качестве первичного ключа, то на это поле следует наложить дополнительное ограничение по уникальности.


Недостаток использования полей со свойством IDENTITY как первичного ключа

Однако, несмотря на явные достоинства использования полей со свойством IDENTITY в качестве первичного ключа, они имеют и серьезный недостаток.


Значение полей со свойством IDENTITY невозможно узнать до того, как запись будет физически создана.


Ну и что? Какие проблемы-то? Создадим запись и узнаем ее новое значение.

Проблема заключается в том, что для того, чтобы узнать значение поля какой-либо записи эту запись надо сначала найти. А поиск записи как раз и осуществляется по значению первичного ключа. Того самого, значение которого необходимо определить. Замкнутый круг: чтобы прочитать значение это значение надо знать!

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

Более того, хотя новое значение поля со свойством IDENTITY всегда больше любого из существующих значений (если шаг приращения - положительное число), но определить это новое значение, просто вычислив максимальное из существующих значений, тоже нельзя. Нет, само максимальное значение, разумеется, будет получено. Просто нет никакой гарантии, что полученное значение - это значение именно той записи, которая и была создана.

Дело тут в том, что, как правило, MS SQL сервер используется в многопользовательских приложениях. Это значит, что сразу несколько пользователей, одновременно, могут создавать новые записи. Получается, что один пользователь создал новую запись, затем начал вычислять максимальное значение и в этот момент другой пользователь также создал новую запись. В результате, первый пользователь в качестве максимального значения получит значение записи созданной вторым пользователем.

Так что же, отказаться от использования полей со свойством IDENTITY в качестве первичного ключа? Вовсе нет. Все-таки существуют способы определения значения поля со свойством IDENTITY у новой записи.


Как определить значение поля со свойством IDENTITY в новой записи

Собственно, есть три принципиальные стратегии определения значения поля со свойством IDENTITY в новой, только что созданной, записи

  • Значение, возвращаемое системной переменной @@IDENTITY
  • Значение, возвращаемое функцией SCOPE_IDENTITY()
  • Нахождение новой записи по значению других полей

Теперь рассмотрим более подробно достоинства и недостатки каждой стратегии


Значение, возвращаемое системной переменной @@IDENTITY

В MS SQL сервере есть ряд системных переменных, значение которых изменяется автоматически при наступлении определенных событий. В частности, значение системной переменной @@IDENTITY автоматически устанавливается равным значению поля со свойством IDENTITY последней созданной записи в текущем соединении. Т.е. создание новых записей в другом соединении (другим пользователем) никак не повлияет на ее значение в данном соединении.

Ну, так вот оно, решение. Просто после создания новой записи читаем значение системной переменной @@IDENTITY и имеем искомое значение.

В целом, верно. Проблема только в том, что системная переменная @@IDENTITY меняет свое значение при создании записи в любой таблице.

На практике, это означает, что если на таблицу установлен триггер на вставку, в теле которого дается команда INSERT на создание записи в другой таблице, которая, в свою очередь, также имеет поле со свойством IDENTITY, то системная переменная @@IDENTITY получит значение поля из этой второй таблицы.

Другими словами, опираться на значение системной переменной @@IDENTITY можно, но, помня о том, что эта переменная не привязана к значению поля одной таблицы.


Значение, возвращаемое функцией SCOPE_IDENTITY()

В версии MS SQL 2000 была введена системная функция SCOPE_IDENTITY(). Эта функция также возвращает значение поля со свойством IDENTITY последней созданной записи, но созданной в пределах текущего SCOPE.

Адекватно перевести термин SCOPE на русский язык достаточно сложно. Но, приблизительно, можно сказать так: SCOPE - это одна процедура или функция. Другими словами, SCOPE_IDENTITY() вернет значение поля со свойством IDENTITY последней записи созданной в пределах той процедуры, где эта функция была вызвана.

Триггер - это уже другой SCOPE (другая функция), поэтому он никак не повлияет на значение возвращаемое SCOPE_IDENTITY().

Даже если два пользователя одновременно вызвали одну и ту же процедуру, но каждый вызвал процедуру в своем SCOPE. Т.е. опять-таки нет конфликта.

К недостаткам этой функции относится то, что ее следует вызывать в пределах того SCOPE, где и была создана новая запись интересующей нас таблицы. А это не всегда возможно.

Другими словами, для корректного использования SCOPE_IDENTITY() необходимо всегда следить за областью действия SCOPE. Зачастую, создавая специальные процедуры.


Нахождение новой записи по значению других полей

Если помните, то основная проблема с определением значения поля со свойством IDENTITY заключается в том, что данное поле используется в качестве первичного ключа. Т.е. по его значению как раз и находят нужную запись.

Однако, зачастую, таблицы имеют поле или набор полей, по которым также можно однозначно определить запись. Например, если речь идет о справочнике, то, разумеется, справочник имеет поле "Название". Также очевидно, что это поле должно быть уникально в пределах справочника. Иначе просто теряется смысл использования самого справочника. Зачем вводить в справочник записи с одинаковым значением?

Почему же не использовать в качестве первичного ключа это самое "Название"? Зачем вообще нужно поле со свойством IDENTITY? Это тема отдельного разговора. Вкратце, "Название" - это для пользователя (внешние данные), а IDENTITY - это для обеспечения ссылочной целостности базы данных (внутренние данные).

Значение поля со свойством IDENTITY в новой записи не известно. Но значение поля "Название" в этой новой записи вполне известно. Пользователь сам же его и ввел! Значит, после создания новой записи можно найти эту новую запись по значению поля "Название" и прочитать значение поля со свойством IDENTITY.

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

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


Как работать с полями со свойством IDENTITY в FoxPro

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

Добавляется запись в таблицу MS SQL сервера имеющую поле со свойством IDENTITY. Необходимо сразу после создания новой записи получить значение поля со свойством IDENTITY на стороне FoxPro.

У FoxPro есть три принципиальные возможности организации работы с MS SQL сервером

  • Прямое использование Pass-Through технологии через функцию SQLEXEC()
  • Использование Remote View
  • Использование Cursor Adapter

Тут следует остановиться на том, какое именно событие собственно создает запись на MS SQL сервер. Ну, с Pass-Trough все ясно. Это собственно прямая команда серверу создать новую запись. А вот с Remote View и Cursor Adapter несколько иначе.

Результатом работы как Remote View, так и Cursor Adapter является курсор. Т.е. некая временная таблица, физически расположенная на машине клиента. По умолчанию, этот курсор автоматически открывается в режиме оптимистической буферизации строк (3) и может быть переключен только в режим оптимистической буферизации таблиц (5). Переключиться в режим пессимистической буферизации или отключить буферизацию совсем для этого курсора невозможно

Следовательно, новая запись сначала физически будет создана именно на клиентской машине в этом самом курсоре. Точнее, в буфере этого курсора. Физическое создание записи на MS SQL сервере произойдет только после сброса буфера.

Для строковой буферизации сброс буфера может произойти автоматически при выполнении одного из следующих действий:

  • Переход (или попытка перехода) на другую запись
  • Закрытие курсора
  • Переключение в режим табличной буферизации
  • По команде TableUpdate()

Для табличной буферизации сброс буфера может произойти только по команде TableUpdate() и никак иначе.

Никакие другие действия и операции ни с Remote View, ни с Cursor Adapter не приведут к созданию новой записи на MS SQL сервере. Если при выполнении какой-либо операции оказалось, что на MS SQL сервере создалась новая запись, это означает, что курсор находился в режиме строковой буферизации, и произошло одно из событий вызвавших автоматический сброс буфера.

Например, такое может произойти по команде Requery() для Remote View. Но это вовсе не означает, что команда Requery() сбрасывает буфер. Вовсе нет. Просто одним из условий выполнения команды Requery() является закрытие ранее существовавшего курсора. А вот это событие как раз и вызовет автоматический сброс буфера, если курсор находится в режиме строковой буферизации.

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

Однако следует понимать, что даже если Вы установите режим табличной буферизации, измените несколько записей в Remote View или Cursor Adapter, а потом дадите команду TableUpdate() все равно сброс буфера будет происходить по одной записи за раз. Т.е. на сервер будет послана не одна команда, например, на модификацию, а набор команд по модификации каждой записи в отдельности.

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


Прямое использование Pass-Through технологии через функцию SQLEXEC()

При таком способе работы программист как бы напрямую работает с MS SQL сервером. Сам формирует все команды, отсылаемые на сервер, получает результат, и сам же его обрабатывает. В этом случае не составляет никакого труда послать дополнительный запрос серверу на значение функции SCOPE_IDENTITY


LOCAL lcNewValue, lnResut
lcNewValue = "Новое значение"
lnResut = SQLExec(m.lnConnectHandle,"INSERT INTO MyTab (Field1) VALUES (?m.lcNewValue)")
IF m.lnResut>0
       SQLExec(m.lnConnectHandle,"SELECT NewIdent=SCOPE_IDENTITY()","NewIdent")
       ?NewIdent.NewIdent
ELSE
       LOCAL laError(1)
       =AERROR(laError)
       * Анализ массива laError для уточнения причины ошибки
ENDIF

В данном примере m.lnConnectHandle - это число, номер соединения с MS SQL сервером, которое настраивается раньше. MyTab - это таблица, которая имеет поле со свойством IDENTITY.

После выполнения второго запроса в результирующем курсоре NewIdent в поле NewIdent первой записи и получим искомое значение. В данном синтаксисе и команда вставки и вызов функции SCOPE_IDENTITY() происходят в одном SCOPE. Поэтому, получаем нужное значение.


Использование Remote View

Remote View - это некая "надстройка" над технологией Pass-Through. По сути, при создании новой записи выполняется та же команда INSERT INTO. Однако проблема в том, что даже если прочитать номер соединения, в котором работает Remote View, а потом выполнить запрос для определения значения, возвращаемого SCOPE_IDENTITY(), то получим NULL, поскольку в этом случае команда вставки и SCOPE_IDENTITY() выполняются в разных SCOPE. Следовательно, остается только два способа определения значения поля со свойством IDENTITY.


* Определение значения системной переменной @@IDENTITY
LOCAL lnConnectHandle
lnConnectHandle = CursorGetProp("ConnectHandle","MyRemoteView")
SQLExec(m.lnConnectHandle,"SELECT NewIdent=@@IDENTITY","NewIdent")
?NewIdent.NewIdent

* Определение по значению другого поля
LOCAL lnConnectHandle, lcNickName
lnConnectHandle = CursorGetProp("ConnectHandle","MyRemoteView")
lcNickName = MyRemoteView.NickName
SQLExec(m.lnConnectHandle,"SELECT TabId FROM MyTab WHERE NickName=?lcNickName","NewIdent")
?NewIdent.TabId

В обоих примерах MyRemoteView - это имя Вашего Remote View. NickName - это имя поля в Remote View по значению которого выполняется поиск новой записи, а TabID - это то самое поле со свойством IDENTITY значение которого и надо определить

Предполагается, что создание новой записи уже произошло. Либо явно была дана команда TableUpdate(), либо Remote View находится в режиме строковой буферизации, и была попытка перехода на другую запись.

А почему во втором случае не было использовано, казалось бы, более очевидное решение поиска после Requery()? Что-то вроде


=REQUERY("MyRemoteView")
SELECT MyRemoteView
LOCATE FOR NickName = "Новое значение"
?MyRemoteView.TabID

На это есть несколько причин.

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

Во-вторых, обычно Remote View содержит в себе дополнительные условия отбора записей, и только что созданная запись может вообще не попасть в условия отбора. Т.е. после Requery() запись, хотя и будет физически создана на сервере, но в самом Remote View отображена не будет.


Использование Cursor Adapter

Объект Cursor Adapter был введен в FoxPro, начиная с версии Visual FoxPro 8. Он призван облегчить работу с удаленными данными при выполнении некоторых стандартных операций. В частности, операции создания новой записи. Через Cursor Adapter можно реализовать все три варианта получения значения поля со свойством IDENTITY в новой записи.


Значение, возвращаемое системной переменной @@IDENTITY

После того, как будет выполнен сброс буфера и произойдет физическая создание новой записи на MS SQL сервере, в объекте Cursor Adapter сработает событие AfterInsert. Вот в нем как раз и надо выполнить дополнительный запрос на сервер и прочитать значение системной переменной @@IDENTITY


PROCEDURE AfterInsert
LPARAMETERS cFldState, lForce, cInsertCmd, lResult
IF lResult=.T. && вставка произошла успешно. Надо получить значение ID
       LOCAL currentArea
       currentArea=SELECT()

       TRY
               IF 1=SQLEXEC(this.DataSource,"SELECT NewIdent=@@IDENTITY","NewIdent")
                       REPLACE TabId WITH NewIdent.NewIdent IN (This.Alias)
                       USE IN IDRes
               ELSE
                       LOCAL laError(1)
                       =AERROR(laError)
                       * Анализ массива laError для уточнения причины ошибки
               ENDIF
       FINALLY
               SELECT (currentArea)
       ENDTRY
ENDIF
ENDPROC


Думаю, код достаточно понятный и не нуждается в пояснениях. После успешного создания новой записи на сервере при помощи SQLExec() для того же самого соединения определили значение @@IDENTITY и записали это значение в текущую запись курсора в ключевое поле.

В версии Visual FoxPro 9 в объект Cursor Adapter было добавлено свойство InsertCmdRefreshCmd. Это команда, которая посылается на сервер только после успешной вставки новой записи. Т.е. здесь нет необходимости убеждаться в самом факте создания новой записи.

В паре с другим новым свойством InsertCmdRefreshFieldList можно выполнить обновление, опираясь на значение системной переменной @@IDENTITY проще. Для этого всего лишь надо сделать следующие настройки объекта CursorAdapter


CursorAdapter.InsertCmdRefreshCmd       = "SELECT @@IDENTITY"
CursorAdapter.InsertCmdRefreshFieldList = "TabId"

Здесь TabId - это не имя поля со свойством IDENTITY в таблице собственно на MS SQL сервере, а имя поля курсора, полученного на стороне клиента в котором и отображается содержимое поля со свойством IDENTITY. Как правило, эти имена совпадают, но, в общем случае, могут и отличаться. Более подробно о свойстве InsertCmdRefreshFieldList читайте ниже. В разделе, посвященном нахождению новой записи по значению других полей.


Значение, возвращаемое функцией SCOPE_IDENTITY()

Здесь все несколько сложнее. Дело в том, что действовать, так же как и в случае определения системной переменной @@IDENTITY нельзя. SQLExec() и Cursor Adapter работают в разных SCOPE. Поэтому при подобном подходе SCOPE_IDENTITY() всегда будет возвращать NULL. Чтобы преодолеть это противоречие используют специальные процедуры.

Для начала, надо создать на самом MS SQL сервере вот такую хранимую процедуру


CREATE PROCEDURE Get_ValueInt
               @ValueIn Int,
               @ValueOut Int OUTPUT
AS
SET NOCOUNT ON
SELECT @ValueOut=@ValueIn

Как видите, цель данной процедуры просто присвоить значение входного параметра выходному параметру. Теперь можно использовать эту процедуру для определения значения SCOPE_IDENTITY() в Cursor Adapter. Для этого в событии BeforeInsert делается такая подмена


PROCEDURE BeforeInsert
LPARAMETERS cFldState, lForce, cInsertCmd
cInsertCmd = cInsertCmd + ;
               "; DECLARE @id int" + ;
               "; SELECT @id=SCOPE_IDENTITY()" + ;
               "; EXEC Get_ValueInt @id, ?@MyTab.TabId"
ENDPROC

Только следует иметь в виду, что в данной подмене MyTab - это уже не имя таблицы на MS SQL сервере, а имя курсора создаваемого у клиента. Точнее, то имя, которое записано в свойстве Alias собственно Cursor Adapter. Соответственно, "TabId" - это имя поля курсора, созданного у клиента и содержащего значение поля со свойством IDENTITY

В данном случае, по сути, формируется динамическая хранимая процедура. Т.е. не просто одна команда INSERT, а последовательность команд. Команды отделяются друг от друга символом точки с запятой, хотя это вовсе не обязательно. Достаточно отделять команды друг от друга простым пробелом.


Нахождение новой записи по значению других полей

Если у Вас Visual FoxPro 8, то Вам остается использовать метод аналогичный методу, примененному для поиска значения системной переменной @@IDENTITY. Т.е. в методе AfterInsert объекта Cursor Adapter выполнить дополнительный запрос через SQLExec()

Начиная с версии Visual FoxPro 9, в объекте Cursor Adapter появились дополнительные свойства, позволяющие автоматизировать эту процедуру без написания дополнительного кода

InsertCmdRefreshFieldList - список полей, значение которых будет обновлено после Insert
InsertCmdRefreshKeyFieldList - поле, или набор НЕ ключевых полей также однозначно идентифицирующих запись, по которым и будет осуществлен поиск новой записи

Следует отметить, что в этих свойствах указываются не имена полей таблицы собственно MS SQL сервера, а соответствующие им имена полей курсора, полученного на стороне клиента. Применительно к тому же примеру это будет примерно так:


InsertCmdRefreshFieldList      = "TabID"
InsertCmdRefreshKeyFieldList   = "NickName"

Другими словами, опираясь на значение поля NickName, будет найдена запись в таблице на MS SQL сервере и прочитано значение поля TabID, после чего это значение будет записано в новую запись на стороне клиента.


Более подробно как обсуждение данной темы, так и примеры использования Вы можете посмотреть здесь

CursorAdapter: Авто-рефреш полей во время TABLEUPDATE
Как автомат-ки получать автоинкриментное поле SQL сервера в КА
CursorAdapter, событие AfterInsert и поле сервера типа IDENTITY

Владимир Максимов www.foxclub.ru

Комментарии: (0) | FoxPro | 2006-06-09

FoxPro : Советы начинающим Часть V

FoxPro : Советы начинающим Часть V

В этой статье :

Триггер
Триггер

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

Триггер - это выражение, значение которого вычисляется при наступлении определенного события. Это выражение должно вернуть обязательно логическое значение .T. или .F.

Но все дело в том, что в качестве одного из аргументов этого выражения может стоять некоторая функция (или несколько функций). Вот эти-то функции, вызываемые из выражения триггера, также называют "триггер".

Как правило, когда речь идет о триггере, то подразумевают именно функцию - триггер, а не выражение - триггер.

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

Если выражение триггера вернет .T., то сделанные модификации принимаются и записываются в базу данных. Если же выражение триггера вернет .F., то сделанные модификации отвергаются и генерится ошибка с кодом 1539 (ошибка триггера). Уточнить, какой именно вид триггера вызвал ошибку, можно используя функцию AERROR().

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

О некоторых особенностях поведения триггеров в FoxPro Вы можете почитать в описании к команде CREATE TRIGGER. Замечу, что в этой команде речь идет именно о выражении, но никак не о функции триггера.


Особенности использования функций - триггеров

Функции триггеров хранятся в хранимых процедурах, поскольку это действия, которые связаны напрямую с данными и не должны зависеть от приложения.

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

  • Триггер всегда анализирует изменения сделанные только в одной записи таблицы. Иначе говоря, даже если вы использовали некоторую групповую команду вставки или модификации (например, APPEND FROM), тем не менее, модификация будет производиться по одной записи за раз чтобы дать возможность триггерам выполнить свои проверки.
  • Внутри тела триггера (внутри выражения или первой функции) произойдет автоматический переход в ту рабочую область и на ту запись, которые и вызвали срабатывание тела триггера. Однако если Вы используете в качестве выражения триггера несколько функций триггера (например, "Func1().AND.Func2()"), то во второй функции такого автоматического перехода уже не произойдет. Поэтому при завершении функции триггера необходимо особо проконтролировать возврат в ту рабочую область, в которой началось выполнение функции триггера.
  • По завершении вычисления выражения триггера Вы вернетесь в ту рабочую область, в которой была дана команда, вызвавшая срабатывание триггера.
  • Внутри тела триггера запрещено перемещать указатель записи в той рабочей области, которая вызвала срабатывание триггера. Этот запрет можно обойти, открыв ту же самую таблицу еще раз в другой рабочей области (USE MyTab IN 0 AGAIN ALIAS MyTab2)
  • Запрещен рекурсивный вызов триггера в пределах одной и той же таблицы. Т.е. если сработал, например, триггер на удаление, то Вы никаким способом не сможете внутри триггера на удаление удалить какие-либо еще записи из этой же таблицы (удалить "ветку" в древовидной структуре через триггер не получится). Хотя допустимо удалить записи из другой таблицы.
  • В принципе, функция триггера может вернуть абсолютно любое значение. Важно только чтобы выражение триггера возвращало именно логическое значение. Ну, например, если в качестве выражения триггера используется что-то вроде: "Func1() > Func2()", то главное, чтобы эти 2 функции возвращали значения, которые можно сравнить между собой, а какого именно типа уже не важно.
  • Следует помнить, что если внутри тела триггера произойдет ошибка, то выражение триггера автоматически вернет .F. и будет сообщение об ошибке 1539 (ошибка триггера), но никак не о той ошибке, которая произошла внутри тела триггера. Поэтому внутри тела триггера следует предусмотреть собственный обработчик ошибок, записывающий причину возможной ошибки в какой-либо объект. Например, внутри тела триггеров автоматически создающихся FoxPro (Referential Integrity) для этой цели создается глобальный массив gaErrors[n,12]. Т.е. из 12 столбцов, а количество строк зависит от количества ошибок.
  • Если Вы модифицируете данные в режиме буферизации, а сброс изменений в исходные таблицы осуществляется исключительно командой TABLEUPDATE(), то в случае возникновении ошибки триггера никакого сообщения об ошибке не появится. Просто команда TableUpdate() вернет .F. Чтобы уточнить причину отказа в сбросе буфера следует использовать функцию AERROR(). Примерно так:
      
      IF TableUpdate(.T.,.T.)=.F.  
            LOCAL laError(1)  
            =AERROR(laError)  
            DO CASE  
            CASE laError[1,1]=1539  
            * Триггер вернул .F. уточняю какой именно триггер  
                  DO CASE  
                  CASE laError[1,5] =1  
                  * Ошибка триггера Insert  
                  CASE laError[1,5] =2  
                  * Ошибка триггера Update  
                  CASE laError[1,5] =3  
                  * Ошибка триггера Delete  
                  ENDCASE  
            ENDCASE  
      ENDIF  
     
    
  • Внутри тела триггера значения полей текущей записи содержат данные после модификации. Чтобы получить значения тех же полей до модификации следует использовать функцию OldVal(). Следует иметь в виду, что если это триггер на вставку и создается действительно новая запись (а не восстановление ранее удаленной по RECALL), то OldVal() вернет значение NULL, даже если данное поле и не может принимать значение NULL. Опираясь на эту особенность, можно внутри тела триггера определить из какого именно типа триггера данная функция была вызвана примерно следующим способом:
      
      DO CASE  
      CASE Deleted()  
     * вызов из триггера на удаление 
      CASE NVL(OldVal("Deleted()"),.T.)  
     * вызов из триггера на вставку 
      OTHERWISE  
     * вызов из триггера на модификацию 
      ENDCASE  
     
    

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

    Кроме того, триггер UPDATE срабатывает при модификации любого поля таблицы. Если же тригер предназначен для контроля изменения только определенного поля таблицы, то разумно в самое начало триггера вставить проверку на факт изменения проверяемого поля:

      
      IF MyField=OldVal("MyField")  
            * Значение поля MyField не изменилось, нет смысла запускать триггер  
            RETURN .T.  
      ENDIF  
     
    

Referential Integrity

В простых случаях FoxPro может самостоятельно создать как выражение, так и функцию триггера. Для этой цели и служит пункт главного меню "DataBase", подпункт "Edit Referential Integrity".

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

Для указания построителю, какое поле считать ключевым, а какое внешним ключом, необходимо установить между двумя таблицами "постоянную связь". Далее вы выбираете один из 3 возможных типов отношений между этими двумя таблицами (для триггера на вставку только 2 варианта). Ну, какие именно варианты есть достаточно понятно.

Update Cascade При изменении значения ключевого поля родительской таблицы изменить значение внешнего ключа во всех соответствующих записях подчиненной таблицы.
  Restrict Запрет на модификацию ключевого поля родительской таблицы, если существует хотя бы одна соответствующая ей запись в подчиненной таблице
  Ignore Не настраивать никаких отношений между таблицами. Модификация в одной таблице никак не влияет на модификацию в другой
Delete Cascade При удалении записи в родительской таблицы удалить все соответствующие записи подчиненной таблицы.
  Restrict Запрет на удаление записи в родительской таблице, если существует хотя бы одна соответствующая ей не удаленная запись в подчиненной таблице
  Ignore Не настраивать никаких отношений между таблицами. Модификация в одной таблице никак не влияет на модификацию в другой
Insert Cascade Не существует, поскольку данный тип взаимоотношений между двумя таблицами не имеет физического смысла
  Restrict Запрет на создание новой записи в подчиненной таблице, если в родительской таблице не существует записи с указанным значением ключевого поля.
  Ignore Не настраивать никаких отношений между таблицами. Модификация в одной таблице никак не влияет на модификацию в другой

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

После того, как Вы выберите типы взаимоотношений между таблицами, произойдет следующее:

  • в хранимых процедурах базы данных будут созданы функции, обеспечивающие указанный тип взаимоотношений выбранных таблиц
  • в свойствах таблиц будут добавлены в выражения триггеров вызов соответствующих функций

В принципе, по окончании создания триггеров "Referential Integrity" постоянную связь, послужившую основой для создания данной ссылочной целостности можно удалить. Это уже никак не скажется на факте существования созданных триггеров. Однако я не советовал бы этого делать по той причине, что в случае внесения изменений в "Referential Integrity" (пусть даже по отношению к другим таблицам) генератор "затрет" ранее созданные триггера.

Генератор "Referential Integrity" несколько прямолинеен. Он действует по принципу: удалить все - создать заново. Поэтому, если постоянная связь была удалена, то триггера функции, созданные на ее основе, при очередной модификации "Referential Integrity" будут удалены, хотя останутся триггера выражения в свойствах таблицы. А это вызовет сообщение об ошибке при попытке вызвать несуществующие функции.

Как правило, функции триггеров, созданных "Referential Integrity" получают имена вроде __RI_UPDATE_MyTab. Если теперь открыть хранимые процедуры на модификацию и посмотреть содержимое созданных функций, то первой реакцией будет немедленно это закрыть. Очень уж громоздко и неприглядно это все выглядит.

Если же преодолеть первый испуг и попытаться разобраться в этих "играх нечеловеческого разума", то общая логика достаточно проста:

  • Если вызов триггера произошел в результате действий пользователя, а не как результат модификации данных в другом триггере (это отслеживается по значению системной переменной _triggerlevel), то делаются некоторые предварительные настройки
  • Делаются некоторые настройки среды окружения
  • Функция триггера обязательно окружается транзакцией на случай групповых (Cascade) модификаций данных.
  • Настраивается свой обработчик ошибок (RIError). Ошибки, возникшие внутри тела триггера, попадут в глобальный массив gaErrors[n,12]. Количество строк определяется количеством ошибок. Запоминать ошибки необходимо потому, что в случае возникновения ошибки в теле триггера выражение триггера вернет .F. и будет ошибка 1539, но никак не та, что привела к прерыванию триггера. Кроме того, массив gaErrors позволяет записать причину отказа в принятии данных изменений (т.е. если произошла логическая ошибка, для контроля которой данный триггер и был создан). В этом случае значение gaErros[1,1] = -1.
  • Связанная таблица (в которой надо сделать проверки или изменения) открывается еще раз в новой рабочей области (RIOpen). Причем организована специальная проверка, чтобы не открывать одну и ту же таблицу несколько раз. Причина повторного открытия в том, что внутри тела триггера недопустимо перемещать указатель записи в той рабочей области, которая вызвала срабатывание триггера.
  • Выполняются нужные проверки или действия. Причем каскадное удаление или обновление реализуются через функции общие для всех триггеров (RIDelete, RIUpdate)
  • Функция RIReUse "освобождает" повторно открытую таблицу. Т.е. устанавливает признак того, что данная копия таблицы уже не используется и ее можно использовать в другом триггере. Это сделано, чтобы повторно не открывать одну и ту же таблицу несколько раз внутри одной иерархии цепочки триггеров.
  • По окончании триггера среда возвращается в то состояние, в котором она была на момент начала выполнения триггера, закрывается транзакция, и закрываются все открытые внутри триггера таблицы (RIEnd)

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

Однако даже в такой относительно простой вещи есть некоторые "подводные камни".

  • Настройка Referential Integrity связана с модификацией контейнера базы данных, поэтому требует единоличного (Exclusive) доступа к базе данных
  • При выполнении модификации базы данных под ногами не должен болтаться всякий мусор оставшийся от предыдущих модификаций. Т.е. непосредственно перед настройкой Referential Integrity необходимо выполнить очистку базы данных

Если одно из этих условий не будет выполнено, то при вызове пункта главного меню "DataBase", подпункт "Edit Referential Integrity" Вы получите предупреждение. Это будет либо напоминание о том, что база данных не открыта в режиме Exclusive и изменения сохранены не будут (хотя состояние Referential Integrity посмотреть можно), либо настойчивое предложение выполнить очистку базы данных (в этом случае даже посмотреть Referential Integrity не удастся).

Еще одна тонкость при использовании "Referential Integrity" заключается в том, что в случае модификации "Referential Integrity" FoxPro ищет, что именно надо заменить в выражении триггерра, опираясь именно на стандартные имена функций. При этом все то, что отличается от имен по умолчанию остается неизменным.

Например, если до настройки "Referential Integrity" Вы уже написали в выражении триггера какую-либо свою функцию (MyFunc()), то Referential Integrity не удалит эту функцию, но добавит к ней свою. В результате выражение триггера примет примерно такой вид: "__RI_UPDATE_MyTab().AND.(MyFunc())"

Стоит ли использовать Referential Integrity? А почему бы и нет. Следует только помнить о том, что триггера созданные при помощи Referential Integrity касаются только и исключительно поддержания ссылочной целостности между парой таблиц. Причем не всех возможных видов ссылочной целостности, а только определенных ее видов. Т.е. в больших проектах этих триггеров недостаточно. Придется дописывать еще что-то свое.

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


Когда и для чего использовать триггер

О чем собственно речь? А речь о том, в каких случаях следует использовать именно триггер, а не что-нибудь другое.

Основная задача триггера - это, прежде всего проверка допустимости уже сделанных изменений. Внесение изменений в теле триггера допустимо, но тут есть ряд ограничений.

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

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

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

Более спорный момент - это контроль уникальности данных. Достаточно подробно это описано в главе Индексы, раздел Контроль уникальности данных при помощи индекса.

А вот для чего триггер точно не стоит использовать, так это для модификации той записи, изменение в которой и вызвало срабатывание триггера. Даже если опустить тот момент, что в FoxPro это просто невозможно, то все равно для этой цели лучше использовать Rule уровня записи.

Правила (Rule)

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

Правило - это выражение, значение которого вычисляется при наступлении определенного события. Это выражение должно вернуть обязательно логическое значение .T. или .F.

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

Как правило, в большинстве случаев подразумевают именно функцию - правило.

В FoxPro различают 2 вида правил:

  • Правило на поле текущей записи таблицы
  • Правило на запись таблицы

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

Вычисление значения выражения - правила для записи таблицы происходит в случае модификации любого поля записи.

Точнее, вычисление происходит при завершении модификации. Но момент завершения модификации зависит от самого способа модификации.

Например, если Вы редактируете данные в Grid (Browse-окне), то правило для поля сработает при переходе на другое поле или запись или при попытке перейти на другой объект. А правило на запись сработает при попытке перехода на другую запись или на другой объект.

Если используется объект формы (не Grid, например, TextBox или EditBox) для редактирования содержимого поля, на которое наложено правило, то при попытке выйти из этого объекта после его редактирования сначала срабатывает правило для соответствующего поля, затем все события объекта-формы (Valid, LostFocus) и затем правило уровня записи таблицы (если есть). Т.е. выполнение правила на запись выполняется при завершении модификации одного поля, даже если Вы и не делаете попытку перейти на другую запись.

Таким образом, при редактировании поля через объект формы Вы никак не сможете перехватить выполнение правила событиями формы. Правило для поля будет выполнено в первую очередь. Конечно, если было редактирование.

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

Разумеется, правило на запись срабатывает только после обработки правил на поля текущей записи, если все они вернули .T. Более того, если одно из правил для поля вернуло .F., то выполнение команды модификации немедленно прерывается и не предпринимается даже попытки выполнить модификацию остальных полей.

Правила вычисляются при любой модификации данных любым способом. Т.е. они работают и в буферизированных данных. Поэтому, если правило вернет .F., то немедленно будет отображено сообщение об ошибке 1582 (отказ в правиле - поля) или 1583 (отказ в правиле - записи). Вы можете написать собственное сообщение об ошибке в предусмотренном для этого окне Message в конструкторе таблиц. Это сообщение будет отображаться только в том случае, если соответствующее правило вернет .F.

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

При программном добавлении правила через команду CREATE TABLE или ALTER TABLE подразумевается именно выражение - правило, но никак не функция. Для добавления правила в этих командах используется ключевое слово CHECK.


Особенности использования правил (Rule)

Функции правил следует хранить в хранимых процедурах, поскольку это действия, которые связаны напрямую с данными и не должны зависеть от приложения.

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

  • Правила всегда анализирует изменения сделанные только в одной записи таблицы. Иначе говоря, даже если вы использовали некоторую групповую команду вставки или модификации (например, APPEND FROM), тем не менее, модификация будет производиться по одной записи за раз чтобы дать возможность правилам выполнить свои проверки.
  • Внутри тела правила (внутри выражения или первой функции) произойдет автоматический переход в ту рабочую область и на ту запись, которые и вызвали срабатывание тела правила. Однако если Вы используете в качестве выражения правила несколько функций правила (например, "Func1().AND.Func2()"), то во второй функции такого автоматического перехода уже не произойдет. Поэтому при завершении функции правила необходимо особо проконтролировать возврат в ту рабочую область, в которой началось выполнение функции правила.
  • По завершении вычисления выражения правила Вы вернетесь в ту рабочую область, в которой была дана команда, вызвавшая срабатывание правила.
  • Внутри тела правила запрещено перемещать указатель записи в той рабочей области, которая вызвала срабатывание правила. Этот запрет можно обойти, открыв ту же самую таблицу еще раз в другой рабочей области (USE MyTab IN 0 AGAIN ALIAS MyTab2)
  • Внутри правила допустимо модифицировать поля той же записи, в которой Вы сейчас и выполняете проверку правил. Но я не рекомендовал бы делать это в правилах для поля, поскольку в этом случае велик риск "зацикливания". Ведь модификация поля тут же вызовет срабатывание правила для этого поля, а если внутри него опять выполняется его же модификация, то Вы можете получить бесконечное срабатывание правила на поле.
  • При модификации полей записи в правиле на запись повторное срабатывание правила на запись НЕ происходит. Т.е. даже если внутри тела правила для записи Вы модифицируете поле, которое имеет свое правило, то зацикливания не произойдет. Просто будет выполнено еще раз правило для измененного поля. Поэтому, если Вам необходимо внутри правила выполнить модификацию каких-то полей этой же записи, то их следует выполнить в правиле на запись
  • Сообщение об ошибке правила будет отображено немедленно, как только выражение правила вернет .F. При этом все прочие правила в той же записи выполнены уже не будут.
  • При удалении записи правила не срабатывают. Однако они срабатывают при создании новой записи или восстановлении ранее удаленной (RECALL)
  • Внутри тела правила значения полей текущей записи содержат данные после модификации. Чтобы получить значения тех же полей до модификации следует использовать функцию OldVal() даже если таблица не была буферезирована. Следует иметь в виду, что если создается действительно новая запись (а не восстановление ранее удаленной по RECALL), то OldVal() вернет значение NULL, даже если данное поле и не может принимать значение NULL. Таким образом, можно выполнить проверку на факт реального изменения значения поля:
      
      IF MyField=OldVal("MyField")  
            * Значение поля MyField не изменилось, нет выполнять проверку  
            RETURN .T.  
      ENDIF  
     
    

Когда и для чего использовать правило

О чем собственно речь? А речь о том, в каких случаях следует использовать именно правило, а не что-нибудь другое.

Основная задача правила - это, прежде всего проверка допустимости изменений именно в процессе их внесения. Внесение изменений в теле правила допустимо, но тут есть ряд ограничений.

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

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

Например, Вы можете написать правило для вычисления контрольной суммы ИНН (Идентификационный Номер Налогоплательщика). Если пользователь ошибся при вводе ИНН, то правило на данное поле тут же ему сообщит, что введено некорректное значение и не даст выйти из поля пока не будет введено корректное значение (и закрыть форму не даст!). Другой вопрос, насколько оправдан такой жесткий контроль. Но это уже зависит от логики Вашей программы.

Можно, конечно, в случае некорректного ввода ввести корректное значение, но тут есть опасность "зацикливания". Бесконечного вызова правила на изменяемое поле. Поэтому лучше этого не делать.

Пожалуй, наиболее удобным, является использование правил для фиксации времени изменений. Т.е. если в Вашей таблице введено специальное поле, например, LastModify типа DateTime, то Вы можете создать правило на запись, в котором и сделать присвоение:

REPLACE LastModify WITH DateTime()

Т.е. при любой модификации записи (кроме удаления) в поле LastModify будет записано дата и время этой модификации. А если добавить еще и поле, содержащее логин пользователя, то Вы получите информацию о том, кто и когда последний раз изменил данную запись. Разумеется, дата и время будут взяты с клиентской машины, где произошло изменение.

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

Также неразумно внутри тела правила делать ожидание реакции пользователя. Правило должно выполняться без каких-либо дополнительных действий со стороны пользователя. Опять же, если необходима реакция со стороны пользователя, то желательно вынести данную процедуру на уровень приложения

Значение по умолчанию (Default)

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

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

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


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

Этот момент требует пояснения, поскольку именно с ним связано большинство недоразумений

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

Значит, значения по умолчанию вычисляются только в момент создания новой записи, причем обязательно до того, как новая запись будет создана физически.

Однако если Вы в команде создания новой записи явно укажете значение поля, для которого есть значение по умолчанию, то в этом случае вычисление значения по умолчанию не произойдет. Будет использовано то значение, которое указано явно в команде создания новой записи.

Ну, например, для поля MyField таблицы MyTab Вы написали некоторое выражение по умолчанию. Если для создания новой записи Вы используете команду вида:

INSERT INTO MyTab (MyField) VALUES ("Новое значение")

То в новой записи в качестве значения поля MyField будет указано именно "Новое значение", а не то выражение, которое Вы указали как значение по умолчанию.

С другой стороны, если для создания новой записи Вы используете команду APPEN BLANK или ту же команду INSERT-SQL, но, не указав в списке полей поле MyField, то в этом случае, поскольку FoxPro явно не указали, какое именно значение следует присвоить полю MyField, будет использовано значение по умолчанию.

Более того, следует иметь в виду, что значение по умолчанию будет использовано и при создании новой записи в буферизированных таблицах.

Вычисление правила для поля таблицы произойдет после формирования значения по умолчанию. Что, в общем-то, и логично: чтобы проверить правильность введенного значения это значение сначала необходимо сформировать.


Особенности использования значения по умолчанию

Функции значений по умолчанию следует хранить в хранимых процедурах, поскольку это действия, которые связаны напрямую с данными и не должны зависеть от приложения.

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

  • Значение по умолчанию всегда формирует значение только в одной записи таблицы. Иначе говоря, даже если вы использовали некоторую групповую команду вставки (например, APPEND FROM), тем не менее, вставка будет производиться по одной записи за раз чтобы дать возможность вычислить значение по умолчанию для каждой вставляемой записи.
  • Внутри тела значения по умолчанию (внутри выражения или первой функции) произойдет автоматический переход в ту рабочую область и на ту запись, которые и вызвали срабатывание тела значения по умолчанию. Однако если Вы используете в качестве выражения значения по умолчанию несколько функций (например, "Func1()+Func2()"), то во второй функции такого автоматического перехода уже не произойдет. Поэтому при завершении функции значения по умолчанию необходимо особо проконтролировать возврат в ту рабочую область, в которой началось выполнение функции.
  • По завершении вычисления выражения значения по умолчанию Вы вернетесь в ту рабочую область, в которой была дана команда, вызвавшая срабатывание этого выражения.
  • Внутри тела функции значения по умолчанию запрещено перемещать указатель записи в той рабочей области, которая вызвала срабатывание функции. Этот запрет можно обойти, открыв ту же самую таблицу еще раз в другой рабочей области (USE MyTab IN 0 AGAIN ALIAS MyTab2)
  • Внутри тела функции значения по умолчанию не имеет смысла ссылаться на поля создаваемой записи той же таблицы, поскольку в общем случае не известно какие значения эти поля будут иметь в момент формирования значения по умолчанию отдельного поля. Т.е. сами поля уже будут существовать, но вот будут ли они содержать какое-либо значимое значение или же все еще будут оставаться пустыми заранее предсказать невозможно.
  • Если в результате расчета функция значения по умолчанию вернет значение, которое не может быть использовано в качестве значения данного поля, то Вы получите сообщение об ошибке, и новая запись создана не будет. Иногда это свойство используют сознательно, чтобы прервать создание новой записи в определенных ситуациях.
  • Если в качестве значения по умолчанию в реквизитах поля таблицы ничего не указано, то FoxPro использует в качестве значений по умолчанию пустые значения (для строк - пробелы, для чисел - ноль и т.п.), но ни в коем случае не значение NULL. Если Вы хотите использовать значение NULL, как значение по умолчанию, то его следует прописать явно как значение по умолчанию. Не забыв, разумеется, указать признак допустимости использования значения NULL для данного поля.

Когда и для чего использовать значение по умолчанию

О чем собственно речь? А речь о том, в каких случаях следует использовать именно значение по умолчанию, а не что-нибудь другое.

Основная задача значения по умолчанию - это сформировать некоторое значение автоматически, когда в случае отсутствия явного указания значения Вас не устраивает пустое значение поля.

Ну, например, генерация нового значения суррогатного ключа. Как правило, это всегда относительно сложная функция (конечно, если Вы не используете поле типа Integer (AutoIncrement), появившееся в 8 версии FoxPro). Эту функцию записывают в хранимых процедурах, а в свойствах таблицу указывают в качестве значения по умолчанию имя этой функции.

Кстати, использование функции для генерации нового значения суррогатного ключа удобно еще тем, что ее вызов можно добавить в значение по умолчанию ключевого поля Local View (кнопка Properties на закладке Fields в дезайнере Local View). В этом случае новое значение ключевого поля будет создаваться сразу при создании новой записи в Local View, а при сбросе изменений в исходную таблицу в качестве значения ключевого поля будет использовано значение, вычисленное в Local View.

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

Самым простым решением в данном случае является присвоение значения NULL при том условии, что данное поле не имеет признака допустимости использования значения NULL. Т.е. сформировать значение, которое заведомо будет отвергнуто в качестве допустимого значения. Это и будет сигналом внешнему приложению, что при создании нового значения суррогатного ключа произошла какая-то ошибка.

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

Дизайн полей таблицы

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

  • Format
  • Input Mask
  • Caption
  • Display library
  • Display class

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

Например, если Вы укажете для какого-либо поля значение свойства Format="T" (отсечение ведущих пробелов), то это отнюдь не значит, что Вы в принципе не сможете ввести в данное поле значение, имеющее ведущие пробелы.

Да, ведущие пробелы будут отсечены, если Вы будете редактировать данное поле в Browse-окне или же использовать прямые команды модификации данных (REPLACE, INSERT-SQL и т.п.)

Однако если Вы будете редактировать данное поле через объект формы (TextBox, Grid), то настройки свойства FORMAT используемых объектов перекроют настройки Format поля таблицы.

Таким образом, эти настройки определяют некоторые реквизиты полей таблицы, если явно не указаны другие значения для этих же реквизитов

В общем-то, назначение каждого из этих реквизитов ясно уже из их названия, но все-таки вкратце опишу их назначение:

Format - определяет формат ввода и вывода данных. Принимаемые значения можно посмотреть в описании к Format property

Input Mask - определяет способ ввода и вывода данных. Принимаемые значения можно посмотреть в описании к InputMask property

По большому счету и Format, и Input Mask предназначены для одного и того же. Просто Format действует на итоговое введенное значение (т.е. значение надо сначала ввести и только потом оно будет модифицировано в соответствии с настройками Format), а Input Mask на процесс ввода

Caption - название поля. Это будет либо заголовок поля в Browse-окне, либо Header.Caption в Grid (если Вы создадите Grid путем "перетаскивания" таблицы из DataEnvironment формы), либо Label.Caption слева от поля (если Вы создадите TextBox путем "перетаскивания" поля из DataEnvironment формы).

Лично мне эта настройка только мешает. Поскольку в процессе отладки я достаточно часто открываю таблицы в Browse-окне для уточнения тех или иных действий. И если вместо имени поля я вижу значение Caption, то приходится прилагать дополнительные усилия, чтобы уточнить о каком именно поле идет речь. Впрочем, это дело личных предпочтений и способов отладки приложения.

Display class - в каком классе следует отобразить содержимое данного поля

Display library - из какой библиотеки классов следует взять класс, указанный в свойстве Display class. Если он не указан, то предполагается базовая библиотека классов.

Вообще-то, какая именно реакция будет при "перетаскивании" полей из DataEnvironment формы определяется в настройках FoxPro: пункт главного меню Tools, подпункт Options, закладка Field Mapping

На этой закладке представлен список классов, которые будут использованы по умолчанию для отображения содержимого тех или иных типов данных, а под этим списком - набор переключателей, определяющих, что именно будет перенесено в объекты формы при "перетаскивании" поля из DataEnvironment

Как видите можно отключить копирование настроек Format, Input Mask, Caption при "перетаскивании" поля из DataEnvironment. Т.е. фактически отменить действие этих настроек в объектах формы.

Надо ли использовать эти настройки? А почему бы и нет! Надо только помнить, что в отличие от всех прочих настроек полей таблиц изменения в этих настройках не приведут к изменению соответствующих настроек в ранее созданных объектах формы. Поэтому после их модификации придется пройтись по всем ранее созданным классам, формам и отчетам для приведения ранее сделанных настроек в соответствие с новыми.

Директивы компилятора

Директивы компилятора - это те команды, которые начинаются с символа "#". Наиболее известные из них это:

  
  #DEFINE  
  #INCLUDE  
 

Однако есть и другие аналогичные команды.

Из-за недопонимания того, что собственно делают эти команды, возникает масса недоразумений. Самое главное, что следует понять об этих командах, это то, что они не работают в уже готовом (откомпилированном) приложении. Их цель - это указание компилятору, что именно надо подставить в данном месте именно в процессе компиляции. Собственно, поэтому они и называются "директивы компилятора"

Рассмотрим простейший код:

  
  #DEFINE MY_CONST 1  
  ?MY_CONST  
 

Что собственно делает этот код. И самое главное, когда он это делает. А происходит следующее:

  • На этапе компиляции фрагмента приложения, как только компилятор встречает директиву #DEFINE, он запоминает, что вместо слова MY_CONST необходимо подставить число 1
  • Если при дальнейшей компиляции компилятор встречает слово MY_CONST, он вместо него подставляет число 1 и продолжает компиляцию.

Т.е. фактически происходит замена слова MY_CONST на цифру 1, начиная с директивы #DEFINE и "ниже".

А "ниже" - это где?

Если речь идет о файле PRG, то тут все ясно: "ниже" - это до конца файла. И именно файла, а не процедуры. Т.е. если файл PRG содержит несколько процедур, то действие директивы #DEFINE будет распространяться на все строки файла PRG физически расположенные "ниже" нее вне зависимости от любых других команд (исключая другую директиву #UNDEFINE).

Но если речь идет о форме, и команда #DEFINE была дана в одном из методов формы, то предсказать на какие еще события и методы она подействует заранее невозможно. Поэтому, если необходимо, чтобы директива #DEFINE подействовала на все без исключения события и методы формы (или класса), эти директивы собирают в один общий файл, называемый "файл заголовка" или "заголовочный файл". И далее подключают этот заголовочный файл через специальный пункт меню Form (или Class), подпункт Include File.

Заголовочный файл - это обычный текстовый файл, имеющий расширение из одной буквы "h"

Следует иметь в виду, что директива компилятора действует только и исключительно в пределах одного объекта проекта. Чаще всего, под объектом проекта понимается конкретный файл. Хотя в отношении библиотеки классов, под объектом проекта следует понимать отдельный класс этой библиотеки классов.

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

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

В принципе, до некоторой степени эту рутинную работу по подключению заголовочного файла можно автоматизировать. Для этой цели служит системная переменная _INCLUDE или (что, то же самое) настройка через пункт системного меню Tools, подпункт Options, закладка File Locations, строка "Default Include File"

Т.е. если Вы дадите команду

_INCLUDE="C:\MyDir\MyHeadFile.h"

Или укажите этот файл в опциях, то при создании новых формы и классов указанный файл автоматически будет подключаться к этим формам и классам. Но эта настройка никак не повлияет на ранее созданные формы и классы. Более того, воспользовавшись пунктом меню Form (или Class), подпункт Include File Вы можете заменить подставленный заголовочный файл на другой или вовсе его удалить.

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

  
  #INCLUDE C:\MyDir\MyHeadFile1.h  
  #INCLUDE C:\MyDir\MyHeadFile2.h  
  #INCLUDE C:\MyDir\MyHeadFile3.h  
 

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

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

Разумеется, если Вы собираетесь компилировать какие-то фрагменты кода "на лету" в готовом приложении, то тогда заголовочный файл может пригодиться.

Но если директивы компилятора так неудобно использовать, то почему же их вообще продолжают использовать?

У меня нет достаточно убедительного ответа на этот вопрос. Поэтому ограничусь тем, что директивы компилятора - это еще один инструмент, который дает Вам FoxPro. А использовать его или нет, решайте сами.

Опубликовал: Владимир Максимов www.foxclub.ru

Комментарии: (0) | FoxPro | 2006-06-08

FoxPro : Советы начинающим Часть IV

FoxPro : Советы начинающим Часть IV

В этой статье :

Типы данных
Типы данных

Цель данного раздела - это не ознакомление со всеми существующими в FoxPro типами данных, а описание некоторых особенностей использования тех или иных типов данных. Понимание этих особенностей позволит упростить выбор типа данных при проектировании таблиц. А также уменьшить количество ошибок в программе.

Прежде всего, следует понимать, что существуют типы данных переменных памяти и типы данных полей таблиц. Это далеко не одно и то же. Например, если Вы используете в таблице поле типа Character, то количество символов в таком поле всегда ограничено некоторым числом не превышающем 254 символа. Но переменная памяти имеет значительно больший размер, ограниченный количеством символов 16,777,184

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

Ниже я буду описывать некоторые типы данных полей таблиц, по мере необходимости давая их же описание как переменных памяти


Точность расчета

Прежде чем говорить собственно о типах данных следует обратить внимание на точность расчета в FoxPro. В системных ограничениях FoxPro указано, что точность расчета ограничена 16 значащими цифрами.

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

Например, у числа

00010203.4050600

есть 10 значащих цифр, начиная с цифры 1 и заканчивая цифрой 6. А у числа

0.004050600

есть 8 значащих цифр, начиная с цифры 0 перед точкой и заканчивая цифрой 6.

Опираясь на собственный опыт работы с FoxPro, могу сказать, что 16 - это не точность расчета, а просто количество знаков, с которыми работает FoxPro при математических расчетах. Между этими понятиями есть разница. Более того, судя по всему, на точность расчета могут оказывать влияние какие-то системные ограничения как самой операционной системы, так и "железа". Выполните такую проверку

  
  CREATE CURSOR test (test N(20))  
  INSERT INTO test VALUES (1234567890123456)  
  INSERT INTO test VALUES (12345678901234567)  
  INSERT INTO test VALUES (123456789012345678)  
  INSERT INTO test VALUES (1234567890123456789)  
  INSERT INTO test VALUES (12345678901234567890)  
  BROWSE NOWAIT  
 

На разных компьютерах этот простой тест может давать несколько отличные результаты. При наихудшем раскладе, реально окажутся заполненными только первые 14 разрядов, а остальные разряды окажутся заполненными нулями. При наилучшем раскладе, реально заполненными будут первые 16 разрядов, а остальные - также заполнены нулями.

Таким образом, применительно к FoxPro можно говорить о том, что доверять можно только первым 14 значащим цифрам. Еще 2 цифры будут содержать сомнительные (но близкие к реальным) данные. А вот все значащие цифры, начиная с 17, будут недостоверны. Т.е. будут заполнены случайными данными.


Numeric

Numeric и Float - это два разных названия для одного и того же типа данных. В дальнейшем, я буду говорить о типе данных Numeric. По умолчанию, любая переменная памяти, содержащая число интерпретируется как переменная типа Numeric, если тип переменной не был указан явно каким-либо способом.

Очень важным для понимания особенностей типа данных Numeric является то обстоятельство, что физически эти данные хранятся как символьные данные. Т.е. каждая цифра, а также символ разделитель целой и дробной части физически хранятся как обычные символы. Если Вы откроете файл DBF как обычный текстовый файл (например, с помощью программы "Блокнот" ("Notepad")), то Вы увидите, что число 1234.56 прямо так и записано. Нет какого-либо преобразования.

Как следствие, ничто не мешает вместо дробной части записать целую часть числа. И действительно, если Вы определите размерность поля, например, как Numeric(5,2), то в такое поле можно записать данные до значения 99999, а не 99.99 как предполагается из заданной размерности. Т.е. указание дробной части носит скорее рекомендательный, чем обязательный характер и вся дробная часть (включая символ разделитель) в случае необходимости может быть использована для записи целой части числа.

Выполните такую проверку

  
  CREATE CURSOR test (test N(5,2))  
  INSERT INTO test VALUES (12.34)  
  INSERT INTO test VALUES (12345)  
  BROWSE NOWAIT  
 

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


Currency

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

Прежде всего, следует понимать, несмотря на то, что данный тип данных также относится к "числовым" данным (т.е. в нем хранятся числа), но это все-таки не тип данных Numeric. Как следствие, прямое сравнение данных типа Currency и Numeric может дать неожиданный результат. Например:

  
  ?268435456.3=NTOM(268435456.3)  
  ?268435456.4=NTOM(268435456.4)  
 

Первое сравнение, как и ожидалось, вернет .T., а вот второе совершенно неожиданно возвращает .F. Почему? Это знают только разработчики FoxPro. Но с практической точки зрения отсюда следует вывод, что перед сравнением разных числовых типов данных их следует приводить к одному типу данных. Причем приведение к типу Numeric требует дополнительного округления. Например:

  
  nNum=268435456.4  
  yCur=8435456.4  
  ?nNum=yCur  
  ?nNum=MTON(yCur)  
  ?NTOM(nNum)=yCur  
  ?nNum=ROUND(MTON(yCur),4)  
 

Символ "$" говорит о том, что далее идет число типа Currency. Его использование аналогично явному преобразованию через функцию NTOM(). Как видите, первые 2 сравнения вернут .F., в то время как последние 2 - .T.

Другая особенность типа данных Currency связана со способом округления результатов промежуточных вычислений. Сравните:

  
  ?4/3*3  
  ?/3*3  
 

Откуда взялось расхождение в четвертом знаке после запятой? А это как раз следствие особенностей округления промежуточных результатов расчета.

Дело в том, что как было сказано ранее, по умолчанию, все числовые переменные памяти считаются имеющие тип данных Numeric. Опять же, как было сказано ранее, FoxPro выполняет расчеты с точностью до 16 значащих цифр.

Т.е. в памяти, результат деления 4/3=1.333333333333333 - до 16 значащих цифр. А результат деления /3=1.3333 - до 4 знаков после запятой, поскольку тип Currency больше не хранит.

Теперь, когда этот промежуточный результат снова умножается на 3, получается: для типа Numeric - 3.999999999999999, а для типа Currency - 3.9999.

Завершающее действие - это округление результата до некоторого фиксированного количества знаков после запятой. В данном случае, выражение типа Numeric будет округлено до 2 знаков после запятой и получится 4.00, а тип Currency округляется до 4 знаков после запятой и остается те же 3.9999

Т.е. в принципе, использовать тип Currency можно, но следует иметь в виду приведенные выше особенности его работы, чтобы получать точные результаты. Но если Ваша программа предполагает сложные денежные расчеты, то лучше использовать типа данных Numeric(18,2) вместо Currency.


Memo

Данный тип предназначен для хранения символьных данных неопределенной длины. Точнее для символьных данных, для которых точно известно, что они могут содержать более 254 символов. А вот верхний предел ограничен числом 2ГБ (2 миллиарда символов - девять нулей) на размер файла с расширением FPT. В этом файле собственно и хранится содержимое полей типа Memo и General.

Особенность работы с мемо-полями заключается в том, что при любой модификации мемо-поля файл FPT увеличивается на некоторое количество байт кратное определенному значению. Это значение определяется настройкой SET BLOCKSIZE. По умолчанию, оно равно 64 байта. Т.е. даже если Вы просто стерли и тут же вставили один символ, то размер файла FPT тем не менее увеличится на 64 байта, а не останется неизменным как ожидалось. Проверьте:

  
  CREATE TABLE test FREE (test M)  
  =ADIR(aTest,"test.fpt")  
  ?aTest[1,2]  
  INSERT INTO test VALUES (space(1))  
  =ADIR(aTest,"test.fpt")  
  ?aTest[1,2]  
  USE  
  DELETE FILE test.*  
 

Как видите, я добавил в мемо-поле только один пробел, но размер файла FPT увеличился на 64 байта, а не на 1 как ожидалось.

А что же содержится в остальных 63 записанных байтах? А ничего! Это пустое место, которое уже никак, никоим образом не может быть использовано.

Таким образом, при интенсивной работе с мемо-полями в них скапливается достаточно большое количество пустого места. Для удаления этого пустого пространства необходимо периодически давать команду PACK. Или, если не хочется удалять записи помеченные как удаленные, PACK MEMO.

Проблема в том, что для выполнения команды PACK необходимо открыть таблицу в режиме EXCLUSIVE, что при работе в многопользовательском приложении - проблематично. Разумно вынести эту команду в специальную процедуру по регулярной очистке база данных, которую периодически запускает администратор или сам пользователь. Более подробно об этой стратегии описано в разделе Удаление записей в таблице


General

Данный тип предназначен для хранения OLE-объектов. Ну, например, в нем можно хранить файл Excel или результат работы MS Graph

Для работы с данными полями есть всего 2 команды

  
  APPEND GENERAL  
  MODIFY GENERAL  
 

Чтобы очистить поле General от содержимого надо просто дать команду APPEND GENERAL, не указав имени файла.

Причем описание опции LINK в команде APPEND GENERAL вводит в заблуждение в том смысле, что исходный файл будет скопирован в поле General в любом случае. Какие бы опции Вы не использовали.

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

А опция LINK в данном случае используется для того, чтобы синхронизировать изменения в оставшейся внешней копии файла и в той его копии, которая находится внутри поля General. Синхронизация происходит в момент открытия поля.

Можете провести простой эксперимент. Создайте в WinWord любой файл, например, test.doc. Теперь сделайте следующее:

  
  CREATE CURSOR test (testGen G)  
  APPEND BLANK  
  APPEND GENERAL testGen FROM "C:\Мои документы\test.doc" LINK  
  MODIFY GENERAL testGen  
 

Как видите, я задал опцию LINK, чтобы связать содержимое поля General и OLE-объект. Если Вы сделаете теперь изменения в файле "test.doc" открыв его в WinWord, то при очередном открытии этого поля General все изменения тут же в нем и отобразятся. Соответственно, верно и обратное. Изменения сделанные через вызов OLE-объекта в поле General попадут в исходный файл. Без опции LINK эти взаимные изменения не работают.

А теперь удалите файл "test.doc". Просто переместить его или переименовать недостаточно. Каким-то образом поле General найдет его под новым именем и на новом месте. Нужно именно удалить файл.

Открываем поле General и видим наш не существующий файл! Т.е. он таки записан в поле General несмотря на опцию LINK. Хотя модифицировать его уже не получится. При попытке сделать модификацию Вы получите сообщение об ошибке OLE.

Впрочем, в том, что файл OLE-объекта будет записан в поле General, можно убедиться, просто посмотрев размер файла FPT до вставки и после (для этого надо создать не курсор, а именно таблицу). Он увеличится примерно на размер вставляемого файла.

Другая особенность заключается в том, что поле General не предназначена для программной манипуляции с ее содержимым. Предполагается, что всю нужную обработку должен выполнять OLE-объект, а назначение поля General - это просто принять результаты изменения.

Частично проблему манипуляции содержимым решает опция DATA команды APPEND GENERAL, но это опять же не прямое, а опосредованное редактирование. Изменения DATA должен обработать OLE-объект, если он это умеет. Например с ними может работать такое OLE-объект, как MS Graph (для отображения графиков). Пример его использования можете посмотреть в стандартном проекте примеров Solution.pjx, который поставляется вместе с FoxPro (формы OleGraph.scx и Sctock.scx)

Поэтому, если Вы захотите, например, программно сохранить содержимое поля General как отдельный файл, то у Вас просто нет для этого никаких инструментов!

Таким образом, при использовании полей General Вы непомерно "раздуваете" базу данных (очень быстро растет размер файла FPT) данными, которыми Вы практически не можете манипулировать. Можно сказать, "архивом".

В связи с этими особенностями полей типа General я не рекомендовал бы использовать данный тип поля на постоянной основе. Т.е. как поле каких-либо основных таблиц базы данных

Собственно файлы OLE-объектов лучше хранить именно как файлы. В отдельной директории. А если их надо "прокачать" через поле типа General, то лучше создавать временную табличку или курсор непосредственно на момент исполнения приложения. В большинстве случаев хватает курсора, содержащего одну запись и одно единственное поле типа General.

Если Вы, тем не менее, твердо желаете сохранить файлы в базе данных, чтобы их не было на диске, то используйте для их хранения поля типа Memo(binary) примерно так:

  
  CREATE CURSOR test (testMemo M NOCPTRANS)  
  APPEND BLANK  
  APPEND MEMO testMemo FROM "C:\Мои документы\test.doc" OVERWRITE  
 

А чтобы извлечь файл обратно:

  
  COPY MEMO testMemo TO "C:\Мои документы\test.doc"  
 

Преимущества хранения файлов в поле типа Memo(binary) именно в том, что ими можно программно манипулировать. Чего нельзя сказать о поле General.

Кроме всего перечисленного не следует забывать, что, по сути, поле General - это особый вид Memo-поля. Соответственно на него распространяются те же особенности модификации, что и на Memo-поле описанные в разделе посвященному Memo. Т.е. требуется периодически давать команду PACK или PACK MEMO для очистки файла FPT от пустого пространства.


Binary

Binary - это не тип поля, а реквизит поля. Может использоваться только с символьными полями. Т.е. возможны Character (binary) и Memo (binary). При программном создании полей данное свойство указывается при помощи ключевого слова "NOCPTRANS".

Для чего, собственно нужен этот реквизит.

Дело в том, что обычно предполагается, что в символьных полях хранится некоторый текст, записанный в одной из поддерживаемых FoxPro кодовых страниц. Соответственно, при чтении таких полей FoxPro автоматически транслирует содержимое таких полей в текущую кодовую страницу. Благодоря этому механизму Вы можете совершенно спокойно открыть в Visual FoxPro таблицу созданную в FoxPro for DOS в кодовой странице 866, и текст будет выглядеть нормально, а не как набор закорючек.

Однако в некоторых случаях этот автоматический механиз трансляции необходимо отключить. Т.е. нужно, чтобы транслировались данные из всех прочих символьных полей, а вот из этих - не надо. В принципе, это можно сделать программно, используя команду SET NOCPTRANS. Но уж больно это утомительно. Лучше указать это непосредственно в реквизитах таких полей.

В каких же случаях может возникнуть необходимость в запрете автотрансляции символов.

А в тех, когда в этих полях не содержится текст, который пользователь должен смотреть и править.

Ну, например, в разделе посвященном типу поле General, я привел пример записи файла с расширением DOC в поле типа Memo(Binary). Разумеется, пытаться прочитать такое содержимое как обычный текст бессмысленно.

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

А возможно данные коды и вообще не предназначены для расшифровки и применяются "как есть". Например, идентификатор записи. Хотя для FoxPro - это не очень хорошее решение.


AutoIncrement

AutoIncrement - это не тип поля, а реквизит поля. Может использоваться исключительно с полем типа Integer. В одной и той же таблице может быть несколько полей со свойством AutoInc.

Это свойство является нововведением 8 версии FoxPro. В более ранних версиях его просто не было.

Более подробно о свойствах полей, использующих данное свойство можно почитать в статье "Autoincrementing Field Values in Tables" из Help к VFP8. В этой статье достаточно подробно описаны особенности его использования. Вкратце, перечень особенностей сводится к следующему:

  • В заголовке таблицы в описании данного поля хранится очередное (не использованное) значение и шаг автоинкремента. При добавлении новой записи изменяется очередное значение в заголовке таблицы. Посмотреть очередное значение и шаг автоинкремента можно, используя функцию AFIELDS(). 17 и 18 столбец создаваемого массива соответственно (в более ранних версиях функция AFIELDS() создавала массив из 16 столбцов).
  • Шаг автоинкремента может принимать значения от 1 до 255. Он не может принимать нулевое или отрицательное значение.
  • Поля с данным свойством не могут редактироваться. Будет ли попытка отредактировать данное поле вызывать сообщение об ошибке, регулируется настройкой SET AUTOINCERROR
  • В буферизированных таблицах процесс добавления новой записи примерно на 35% медленнее по сравнению с таблицами, не имеющими автоинкрементных полей
  • FoxPro никак не контролирует появление "дыр" в последовательности автоинкремента. "Дыры" могут появляться, например, при удалении ранее созданных записей. Значение новой записи берется из заголовка таблицы, а не на основе какого-либо расчета.
  • В случае буферизации таблицы, при работе нескольких пользователей одновременно также могут образовываться "дыры" в последовательности автоинкремента, если один из пользователей не принял внесенные изменения (TableRevert())
  • При использовании Local View новое значение поля с автоинкрементом отобразится только после выполнения перезапроса (Requery()).

Можно заметить, что при неаккуратном создании автоинкрементного поля можно вызвать ошибку переполнения данных, если задать величину очередного значения близкую к предельно допустимому для типа Integer (2,147,483,647). В этом случае Вы можете при создании новой записи получить сообщение о переполнении типа данных.

Следует понимать, что свойство AutoIncrement не обеспечивает уникальность значения поля, на которое это свойство распространяется. Это просто не его задача. На первый взгляд, это может показаться странным. Раз поле нельзя редактировать и значение нового поля получается путем прибавления некоторого значения к предыдущему, то где же тут взяться возможным повторам?

Ну, например, предположим, что изначально Вы создали простое поле типа Integer и создали несколько записей. Затем Вы решили, что лучше вместо типа Integer использовать тип Integer(Autoincrement) и модифицировали структуру уже существующей таблицы. Если Вы проявили неаккуратность и при настройке свойств автоинкремента оставили очередное значение автоинкремента равным 1, то новая запись будет создана со значением равным 1, несмотря на то, что возможно запись с таким значением уже существовала.

В связи с этим, если принципиально важным является именно уникальность значения поля с автоинкрементом, например, если Вы хотите использовать его как суррогатный ключ, то необходимо добавить такой контроль. Например, создав индекс типа Primary или Candidat. И не надеяться на то, что повторов "не может быть, потому что не может быть никогда". Повторюсь, свойство AutoIncrement не имеет никаких функций по контролю уникальности значения.

Пример использование полей типа AutoIncrement можно посмотреть в новой базе данных NorthWind.dbc из поставки VFP8. Эта база используется как пример "внешней" базы для уяснения работы с новым объектом FoxPro CursorAdapter

Свойство AutoIncrement является нововведением 8 версии FoxPro. И судя по всему, даже сами разработчики FoxPro не очень-то представляют, как его можно использовать.

Напрашивается решение использовать такое поле как уникальный идентификатор записи при генерации суррогатного ключа. Однако в "старых" стандартных примерах это свойство не используется. При генерации суррогатного ключа по-прежнему используют специальную функцию NewId() и хранение последнего использованного значения во временной таблице.

По большому счету, это можно списать на консерватизм программистов. Например, в примерах от FoxPro по-прежнему в качестве ключевого поля используется тип данных Character, хотя собственно значение - это автоинкрементное число, но записанное как символьная строка.

Да, есть ситуации, когда использование в качестве суррогатного ключа типа данных Character предпочтительнее, чем Integer. Но это задачи особой сложности, с которыми большинству программистов не придется сталкиваться (репликация).

Аналогично есть ситуации, когда использование функции для генерации уникального ключа (NewId()) предпочтительнее использования AutoIncrement (обновляемые Local View по подчиненным таблицам с одновременным редактированием нескольких записей главной таблицы). Но! Я бы не сказал, что такие задачи невозможно решить при использовании автоинкрементных полей. Хотя я согласен, при определенных обстоятельствах, использование автоинкрементых полей может потребовать несколько более сложного программирования, чем использование функции для генерации уникального ключа (NewId())

Здесь не место для подробного обсуждения решения подобных проблем, но вкратце решение сводится к следующему:

  • Если главная таблица - это также Local View, то она должна иметь обязательно строковую буферизацию, а таблица-источник должна находится в табличной буферизации
  • При создании новой записи в главном Local View немедленно делаем сброс в буфер таблицы-источника
  • Поскольку в буфере таблицы-источника новое значение автоинкрементного поля уже создано, то можно тут же его прочитать и использовать для создания ссылки в подчиненном Local View
  • Найти новую запись в буфере таблицы-источника можно по максимальному значению автоинкрементного поля
  • По окончании редактирования осуществляется сброс буфера сначала из главной таблицы, потом из подчиненного Local View. Или же осуществляется откат, в случае отмены внесенных изменений

А вот чего не следует делать при работе с автоинкрементыми полями, так это опираться на очередное значение, хранимое в заголовке таблицы. Т.е. то значение, которое возвращается в 17 столбце массива, создаваемого функцией AFIELDS().

Дело в том, что функция AFIELDS() считывает данные из буфера таблицы, а при добавлении новой записи новое значение автоинкремента берется непосредственно из таблицы-источника. Но это могут оказаться разные значения.

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

  • Первый пользователь добавил новую запись в буфер. Значение в новой записи равно 1. Очередное значение автоинкремента в буфере таблицы равно 2 и очередное значение автоинкремента собственно в таблице равно 2.
  • Второй пользователь добавил новую запись в буфер. Значение в новой записи равно 2. Очередное значение автоинкремента в буфере таблицы равно 3 и очередное значение автоинкремента собственно в таблице равно 3.
  • А у первого пользователя очередное значение автоинкремента в буфере таблицы по-прежнему равно 2, хотя очередное значение автоинкремента собственно в таблице равно уже 3. И при создании новой записи будет использовано именно значение 3.

Поэтому, при работе с автоинкрементыми полями в общем случае невозможно предсказать, какое значение будет у новой записи. Можно только прочитать это значение сразу после создания новой записи. А чтобы не перепутать записи, созданные разными пользователями, следует создавать новые записи, наложив на таблицу-источник табличную буферизацию.

Проще всего найти новую запись, опираясь на максимальное значение автоинкрементного поля, особенно, если по нему построен индекс:

  
  select MyTab  
  SET ORDER TO FieldAuto  
  GO BOTTOM  
 

В результате, указатель записи окажется на той записи, которая имеет максимальное значение ключа в соответствии с выражением индекса FieldAuto. А если выражение этого индекса состоит только из имени поля, то Вы и попадете на последнюю созданную запись.

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

  
  select MyTab  
  SET ORDER TO 0  
  GO BOTTOM  
 

К сведению

Существует команда, которая добавляет запись не в конец файла, а в указанное место. Это старая команда еще из DOS-версий FoxPro, которая называется INSERT (не надо путать ее с INSERT-SQL). Но данная команда оставлена только для совместимости с более ранними версиями и ее использование сопряжено с большим количеством ограничений. Так что, лучше ее не использовать.

Удаление записей в таблице

Таблицы FoxPro - это прямые наследники формата DBASE. В этом формате процесс удаления записей разбит на 2 этапа. Сначала записи помечаются как удаленные, но физически все еще сохраняются в таблице. А для их физического удаления необходимо дать специальную команду. Причем физическое удаление требует эксклюзивного (единоличного) доступа к таблице.

Под термином "удаление" в FoxPro понимается именно установка метки на удаление, а не физическое удаление записи в таблице. Т.е. команды DELETE, DELETE-SQL физически не удаляют записи; триггер DELETE срабатывает при установке метки на удаление; триггер INSERT срабатывает при снятии метки на удаление (ну, и при физическом создании новой записи)

Чтобы записи, помеченные как удаленные, не отображались при работе с таблицами, используют специальную глобальную настройку

  
  SET DELETED ON  
 

Здесь несколько "нелогичная" настройка: ON - прячет записи, помеченные как удаленные (учитывает такие записи), а OFF - наоборот, отображает такие записи (игнорирует, не учитывает такие записи).

Следует помнить, что при использовании Private DataSession настройка SET DELETED сбрасывается в значение по умолчанию (OFF). Т.е. при открытии Private DataSession необходимо позаботиться о корректной настройке среды данных.

Для физического удаления записей ранее помеченных как удаленные, используется специальная команда "PACK". Следует помнить, что в отличие от команд на удаление (DELETE, DELETE-SQL) данная команда требует открытия таблицы в режиме EXCLUSIVE. Что, как правило, недопустимо при работе в сетевом режиме. Для преодоления этого противоречия используется одна из 2 стратегий программирования:

  • При создании новых записей использовать записи ранее помеченные как удаленные
  • Вынести выполнение команды PACK в специальные служебные процедуры по обслуживанию базы данных
При создании новых записей использовать записи ранее помеченные как удаленные

В принципе, тут возможны два варианта решения: восстанавливать ранее удаленные записи (командой RECALL) или вместо удаления записей по команде DELETE менять содержимое специально созданного для этой цели поля, как раз и определяющего - удалена запись или нет.

В любом случае, данная стратегия предполагает отказ от целого ряда команд и функций которые так или иначе могут автоматически создавать новые записи (APPEN BLANK, APPEND FROM, INSERT-SQL, буферизация, обновляемые Local View и т.п.). А создание новых записей осуществлять через специальную функцию, которая сначала ищет запись, помеченную как удаленная, и использует ее как новую запись.

К недостаткам данного способа следует еще отнести относительную сложность процедуры вставки новой записи. Тут недостаточно сделать просто LOCATE + RECALL. Все несколько сложнее. Достаточно много нюансов блокировки при работе в многопользовательском режиме.

Попробуйте, например, прикинуть какие потребуются действия, чтобы при создании новой записи "одновременно" двумя пользователями не произошло затирание информации, введенной одним из пользователей. Т.е. два пользователя "одновременно" сделали LOCATE, но RECALL естественно сделал только один из них. Как заставить другого пользователя отказаться от притязаний на обладание найденной записью в пользу первого, сделавшего RECALL? Разумеется, задача решаемая. Я просто хочу показать, что она достаточно не тривиальна.

Кроме того, данная стратегия фактически запрещает использовать Memo-поля. Почему? Да потому, что при работе с Memo-полями время от времени необходимо давать команду PACK. Это связано с особенностью работы с memo-полями. Подробнее об этих особенностях читайте в разделе "Типы данных". Ну а если все равно приходится давать команду PACK, то какой смысл во всех этих сложностях?

Итого, получается, что использование данной стратегии накладывает значительные ограничения на программиста.

После всех этих рассуждений, возникает вопрос, который следовало бы задать с самого начала. А когда реально нет возможности выполнить команду PACK?

Нет возможности выполнить команду PACK, когда приложение работает в многопользовательском режиме и невозможно его остановить даже на профилактику, хотя бы на полчаса раз в год.

Разумеется, такие задачи тоже существуют. Но! Если у Вас стоит именно такая задача, то выбор в качестве хранилища данных таблиц DBASE - это весьма странный выбор. Дело тут не в том, что такую задачу сложно решить при помощи таблиц DBASE, а в том, что такие задачи предъявляют повышенные требования, как к надежности, так и к объему (количеству записей) базы данных. Т.е. в большинстве случаев требования таких задач заведомо превышают физические возможности формата хранения данных в таблицах DBASE.

Вынести выполнение команды PACK в специальные служебные процедуры по обслуживанию базы данных

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

Казалось бы, есть более простое решение: давать команду PACK при каждом входе (выходе) из программы. Однако это лишь кажущаяся простота.

Если Вы разрабатываете программу для одного пользователя, то в принципе такая стратегия оправдана. Но при работе в сетевом (многопользовательском) режиме данная стратегия приведет к тому, что часть пользователей просто не сможет с первого раза войти в программу, поскольку в этот момент другой пользователь будет "держать" таблицы в режиме EXCLUSIVE.

Кроме того, нет особой необходимости физически удалять записи, помеченные как удаленные, непосредственно в момент (или в том сеансе данных) когда произошла установка метки на удаление. При наличии небольшого количества записей помеченных как удаленные скорость работы (выполнения запросов, поиск данных) упадет незначительно. Ну, будет немного "мусора", ну и что?

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

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


Вывод однозначен. Использование специальной процедуры очистки базы данных от записей помеченных как удаленные предпочтительнее во всех смыслах перед повторным использованием ранее удаленных записей.

Следует ли создавать индекс по выражению Deleted()

О чем вообще речь? Дело в том, что для ускорения выборок данных в FoxPro используется специальная технология, называемая Rushmore - оптимизация. Что это такое в данном случае не важно. Важным является тот факт, что данная технология для ускорения выборки использует индексы. Без индексов она просто не работает.

Так вот, факт наличия удаленных записей в таблице хотя и отсекается настройкой SET DELETED ON, но, тем не менее, влияет на факт оптимизации запроса. По существу, настройка SET DELETED ON - это специфический фильтр, накладываемый на таблицу или дополнительное (неявное) условие выборки записей. Ну, а раз есть условие, но по нему нет индекса, то это приводит к снижению уровня Rushmore-оптимизации.

В связи с этим, в литературе советуют создавать индекс по выражению Deleted() примерно такого вида

INDEX ON Deleted() TAG Udal

Так вот, несмотря на то, что создание такого индекса приведет к улучшению Rushmore - оптимизации, но в подавляющем большинстве случаев это приведет к замедлению получения выборки. Подробнее о причинах такого поведения читайте в разделе Сколько и каких индексов надо создавать

Т.е. я бы НЕ рекомендовал Вам создавать подобный индекс.

Команда ZAP

Для физического удаления записей в таблице существует еще одна команда: ZAP. Данная команда предназначена для физического удаления вообще всех записей таблицы. Т.е. ее использование по результатам действия эквивалентно такой последовательности команд:

  
  select MyTab  
  DELETE ALL  
  PACK  
 

Однако есть и отличия. Дело в том, что при использовании команды DELETE срабатывают все связанные с данной операцией триггеры на удаление. Если, например, триггер на удаление запретит удаление какой-либо записи, то она и не сможет быть удалена.

Так вот, команда ZAP не вызывает срабатывание триггеров на удаление. Просто удаляет все записи таблицы без каких-либо проверок. В связи с такой особенностью, FoxPro при использовании данной команды обязательно попросит подтвердить Ваше решение удалить все записи. Подавить выдачу такого системного подтверждения, можно сделав настройку SET SAFETY OFF.

Также как и команда PACK, команда ZAP требует открытия таблицы в режиме EXCLUSIVE.

Хранимые процедуры

Хранимые процедуры - это те процедуры, которые хранятся в контейнере базы данных.

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

Если интересно, где именно они хранятся, то можете открыть базу данных как таблицу и увидеть, что текст "хранимых процедур" хранится в Memo-поле "Code". В строке со значением поля ObjectName = "StoredProceduresSource" исходный текст "хранимых процедур", а в строке со значением поля ObjectName = "StoredProceduresObject" - откомпилированный текст. В принципе, если Вы желаете скрыть от слишком "продвинутого" пользователя исходный текст "хранимых процедур", то Вы можете смело очистить содержимое поля Code для строки со значением поля ObjectName = "StoredProceduresSource", поскольку в процессе работы используется не исходный, а откомпилированный текст "хранимых процедур".

"Хранимые процедуры" становятся доступны в момент открытия контейнера базы данных, а контейнер базы данных автоматически открывается при открытии любой таблицы включенной в базу данных. Таким образом, "Хранимые процедуры" можно использовать сразу после открытия таблицы без каких-либо дополнительных команд типа SET PROCEDURE.


Для чего нужны хранимые процедуры

А зачем вообще понадобилось хранить какие-то процедуры внутри контейнера базы данных? Разве недостаточно использовать обычные процедурные файлы?

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

Исходя из данного предназначения хранимых процедур, становится ясно, какие именно процедуры и функции стоит в ней хранить, а какие нет.


Если процедура или функция обеспечивает целостность и непротиворечивость данных вне зависимости от приложения, то такую процедуру или функцию следует хранить в "Хранимых процедурах"

Определение получилось несколько мудреное. Если сказать то же самое проще, то в "Хранимых процедурах" должны хранится те процедуры и функции, вызов которых прописывается в свойствах таблиц или контейнера базы данных. Имеется в виду, что когда Вы вызываете режим модификации таблицы, то там есть ряд мест, где можно написать вызов функции (Rule, Triggers, Default). Вот те функции, которые там вызываются и должны быть записаны в "хранимых процедурах".

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


Особенности работы с хранимыми процедурами

Поскольку физически "хранимые процедуры" хранятся в Memo-полях, то в связи с определенными особенностями по работе с Memo-полями после внесения исправлений в "хранимые процедуры" следует выполнить очистку базы данных для уменьшения размера файла DCT и удаления неиспользуемого пространства.

"Хранимые процедуры" можно выгрузить в текстовый файл командой COPY PROCEDURES и соответственно загрузить командой APPEND PROCEDURES. Т.е. в принципе, можно динамически добавлять/удалять хранимые процедуры, хотя я не советовал бы заниматься этим новичкам. Слишком велик риск, разрушить все правила целостности базы данных.

Список имен всех "хранимых процедур" текущей базы данных можно получить по команде DISPLAY PROCEDURES (или LIST PROCEDURES)

Связи и отношения между таблицами

Как обычно, в FoxPro существует путаница и с этим понятиями. Точнее так, эта путаница возникла при включении в среду FoxPro такого объекта как Контейнер базы данных. Впрочем, по порядку.

Итак, в FoxPro термин "связь" или "отношение" применяется в следующих случаях

  • Постоянная связь - это некоторый графический объект, визуально отображающий взаимосвязь двух таблиц между собой в контейнере базы данных
  • Связь - это некоторая настройка, накладываемая на две таблицы и определяющая положение указателя записи в подчиненной таблице при перемещении указателя записи в главной таблице
  • Связь - это некоторый графический объект, визуально отображающий взаимосвязь таблиц в предыдущем смысле, но отображаемая в DataEnvironment формы или отчета

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


Постоянная связь (persistent relationship)

Повторю здесь еще раз свое определение термина "постоянная связь"

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

Обращаю внимание на то, что это именно "графический объект". Он не несет за собой никакого физического смысла.

Но если "постоянная связь" не имеет физического смысла, то для чего же она используется? Вероятно, точнее, следовало бы определить "постоянную связь", как некоторый шаблон, который используется при создании других объектов. Собственно, я знаю только о трех применениях "постоянной связи"

  • Для настройки и создания определенного вида триггеров в Referential Integrity
  • Автоматически предлагает создать обычную связь между таблицами, при включении их в DataEnvironment формы или отчета
  • Автоматически предлагает взаимосвязь нужного вида JOIN при включении таблиц в дизайнер запросов (Query или Local View)

Обратите внимание, что связь в DataEnvironment (или в дизайнере запросов) уже не имеет никакого отношения к "постоянной связи" в контейнере базы данных.

Да, она может быть создана автоматически на основе существующей "постоянной связи", но это будет исключительно рекомендация, с которой Вы можете согласиться или НЕ согласиться и удалить ее из DataEnvironment (или в дизайнере запросов)

Программно "постоянная связь" настраивается через команду ALTER TABLE, используя ключевое слово REFERENCES. "Постоянная связь" всегда устанавливает связь между тэгами структурных индексных файлов связываемых таблиц. Причем хотя бы в одной из таблиц этот тэг должен иметь тип "Primary". Т.е. связь должна быть либо один-к-одному, либо один-ко-многим. Но никак не много-ко-многим.

Связь вообще и "постоянную связь" в частности нельзя настроить дважды между одними и теми же таблицами. Пусть и по разным критериям. В FoxPro такое недопустимо, даже если связь осуществляется через таблицы посредники.

Т.е. если уж вы настроили связь между двумя таблицами, то другую связь между этими же таблицами Вы можете создать только взамен существующей, но никак не одновременно с ней. Попытка организации такой связи вызовет сообщение об ошибке.

"Постоянная связь" может быть настроена "сама на себя". Т.е. можно установить постоянную связь между двумя индексами одной и той же таблицы. Это имеет смысл, если таблица имеет "древовидную" структуру и Вы хотите установить триггер на удаление по типу "Restrict" (установить триггер на удаление по типу "Cascade" - не получится, точнее он не будет работать, поскольку в FoxPro запрещена рекурсия триггеров).

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

Так-то оно так, да вот FoxPro имеет весьма слабый инструментарий для полноценного проектирования базы данных. Вот и эта картинка. Физически она хранится в файле ресурсов FoxUser.dbf (fpt). Т.е. если Вы переносите проект на новое место (в другую папку, на другой компьютер), то чтобы картинка не сбилась, надо будет захватить и файл ресурсов. Подробнее о том, как это сделать читайте в разделе Содержимое главной директории проекта

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

Для полноценного проектирования базы данных лучше использовать другие продукты, которые имеют общее название: Case - средства для проектирования базы данных. Из наиболее известных это:

  • Power Designer - считается самым лучшим
  • ERWin - пожалуй, самый известный в паре с BPWin
  • Visio - знаменит тем, что это продукт MicroSoft, который был русифицирован, и можно найти бесплатную версию в Internet

В принципе была попытка создать полноценное средство для проектирования базы данных в FoxPro. Этот продукт получил название FoxCase. Но ни широкого распространения, ни полноценного развития он так и не получил.


Связь (обычная)

В принципе, в противовес "постоянной связи" этот вид связи следовало бы назвать "временная связь". В том смысле, что данный тип связи устанавливается "по требованию". Т.е. вот надо мне в этом месте организовать связь, я тут же ее и организовываю, используя команду SET RELATION. Отпала в ней надобность - тут же и удаляю. Или она сама удалится при закрытии одной из связанных таблиц.

Это так надо бы было назвать, но дело в том, что данный вид связи исторически появился раньше, чем "постоянная связь". Еще в версии FoxPro for DOS. Когда "постоянной связи" еще в проекте не было. А поскольку не было необходимости этот вид связи от чего-то отличать, то его так и назвали "связь". Без каких-либо уточняющих прилагательных.

Поэтому, когда в FoxPro используют термин "связь", то, скорее всего, речь идет именно об этой связи. Если же говорят о "постоянной связи", то обязательно добавляют уточняющее прилагательное "постоянная".

Итак, что же такое "связь" в FoxPro


Связь- это некоторая настройка, накладываемая на две таблицы и определяющая положение указателя записи в подчиненной таблице при перемещении указателя записи в главной таблице

Все. На этом функции связи заканчиваются. Более того, связь действует только до тех пор, пока вы находитесь в рабочей области главной таблицы. Стоит Вам перейти в рабочую область подчиненной таблицы, как всякие ограничения перестают действовать, и Вы можете спокойно переместить указатель записи на абсолютно любую запись.

Можно сказать, что связь - это взгляд на подчиненную таблицу со стороны главной. Не то, что есть подчиненная таблица на самом деле, а то, как ее видит главная таблица. Изменяя "точку зрения" (текущую рабочую область) мы по-другому видим и подчиненную таблицу.

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

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

Еще одна тонкость заключается в том, что связь можно настроить не по полному, а по частичному (по первым символам) совпадению ключа. Разумеется, если используется настройка SET EXACT OFF (это настройка по умолчанию).

Например, если подчиненная таблица имеет 2 поля ParentID и NickName и требуется упорядочить записи в пределах каждого значения ParentID в алфавитном порядке NickName, то в подчиненной таблице строится примерно такой индекс:

INDEX ON ParentID+NickName TAG SortOrd

Здесь я предполагаю, что ParentID - это поле символьного типа. Тогда в главной таблице настраивается связь по выражению только ParentID

SET RELATION TO ParentID INTO ChildTab

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

Строго говоря, команда SET RELATION TO устанавливает связь по типу один-к-одному. Т.е. если реально связь имеет вид один-ко-многим, то Вы не увидите этих "многих". Отображаться будет только первая попавшаяся запись дочерней таблицы. Для установки связи один-ко-многим после команды SET RELATION TO следует дать команду SET SKIP TO. В подавляющем большинстве случаев использовать команду SET SKIP TO нет необходимости. Надобность в ней возникает только в случае визуального отображения связи вида один-ко-многим. Если речь идет о программировании, то стоит ограничиться только командой SET RELATION TO

Если Вы работаете с DataEnvironment формы или отчета, то установка связи между таблицами фактически означает команду SET RELATION. А установка значения свойства объекта Relation.OneToMany = .T. фактически означает команду SET SKIP TO. Еще раз напомню, для правильной работы связи просто необходимо установить главный индекс в подчиненной таблице. Т.е. свойство Order соответствующего объекта Cursor. Индекс главной таблицы на работу связи никак не влияет.

Особенности работы SET RELATION в Grid (Browse - окне)

Если главную таблицу Вы отображаете в одном Grid, а подчиненную в другом, то только одна команда SET RELATION TO (без дополнительной команды SET SKIP TO) даст эффект отображения данных вида один-ко-многим. Т.е. при перемещении указателя записей в главном Grid в подчиненном автоматически будут отображаться только записи соответствующие текущей записи главной таблицы. Без каких-либо дополнительных настроек и программирования.

Если данные и главной и подчиненной таблицы Вы отображаете в одном Grid, то требуется дать команду SET SKIP TO для визуализации связи один-ко-многим. При этом строки главной таблицы, которым соответствует "много" записей в подчиненной таблице будут отображать свое содержимое только для первой записи подчиненной таблицы, а содержимое остальных строк будет отображаться в виде "квадратиков"

В Grid существует набор свойств, которые позволяют организовать связь с главной таблицей, если в Grid отображается содержимое подчиненной таблица (ChildOrder, LinkMaster, RelationExp). Но я не советовал бы их использовать, поскольку их настройка "наложится" на настройку объекта Relation в DataEnvironment и результат выйдет совершенно непредсказуемый. Лучше настраивать связь в том объекте, который для этого собственно и предназначен.

Особенности работы SET RELATION в отчетах (Report)

Если таблицы имеют связь вида один-ко-многим, то для печати в отчете этих "многих" обязательно следует использовать команду SET SKIP TO (для DataEnvironment настроить Relation.OneToMany = .T.). В этом случае строки главной таблицы, которые имеют несколько записей в подчиненной таблице "размножатся"


Когда следует использовать связь

Ну, с "постоянной связью" все относительно просто. Ее следует использовать, если Вы собираетесь построить Referential Integrity. Просто все остальные способы использования "постоянных связей" не стоят усилий по их созданию.

А вот с "обычной" связью все не так однозначно.

Дело в том, что связь имеет смысл при отображении данных (Grid, Browse, Report), а при программировании зачастую удобнее пользоваться прямым поиском в дочерней таблице через LOCATE или SEEK() (не всегда, но "как правило").

Да и при просмотре не все так гладко. Есть несколько проблем, которые в принципе решаемы при помощи связей, но приводят к заметному (для пользователя) "притормаживанию" приложения:

  • На подчиненную таблицу требуется наложить дополнительный фильтр
  • Необходимо иметь возможность изменения сортировки подчиненной таблицы

Эти и ряд других проблем решаются с помощью создания выборок (Select-SQL) или Local View. Когда отображается не вся таблица, а только нужная ее часть. Некое подобие клиент-серверной технологии. Но в этом случае фактически отпадает надобность в установке связи, поскольку вся необходимая информация и так отбирается в запросе или Local View.

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

Так что же, связи вообще не нужны? Ну, почему же. Просто они, как и индексы для сортировки (имею в виду индексы, необходимые для отображения информации в нужном порядке) перешли в разряд "второстепенных" инструментов и используются не на основных таблицах, а на временных выборках и Local View.

Опубликовал: Владимир Максимов www.foxclub.ru

Комментарии: (0) | FoxPro | 2006-06-08


Страница 6 из 51Первая«3456789 »Последняя