Новости :

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

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


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

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

О каком "провале" собственно идет речь?

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

Второй разрыв - это когда и что лучше всего применять. Обычно изложение строится таким образом: для решения задачи сделаем так, так и вот так. Но не предпринимается даже попытки объяснить, а почему собственно не иначе, когда есть другой способ? Как следствие, когда новичек узнает о том, что одну и ту же задачу можно решить даже не двумя, а тремя, четыремя и более способами у него в голове начинается полная карусель. Бросает из одной крайности в другую.

Есть и еще одна проблема, связанная с предыдущими двумя. Обычно в FoxPro начинают разбираться люди, уже попробовавшие свои силы в программировании на других языках (чаще всего в Delphi или Basic, да даже программисты работавшие ранее в FoxPro for DOS) и они пытаются применить стиль программирования хорошо (или не очень) им известного языка в Visual FoxPro и очень удивляются сталкиваясь с неожиданными (в смысле - не ожидАемыми) проблемами: вот ведь, в руководстве написано, что это можно, а у меня не выходит! А проблема оказывается в самом стиле программирования. Кое-какие приемы, хорошо работающие в Delphi для FoxPro являются попросту неприменимы.

Вот я и попытаюсь заполнить этот "провал" и описать некоторые рекомендации по программированию на FoxPro, с пояснениями почему собственно желательно делать так, а не иначе.
Комментарии: (0) | FoxPro | 2006-06-08

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

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

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

Таблица
Таблица


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

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

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

Таблица- это некий образ файлас расширением DBFоткрытый в указанной сессии данныхи в указанной рабочей области.

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

Алиас(alias) - это "псевдоним" файла DBFоткрытого в какой-либо рабочей области. Как правило, алиас совпадает с именем файла DBF. Однако это не всегда так. Дело в том, что в одном сеансе данных не может быть двух одинаковых алиасов. Поэтому, если алиас таблицы не указан явно при ее открытии (опция ALIAS в команде USE, или каким-либо другим способом), то FoxPro самостоятельно назначит уникальный алиас для таблицы, начав разумееется с алиаса совпадающего с именем файла DBF если это возможно.

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

В версиях FoxPro 2.x в том смысле, в котором используется термин "таблица"использовался термин "база данных"(видимо отсюда пошло расширение - первые буквы английской фразы DataBase File), поскольку в тех версиях еще не существовало файла DBC. Соответственно, когда программисты переходят на версию Visual FoxPro, то их бывает достаточно трудно понять из-за этой путаницы с терминами.

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

Рабочая область(Work area) - это некий числовой идентификатор, который может быть присвоен таблице. Если у Вас был опыт программирования в других языках, то можно сказать, что рабочая область - это "хэндл" или "дескриптор" таблицы внутри среды FoxPro

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

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

Кроме понятия "рабочая область" в FoxPro введено понятие "сеанс данных"(DataSession)

Сеанс данных (DataSession)- это некоторая динамическая копия среды FoxPro. Открывая среду FoxPro Вы автоматически открываете сеанс данных, который автоматически же и завершается при выходе из FoxPro. Но внутри собственно FoxPro Вы имеете возможность сделать как бы копию среды FoxPro используя так называемые "частные сеансы данных" (Private DataSession)

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

В результате таблицы открытые в одном сеансе данных "не видны" в другом сеансе данных. И соответственно манипуляции (не все) производимые с таблицами в одном сеансе данных не влияют на другие сеансы данных.

Самый распростарненный случай применения Private DataSession- это одновременное открытие нескольких форм, рассматривающих один и тот же документ с разных сторон. Как правило, для этой цели между таблицами устанавливается связь по SET RELATION. Если открыть подобные формы в одном сеансе данных, то зачастую это становится большой проблемой, поскольку в одной форме требуется наложить на таблицы одни индексы и связи, а в другой - другие. И переключение из одной формы в другую приводит к непредсказуемым изменениям содержимого.

Название таблицы

Файл таблицы, как и любой другой файл в системе Windows может содержать до 128 символов, содержать пробелы, русские символы, цифры и некоторые спец.символы. Однако для упрощения работы в FoxPro я бы порекомендовал следующие ограничения в наименовании файла таблицы
  • Не использовать в названии русские символы - причина этой рекомендации в том, что FoxPro разрабатывался прежде всего для англоязычных пользователей и использование в нем символов другого языка - это уже последующее дополнение. Как следствие, велик риск, что чего-то, где-то недосмотрели и при определенных ситуациях русские буквы в имени вызовут неожиданные глюки
  • Не использовать в названии пробелы - в принципе, ошибок использование пробелов не вызовет, но несколько усложнит сам процесс программирования, поскольку имена и пути доступа, содержащие пробелы необходимо заключать в кавычки. Просто добавит лишней заботы - не забывать кавычки. А зачем усложнять себе жизнь, когда без этого легко можно обойтись.
  • Ограничивайте длину названия 8 символами и не используйте в названии цифр и спец.символов - в отличии от аналогичной рекомендации в отношении наименования файла база данных этому есть причина. Причина не настолько явная, чтобы ее описать в двух словах. Но цепочка рассуждений приведшая к этой рекомендации основана на рекомендации по наименованию ключевых полей таблиц и наименований индексных тэгов. Прочтя эти разделы Вам станет понятна причина этой рекомендации.
  • Не называйте таблицу также как и одно из его полей - разумеется ошибки это не вызовет, но усложнит понимание написанного кода самим программистом. Не всегда с ходу можно однозначно определить, что речь идет именно о таблице, а не о поле таблицы. А если еще и их названия совпадают, то совсем тяжело становится.
  • Не используйте для названия одно из зарезервированных в FoxPro слов - опять же, ошибки это не вызовет, но резко снизит "читабельность" кода. Ведь зарезервированные слова автоматически подсвечиваются опеределенным цветом (если Вы используете стандартный текстовый редактор FoxPro) и с ходу становится проблематично отличить опцию или команду от имени таблицы
  • Не используйте псевдонимы таблицы внутри базы данных - в данном случае речь идет о том, что внутри базы данных можно присвоить таблице псевдоним, отличный от имени файла DBF. Речь не идет об опции ALIAS в команде USE. Это несколько другое. Если Вы войдете в режим модификации структуры таблицы, и перейдете на закладку "Table", то увидите, что в опции "Name" стоит имя, совпадающее с именем файла DBF. Вот это-то имя и можно изменить, присвоив таблице некоторый "псевдоним" по которому и будут обращаться к указанной таблице. Лично я не рекомендовал бы новичкам использование подобных псевдонимов, поскольку это может внести путаницу в сам процесс программирования.

    Расположение таблицы

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

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

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

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

    Собственно работа с таблицами

  • У объекта Grid при указании значения для RecordSourceType под термином "таблица" (Table) подразумевают как раз-таки именно файл DBF, а под термином "алиас" (Alias) - образ файла. Что вызывает огромное количество проблем у новичков с этим объектом. Ни в коем случае не используйте в качестве RecordSourceType указание "Table" - это приведет к непредсказуемому поведению данного объекта. Оставьте значение по-умолчанию "Alias"

    [li]Как уже было замечено выше, таблицы всегда открываются в конкретной рабочей области и для перехода в нужную рабочую область есть 2 способа адресации: либо по номеру этой рабочей области, либо по имени алиаса (alias) таблицы открытой в этой рабочей области.

    Так вот, я бы рекомендовал всегда адресоваться к рабочей области именно по имени алиаса (alias) таблицы, а не по номеру.

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

    Рабочая область с номером 0

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

    Такая адресация позволяет делать несколько вещей

    Во-первых, это позволяет открывать новые таблицы не заботясь о том, не занята ли текущая рабочая область другой таблицей. Т.е. дается команда вида
  
  USE MyTable IN 0  
 


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

Другое применение нулевой рабочей области - это настройки по умолчанию.

Например, Вы вероятно уже знаете, что любое Viewпо-умолчанию открывается в режиме оптимистической буферизации строк (3), но если Вам необходимо использовать его в режиме оптимистической буферизации таблиц (5), то необходимо после его открытия сделать это переключение используя функцию CursorSetProp()примерно так
  
  USE MyView IN 0  
  =CursorSetProp('buffering',5,'MyView')   
 


Однако используя нулевую рабочую область можно сделать глобальную настройку для открытия всех талиц в 5 режиме буферизации примерно так:
  
  =CursorSetProp('buffering',5,0)  
  USE MyView IN 0   
 


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

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

Большая таблица

Очень часто в конференциях проскакивает словосочетание "большая таблица".

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

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

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

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

Курсор


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

Курсор- это образ файла DBF открытого в одной из рабочих областей

Курсор- это временная таблица являющаяся результатом выполнения команды Select-SQL

Курсор- это указательположения индикатора ввода текста с клавиатуры

Ну, последнее определение не очень-то интересно. В том смысле, что здесь все ясно, кроме того, почему этот термин был использован еще и для временных таблиц, ведь слово "cursor"собственно и переводится как "указатель".

Курсор как образ файл DBF

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

Замечу еще, что курсор, как объект, используется не только как образ файлов DBF, но и как образ View. А View и таблица - это не одно и то же.

Курсор как временная таблица

Вот это наиболее употребительное использование данного термина. Собственно есть 2 способа создания таких курсоров

Первый способ- это использование команды CREATE CURSOR. Созданный таким способом курсор будет редактируемым. И это будет именно временная таблица, т.е. она будет автоматически уничтожена в момент закрытия. Ну про этот способ сказать особо нечего. Здесь нет каких-то проблем и особенностей

Второй способ- это использование опции CURSORв команде SELECT-SQL. Примерно в следующем синтаксисе
  
  SELECT * FROM MyTable INTO CURSOR TmpTable  
 


Вот этот-то TmpTable и есть курсор

В зависимости от различных условий этот курсор может иметь разное физическое "воплощение" и разные свойства

Если SQL-запрос полностью оптимизируем, то вместо создания нового файла будет просто открыта та же самая таблица с наложенным на нее фильтром. Зачастую это очень неприятная неожиданность. Проверить, чем же физически является сформированный курсор, можно используя функцию DBF()
  
  SELECT * FROM MyTable INTO CURSOR TmpTable  
  ?DBF('TmpTable')   
 


Если будет возвращено имя файла с расширением DBF, то данный курсор является той же самой исходной таблицей с наложенным на нее фильтром.

Если Вы хотите при любых запросах быть уверенными, что курсор - это именно временная таблица, а не исходная таблица с наложенным фильтром, то Вам следует добавить опцию NOFILTER
  
  SELECT * FROM MyTable INTO CURSOR TmpTable NOFILTER  
 


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

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

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

Еще один немаловажный вопрос связан с тем, что полученные таким образом курсоры нельзя редактировать. Они доступны только на чтение. Начиная с 7 версии Visual FoxPro для решения этой проблемы появилась специальная опция ReadWrite
  
  SELECT * FROM MyTable INTO CURSOR TmpTable NOFILTER READWRITE  
 


Использование этой опции позволяет создавать курсор, который можно редактировать. Для более младших версий FoxPro для того, чтобы курсор можно было редактировать его следует переоткрыть
  
  SELECT * FROM MyTable INTO CURSOR TmpReadTable NOFILTER  
  USE (DBF('TmpReadTable')) IN 0 AGAIN ALIAS TmpWriteTable  
  USE IN TmpReadTable   
 


Переоткрытый таким образом курсор TmpWriteTableуже можно будет редактировать

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

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

Формирование имени курсора в команде Select-SQL

Это не такой простой вопрос, как может показаться. Проблема здесь в том, что имя курсора - это фактически алиас (alias) временной таблицы. Но в FoxPro в одном сеансе данных не может быть открыто 2 таблиц с одинаковыми алиасам.

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

Ну например, Вы открыли 2 формы использующих Default DataSessionи в обоих создали курсор с одинаковым именем. В этом случае, курсор созданный позднее затрет курсор созданный ранее. При этом настройка SET SAFETYне играет никакой роли. Курсор будет пересоздан молча. Без каких-либо дополнительных запросов.

Избежать подобных конфликтов можно несколькими способами
  • Открывать формы и отчеты только в Private DataSession
  • Самостоятельно следить за уникальностью имен курсоров
  • Использовать функцию для генерации уникальных имен файлов
    Последний вариант кажется наиболее предпочтительным. Однако тут следует быть осторожным. Дело в том, что в описании к FoxPro для генерации уникальных имен файлов предлагается использовать следующую функцию
  
  lcCursorName=SubStr(SYS(2015),3,10)  
 


Проблема в том, что функция SYS(2015)может содержать в возвращаемом значении как буквы, так и цифры. Это значит, что при использовании выделения строки по SubStr() Вы вполне можете получить первым символом цифру. А использование в качестве имени переменной цифры в синтаксисе FoxPro недопустимо и Вы неожиданно получите сообщение о синтаксической ошибке. Чтобы этого избежать следует либо принудительно подмешать букву
  
  lcCursorName='t'+SubStr(SYS(2015),3,10)  
 


Либо вообще не выделять строку
  
  lcCursorName=SYS(2015)  
 


Соответсвенно выполнение запроса станет выглядеть так:
  
  LOCAL lcCursorName  
  lcCursorName=SYS(2015)  
  SELECT * FROM MyTable INTO CURSOR &lcCursorName NOFILTER   
 


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

Поля таблицы


Поле таблицы - это столбец который Вы видите каждый раз открывая таблицу на просмотр.

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

Поле таблицы, включенной в {базу данных} может содержать до 128 символов, содержать русские символы, цифры и некоторые спец.символы. Однако для упрощения работы в FoxPro я бы порекомендовал следующие ограничения в наименовании файла таблицы
  • Не использовать в названии русские символы - причина этой рекомендации в том, что FoxPro разрабатывался прежде всего для англоязычных пользователей и использование в нем символов другого языка - это уже последующее дополнение. Как следствие, велик риск, что чего-то, где-то недосмотрели и при определенных ситуациях русские буквы в имени вызовут неожиданные глюки.
  • Не использовать в названии цифры и спец.символы - в принципе, ошибок использование цифр и спец.символов не вызовет. Причина этой рекомендации в повышении сложности восприятия названия.

    Ну, например, если в таблице необходимо указать сумму платежа и сумму налога, то Вы конечно можете создать 2 поля Platezh1 и Platezh2. Но в этом случае, вам каждый раз придется вспоминать, что собственно обозначает цифра 1, а что цифра 2. Если Вы работаете постоянно только с одним проектом, то это не страшно, как-нибудь да запомните. Но если Вы отложили этот проект и вернулсь к нему через несколько месяцев, то напряженные размышления на тему - а что это собственно такое? - Вам обеспечены. Гораздо разумнее дать значимые имена: Platezh и Nalog
  • Если по данному полю Вы создаете простой индексный тэг, выражение которого состоит только из имени поля, то ограничивайте название поля 10 символами - причина здесь в том, что количество символов в названии индексного тэга не может быть больше 10. Это значит, что Вам придется указать в качестве названия индексного тэга что-то отличное от названия поля, по которому этот индекс построен. В принципе, ничего страшного. Однако если имя тэга и имя поля совпадают, то это сильно упрощает процесс программирования и позволяет в некоторых случаях создавать универсальный программный код не зависящий от использования конкретной таблицы.
  • Не называйте таблицу также как и одно из его полей - разумеется ошибки это не вызовет, но усложнит понимание написанного кода самим программистом. Не всегда с ходу можно однозначно определить, что речь идет именно о таблице, а не о поле таблицы. А если еще и их названия совпадают, то совсем тяжело становится.
  • Не используйте для названия одно из зарезервированных в FoxPro слов, в особенности те слова, которые используются в команде Select-SQL - это может вызвать сообщение о синтаксической ошибке, для подавления которой придется сильно усложнить программный код. Кроме того, это снизит "читабельность" кода. Ведь зарезервированные слова автоматически подсвечиваются опеределенным цветом (если Вы используете стандартный текстовый редактор FoxPro) и с ходу становится проблематично отличить опцию или команду от имени поля таблицы.

    Если воображение Вам напрочь отказывает и Вы не знаете как по другому назвать поле кроме как например "Order" или "Group", то добавьте в качестве первого символа букву, обозначающую тип данных, используемых в данном поле. Например, "nOrder" или "cGroup"
  • В некоторых книгах рекомендуется в качестве первого символа имени поля всегда указывать букву, обозначающую тип данных, использующийся в данном поле. Я бы не рекомендовал этого делать, по той причине, что FoxPro - это регистро-независимый язык. Т.е. ему все-равно большие или маленькие буквы используются в именах. Как следствие, все команды и функции FoxPro возвращающие какие-либо имена возвращают их либо в верхнем (большими буквами), либо в нижнем (маленькими буквами) регистре. При таком написании название воспринимается как одно целое слово без смыслового разбиения. Т.е. первая буква не воспринимается как тип данных, а воспринмается просто как начало слова.

    Собственно работа с полями таблицы

  • Крайне нежелательно динамически в процессе работы модифицировать поля таблицы или добавлять/удалять поля. Это сопряжено с большими проблемами и значительным усложнением программного кода.

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

    Дело в том, что если алиас не указан, то FoxPro предполагает, что речь идет о поле таблицы, расположенной в текущей рабочей области. А если у таблицы в текущей рабочей области нет такого поля, то о переменной памяти с тем же именем. Но при написании относительно сложной программы далеко не всегда можно с уверенностью сказать, что мы находимся в нужной рабочей области. Явное указание алиаса таблицы снимает эту проблему.
  • Заполняйте раздел "Comment" для всех полей таблицы в дезайнере таблиц. Написание комментариев, хотя бы минимальное, в любом случае очень полезная вещь. Этот текст автоматически отображается в окне самого проекта (Project), когда указатель встает на соответствующее поле таблицы. Да и документирование базы данных упрощается. Можно использовать этот текст (через функцию DBGetProp()) при выдаче сообщений об ошибках.


    Ключевое поле

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

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

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

    Начну сразу с вывода:
    Для большинства задач в FoxPro удобно использовать в качестве ключевого поля суррогатный ключ типа Integer. Ключевое поле желательно вводить для всех без исключения таблиц базы данных

    Ну а теперь рассмотрим как же я дошел до выводов таких

    Естесственные или Суррогатные ключи

    До сих пор не утихают споры о том, что лучше использовать: суррогатные или естесственные ключи.

    Естесственный ключ - это поле или набор полей, которые имеют некий физический смысл. Ну например, табельный номер, номер паспорта, и т.п.

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

    Рассмотрим какие преимущества и недостатки имеют естесственные и суррогатные ключи

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

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

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

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

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

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

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

    В одной из конференций, один из посетителей выразился по этому поводу достаточно эмоционально
  
  1. ИНН может быть одинаковым у разных организаций. Не верите? Я тоже не верил...   
  2. ИНН может быть разным у одной и той же организации.   
  3. У одного и того же человека может быть два разных номера паспорта.   
  4. У одного и того же человека могут быть разные фамилии.   
  5. У одной и той же организации могут быть разные наименования (в разное время года :) )   
  6. Количество детей, конечностей, зубов и даже папилярные линии... МОГУТ ИЗМЕНЯТЬСЯ!!!   
 


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

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

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

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

Но в отношении суррогатного ключа - такое его использование просто недопустимо! Ну не должен пользователь знать как там внутри программы все "тикает". Пользователю интересно показание часов, а не вские там "шестеренки". Если Вы хотите предоставить пользователю некую аббревиатуру (краткое название, "ник"), то это ни в коем случае не должен быть суррогатный ключ. Введите для этой цели еще одно дополнительное поле.
Есть и еще один, более хитрый аргумент, в пользу хотя бы частичного использования естесственных ключей. Дело в том, что очень часто возникает необходимость в некоторой внутренней классификации. Ну например, если речь идет о документах, то они могут находится в некоторых взаимоисключающих сотояниях:
  
  1 - Получено  
  2 - Отложено  
  3 - Утверждено  
 


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

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

Какой тип данных использовать: Character или Integer

Раньше, когда для хранения числовых данных в FoxPro существовали только поля типа Numericперевес в аргументации склонялся в пользу использования полей типа Character, но с появлением полей типа Integerвсе стало не так однозначно

При сравнении способа хранения ключевого поля в символьном или числовом формате выдвигаются 3 аргумента

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

    Числовые поля легче формировать

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

    Замечу еще, что не стоит для генерации нового значения ключа использовать именно определение максимального значения в текущей таблице. Это хорошо в однопользовательском режиме, но в многопользовательском Вы рискуете получить 2 одинаковых значения ключа при одновременном добавлении новой записи двумя пользователями одновременно. Обычно используют специальную служебную таблицу, хранящуюю значение последнего использованного (или первого не использованного) значения ключа. Примеры использования такой таблицы приведены в стандартных проектах примеров FoxPro: Solution.pjx (форма NewID.scx) и TasTrade.pjx. А в 8 версии FoxPro появились автоинкрементные поля, которые совсем упростили данную задачу.

    Символьные поля "экономичнее", т.е. можно уместить больше значений в том же размере

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

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

    Поле типа Integer может принимать знаячение в диапазоне от -2,147,483,647 до 2,147,483,647. Ну, отрицательные значения как правило не используются, но и положительные значения в 2 раза больше, чем максимально возможное количество записей. С учетом возможного удаления записей - в самый раз.

    Поскольку поле типа Integer физически занимает 4 байта (4 символа), то посмотрим, сколько же значений может быть присвоено при таком же размере для символьного поля

    В одном байте может быть записано 256 значений, т.е. это составит 256**4=4,294,967,296. Но поскольку некоторые значения нельзя использовать по ряду соображений (например, символ перевода строки, Esc и т.п.), то получается, что в смысле количества значений поля типа Integer ничуть не уступает полю типа Caracter(4), даже пожалуй несколько превосходит

    Замечание

    Следует заметить, что в FoxPro поля типа Numeric храняться как символьные поля, т.е. для хранения каждой цифры нужен один байт. Это значит, что если предполагаемое количество значений в данной таблице не превышает тысячи (меньше 4 символов), то возникает искушение "сэкономить" и вместо типа Integer ввести скажем поле типа N(2)

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

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

    Символьные поля "универсальнее" при так назваемых задачах репликации

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

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

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

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

    Надо ли использовать ключевое поле во всех без исключения таблицах

    На первый взгляд, вопрос может показаться странным. Разве можно без ключевого поля? Оказывается, в некоторых случаях можно.

    Например, для организации связи много-ко-многим стандартным способом является создание таблицы-посредника. Что имеется в виду?

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

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

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

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

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

    Название ключевого поля


    Поскольку ключевое поле - это обычное поле таблицы, то на него распространяются все рекомендации приведенные в разделе "Поля таблицы". Однако поскольку это все-таки очень специфическое поле, то для него я бы добавил следующие рекомендации
  • Образуйте название ключевого поля таблицы добавив к имени таблицы 2 буквы "ID" (от слова identifier - идентификатор), отбросив букву "s" если имя таблицы - это слово во множественном числе - Например, если вы назвали таблицу контрагентов "Partners", то ключевое поле будет назваться "PartnerID". При таком способе наименования однозначно можно сказать к какой таблице относится ключевое. Но не стоит назвать ключевое поле также как и собственно таблицу, поскольку в некоторых случаях станет весьма проблематично сходу определить о чем идет речь - о поле или собственно о таблице
  • Называйте внешние ключи также как и соответствующие ключевые поля - такой способ наименования внешних ключей очень облегчает понимание о чем собственно идет речь при написании программы.
  • Ограничивайте название ключевого поля 10 символами - причина здесь в том, что количество символов в названии индексного тэга не может быть больше 10. А по ключевому полю обязательно следует построить индекс. Если количество символов в ключевом поле будет больше 10, то название индексного тэга будет отличаться от названия ключевого поля. А это не очень хорошо в том смысле, что серьезно затруднит программирование, поскольку использовать этот индекс Вы будете сплошь и рядом.

    Кстати, из этой рекомендации вместе с самой первой рекомендацией по наименованию ключевых полей вытекает, что количество символов в имени таблицы не должно превышать 8 (или 9, если название таблицы - это множественное число)

    Собственно работа с ключевыми полями таблицы


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

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

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

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

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

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

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

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

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

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

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

Индекс
Типы (виды) индексов
Режим сортировки индексов (Collate)
Метод половинного деления
Использование индексов для отображения данных
Контроль уникальности данных при помощи индекса
Название индексных тегов
Сколько и каких индексов надо создавать
Собственно работа с индексами
Индекс
Важнейшим элементом любой системы управления базами данных является наличие средств ускоренного поиска данных, поскольку поиск - это самая распростарненная операция в системах обработки данных. Но простое сканирование исходной таблицы в поисках нужной записи - это относительно медленная операция. В предельном случае, необходимо будет просканировать все записи таблицы, чтобы найти нужную запись или убедится, что такой записи не существует. Чтобы уменьшить количество просматриваемых записей, как правило, создают специальный файл, который содержит в себе нечто вроде списка пар: значение записи - номер запись. Т.е. в этом файле, перечисленны все значения некоторого поля или функции полей таблицы и указаны идентификаторы соответсвующих записей Этот специальный файл называют индексным файлом, а имя поля или функцию полей, на основе которого вычисляется значение записи - индексным ключем

В FoxPro различают 2 вида индексных файлов

Простой (обычный) индексный файл - это файл с расширением IDX, который содержит в себе только один индексный ключ

Мультииндексный файл - это файл с расширением CDX, который может содержать в себе несколько индексных ключей. По существу, мультииндексный файл - это объединение в одном файле нескольких простых (обычных) индексных файлов. Каждый отдельный индекс в этом файле за неимением лучшего термина называют тегом (от английского TAG - этикетка, метка)

Кроме того, выделяют еще особый структурный индексный файл - это обычный мультииндексный файл, но его имя совпадает с именем файла DBF по полям которого и строятся индексные ключи

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

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

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


Индекс- это один тег структурного индексного файла

Типы (виды) индексов

В FoxPro в настоящий момент различают четыре типа (вида) индексов

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

Очень много недоразумений возникает из-за контроля уникальности данных в индексе типа Candidat. Типичные ошибки, заключаются в следующем:
-) Пустое значение (одни пробелы) - это тоже значение, поэтому невозможно создать 2 записи с пустыми значениями. Это будет воспринято как попытка ввода 2 одинаковых значений.
-) Установка ограничения SET DELETED ON - не есть физическое удаление записей помеченных как удаленные. Это всего-лишь наложение специфичекого фильтра, который делает такие записи "невидимыми", но тем не менее они по прежнему существуют в таблице. Поэтому попытка ввода новой записи со значениями, которые есть в одной из записей помеченных как удаленная также будет рассматриваться как ввод дублирующего значения.
Primary Можно сказать, что данный тип - это частный случай индекса типа Candidat. Он обладает всеми свойствами индекса типа Candidat, но с дополнительными ограничениями. Данный тип индекса может быть только один в каждой таблице и данный тип индекса может быть только у таблиц включенных в базу данных. Если таблица с индексом типа Primary исключается из базы данных, то индекс типа Primary автоматически конвертируется в тип Candidat.

Строго говоря, никакой дополнительной функциональности, с точки зрения целостности данных индекс типа Primary по сравнению с индексом типа Candidat не дает. Однако факт его наличия облегчает процесс проектирования базы данных. Дело в том, что по умолчанию предполагается, что индекс типа Primary построен по ключевому полю таблицы. Соответственно в тех визуальных средствах программирования, где необходимо указание ключевого поля (например, View Designer на закладке Update Criteria) FoxPro автоматически предлагает считать ключевым то поле, по которому построен индекс типа Primary.
Regular Это самый распространенный тип индексов. Его использование не предполагает никаких особых ограничений на содержимое таблицы. Можно сказать, что это обычный (простой) индекс
Unique Данный тип индекса не запрещает ввод в таблицу одинаковых значений, однако учитывает (отображает) только одно (самое первое) из введенных одинаковых значений. Можно сказать, что это своеобразный фильтр, который отсекает повторяющиеся значения.

Отдельного упоминания заслуживает использование команды APPEND BLANK применительно к таблицам, по которым построены индексы типа Primary или Candidat.

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

  • Вместо APPEND BLANK использовать INSERT-SQL указав заранее созданные уникальные значения
  • Использовать специальную функцию, генерирующую новые уникальные значения, вызов которой организовать из DEFAULT поля, по которому создан индекс типа Primary или Candidat.

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


Режим сортировки индексов (Collate)

Настройка SET COLLATE определяет режим сортировки символьных строк. Иными словами, какой символ надо поставить первым, а какой вторым, при упорядочивании.

По умолчанию, в FoxPro используется режим сортировки MACHINE. Это такой порядок сортировки, когда символы выстраиваются в соответствии с их ASCII-кодами. Чтобы понять что это означает (т.е. в каком порядке будут сортироваться символьные данные) посмотрите таблицу ASCII кодов. Это можно сделать так:

  
  CREATE CURSOR curASCII (nASC   I, cCHR   C(1))  
  FOR m.lnI=1 TO 255  
         INSERT INTO curASCII (nASC,cCHR) VALUES (m.lnI, CHR(m.lnI))  
  ENDFOR  
  BROWSE NOWAIT  
 

Обратите внимание, что сначала идут большие буквы и только потом маленькие. Кроме того, применительно к буквам русского алфавита, выпадает буква "ё" (ASCII-код 184) и "Ё" (ASCII-код 168). Именно в таком порядке и будут следовать символьные строки.

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

  
  SET COLLATE TO 'RUSSIAN'  
  select curASCII  
  INDEX ON cCHR TAG cCHR  
  BROWSE NOWAIT  
 

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

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

Таким образом внутри одного индексного файла могут быть индексы созданные в разных режимах сортировки. Проверить режим сортировки в котором был создан индекс можно при помощи функции IDXCOLLATE()

Однако я бы не советовал на первых порах экспериментировать с SET COLLATE. Оставьте эту настройку в режиме по умолчанию, т.е. SET COLLATE TO 'MACHINE'. Этому есть причины.

  • В версиях FoxPro младше 6 ServicePack 5 запросы Select-SQL теряли некоторые числовые значения если была установлена сортировка отличная от MACHIN. Т.е. эта ошибка сохранялась вплоть до Visual FoxPro 6 Service Pack 4 и была исправлена только с выходом Service Pack 5 к 6 версии.
  • При использовании Private DataSession установка SET COLLATE - это одна из тех установок, которая сбрасывается в значение по умолчанию. Вследствии чего, ее надо устанавливать повторно каждый раз при открытии Private DataSession
    Полный список настроек, сбрасываемых в значения по умолчанию при использовании Private DataSession можно почитать в описании к команде SET DataSession
  • При оптимизации используются только те индексы, которые созданы в том же режиме сортировки, что и текущий режим сортировки системы.
  • В подавляющем большинстве случаев, вместо использования режима сортировки SET COLLATE TO 'RUSSIAN' можно использовать режим сортировки MACHINE с индексным ключем UPPER(MyField) или LOWER(MyField). Применительно к русским буквам эффект будет тот же (за исключением букв "ё" и "Ё")

Метод половинного деления

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

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

Запись Значение
1 500
2 300
3 200
4 400
5 100


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

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

Значение Запись
100 5
200 3
300 2
400 4
500 1


А теперь посмотрим как поиск записи со значением 200 приблизительно будет выглядеть в индексе в случае использования метода половинного деления.

-) На первом этапе определяются начальные и конечные значения. В данном случае - это 100 и 500. Значение 200 находится между ними.

-) Вычисляется среднее арифметическое от граничных значений: (100+500)/2=300 и берется ближайшее значение из списка значений к вычисленному среднему. В данном случае это то же самое значение 300

-) Теперь смотрим в какой половине находится искомое значение 200. Оно находится в интервале от 100 до 300

-) Вычисляется среднее арифметическое этого нового интервала: (100+300)/2=200 и берется ближайшее значение из списка значений к вычисленному среднему. В данном случае это то же самое значение 200.

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

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

Предельно допустимое количество записей в DBF-таблице - это 1 миллирад (1 billion) записей (это единица и девять нулей). Значит при прямом сканировании таблицы в предельном случае может понадобиться просканировать весь миллирад записей. Т.е. сделать миллиард операций.

А в случае половинного деления количество операций определяется значением степени в которую следует возвести число 2, чтобы превысить миллирад (логарифм от миллиарда по основанию 2). Это число 30, поскольку 2**30=1,073,741,824. Т.е. в предельном случае для поиска значения в индексе потребуется не более 30 шагов

Выигрыш в производительности очевиден: миллирад против тридцати.

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


Использование индексов для отображения данных

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

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

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

Однако это вовсе не означает, что индексы для упорядочивания записей не используются. Дело в том, что любая таблица используемая в FoxPro может быть проиндексирована. Это относится как к обычным таблицам, так и к результатам выполнения команд Select-SQL, Local View и Remote View.

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

Некоторые особенности создания индексов для временных таблиц

  • Если курсор, созданный командой Select-SQL имеет статус Read-Only, то для него можно построить только один индексный тэг.
    По умолчанию, курсор созданный командой Select-SQL всегда имеет статус Read-Only, чтобы это изменить необходимо использовать опцию READWRITE (она появилась только в 7 версии). А для более младших версий курсор следует переоткрыть. Подробнее об этом читайте в разделе Курсор
  • Индексация View возможна только если он находится в 3 режиме буферизации (оптимистическая буферизация строк). Если View находится в 5 режиме буферизации (оптимистическая буферизация таблиц), то при попытке индексации Вы получите сообщение об ошибке.
  • Обновление содержимого View по команде Requery() также обновит и содержимое всех созданных для него индексов.
  • Если Вы создали структурный индексный файл к курсору или View, то он будет автоматически удален в момент закрытия этого курсора или View

Контроль уникальности данных при помощи индекса

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

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

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

Это можно сделать добавив в индекс FOR-условие, которое в данном случае будет выглядеть так: FOR .NOT.Deleted()

Но добавление FOR-условия в выражение индекса автоматически исключает его из списка индексов, участвующих в оптимизации. Т.е. для ускорения выборок понадобиться создать еще один индекс с тем же выражением индексного ключа, но без FOR-условия. Чтобы этот новый индекс не контролировал уникальность, он должен быть типа Regular

Как следствие, получаем "раздувание" индексного файла на "лишний" индекс.

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

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

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

Итого, контроль уникальности данных при помощи индекса типа Candidat с FOR-условием по сравнению с триггером проще в реализации, но приводит к значительно большему увеличению объема базы данных (в байтах) и изменению логики обработчика ошибок при редактировании данных.


Название индексных тегов

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

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


Сколько и каких индексов надо создавать

Вопрос крайне неоднозначный и сильно зависит от конкретной задачи, однако некоторые рекомендации все-таки можно сделать.

  • Прежде всего, Вам следует определиться в каком режиме сортировки (SET COLLATE) Вы будете работать. Дело в том, что индекс запоминает тот режим сортировки в котором он был создан и в оптимизации запросов участвуют только те индексы, которые были созданы в том же режиме сортировки, что и используемый на момент выполнения запроса.

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

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

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

Индексы используются для ускорения получения выборок в так назваемой Rushmore-оптимизации. Что это такое и как оно работает я здесь объяснять не будут. С точки зрения создания индексов нас интересует следующая особенность работы Rushmore-оптимизации:

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

Следствием этой особенности работы является следующий вывод


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

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

Объем информации НЕ удовлетворяющей условию < Объем индекса

Объем информации НЕ удовлетворяющей условию - это произведение количества записей не попадающих в выборку на объем в байтах одной записи.

Объем индекса - это сумма размеров индексных тэгов, участвующих в оптимизации.

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

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

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

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

Частным случаем индекса по логическому выражению является индекс по выражению Deleted(). Этот индекс используется в случае настройки SET DELETED ON для того, чтобы оптимизировать выборку на предмет отсечения записей помеченных как удаленные. Как уже было сказано чуть выше, если количество удаленных записей относительно невелико, то данный индекс приведе не к ускорению, а к замедлению выборки.

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

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

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

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

Замечу еще, что индексы содержащие FOR-условия в оптимизации не участвуют


Собственно работа с индексами
  • В FoxPro не поддерживается значение индексного ключа переменной длины. Т.е. если в качестве выражения индексного ключа Вы напишите AllTrim(MyField), то хотя ошибки это и не вызовет, но фактически будут удалены только ведущие пробелы, а общая длина каждого значения будет дополнена концевыми пробелами до некоторой фиксированной величины. В данном случае до длины поля MyField. Поэтому, чтобы избежать различных недоразумений, лучше в таких сложных случаях самостоятельно явно указывать длину индексного ключа, например PADR(AllTrim(MyField),50)
  • По возможности, старайтесь избегать сложных выражений индексного ключа для тегов структурного индексного файла. Чем проще выражение индексного ключа - тем лучше. В идеале, лучше иметь индексный ключ состоящий из названия одного поля.
  • Индекс "запоминает" режим сортировки (SET COLLATE) при котором он был создан и при модификации исходных таблиц автоматически сортирует новые значения в "запомненном" режиме сортировки вне зависимости от текущей настройки SET COLLATE. Проверить в каком режиме сортировки был создан тот или другой тег можно функцией IDXCOLLATE()
  • На результат поиска по индексному выражению влияет настройка SET EXACT, а внутри команды SELECT-SQL аналогичная ей настройка SET ANSI
    Эти настройки определяют как будет осуществлятся поиск в случае если заданный критерий поиска отличается по длине от длины индексного ключа. По умолчанию, если заданный критерий поиска меньше длины индексного ключа, то будут найдены записи, где заданный критерий - это первые символы индексного ключа. Фактически поиск ведется по первым символам, а не по всему ключу.
    Если необходимо избежать влияния этих настроек на результат поиска (т.е. требуется строгое соответствие), то дополняйте критерий поиска нужным количеством пробелов справа используя функцию PADR(KeyValue,10)
  • Следует помнить, что изменения данных в исходной таблице приводит к немедленному изменению всех открытых в данный момент индексов и перестроению в соответствии с главным индексом. Это значит, что крайне нежелательно модифицировать те поля, которые входят в выражение индексного ключа главного индекса, поскольку это может привести к непредсказуемым последствиям. Предварительно надо или переглючится на другой индекс или вообще отключить главный индекс (SET ORDER TO 0)

    Рассмотрим следующий пример

      
      CREATE CURSOR test (testID I)  
      INSERT INTO test (testID) VALUES (2)  
      INSERT INTO test (testID) VALUES (1)  
      INDEX ON testID TAG testID  
     
    

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

      
      SELECT test  
      SCAN  
            REPLACE testID WITH testID+10  
      ENDSCAN  
     
    

    Так вот, в данном случае произойдет модификация только одной записи. Механизм здесь следующий:

    -) На первом шаге цикла SCAN указатель записей будет установлен на первую запись в соответсвии с главным индексом. Это запись со значением поля testID=1

    -) По команде REPLACE значение поля TestID первой записи будет увеличено на 10 и примет значение 11. Немедленно произойдет модификация индексного файла и его перестроение. Теперь эта же самая запись оказывается не первой, а самой последней

    -) На команде ENDSCAN будет предпринята попытка перейти на следующую запись, но в соответствии с новым значением индекса текущая запись уже последняя. Поэтому цикл будет завершен и запись со значением testID=2 так и останется не измененной.

    Для исправления этой ситуации необходимо перед началом цикла сбросить главный индекс

      
      SELECT test  
      SET ORDER TO 0  
      SCAN  
            REPLACE testID WITH testID+10  
      ENDSCAN  
     
    
  • Индексный файл - это все-таки отдельный (другой) файл. Поэтому если индексный файл не открыт (командой SET INDEX или опцией ORDER в команде USE), то изменения сделанные в исходной таблице не отразятся в индексе. Это может привести к серьезным недоразумениям - запись введена, но ее не видно.

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

  • Курсор, созданный командами CREATE CURSOR или SELECT ... INTO CURSOR можно индексировать. Однако если курсор созданный командой SELECT ... INTO CURSOR имеет статус Read-Only (т.е. не была указана опция ReadWrite или он не был переоткрыт), то для такого курсора можно будет создать только один индексный тег. Если Вы создадите структурный индексный файл для таких курсоров, то этот файл будет автоматически удален в момент закрытия курсора
  • Local View и Remote View также можно индексировать если они находятся в режиме оптимистической буферизации строк (3). В режиме оптимистической буферизации таблиц (5) индексирование невозможно. Обновление содержимого View по команде Requery() также обновит и структурный индексный файл созданный для данного View. Закрытие View автоматически уничтожит и структурный индексный файл.

Опубликовал: Владимир Максимов 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

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


Страница 1 из 212 »