FoxPro Club Главная

Конференция Решения Фотоальбом Сайт М.Дроздова Статьи Файловый архив Книга Visual FoxPro 9.0 Русский Help для Visual FoxPro
Пользователей: 9146
Вход

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

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

Типы данных
  • Точность расчета
  • Numeric
  • Currency
  • Memo
  • General
  • Binary
  • AutoIncrement

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

    Хранимые процедуры
  • Для чего нужны хранимые процедуры
  • Особенности работы с хранимыми процедурами

    Связи и отношения между таблицами
  • Постоянная связь (persistent relationship)
  • Связь (обычная)
  • Когда следует использовать связь

    Типы данных

    Цель данного раздела - это не ознакомление со всеми существующими в 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=$268435456.4  
      ?nNum=yCur  
      ?nNum=MTON(yCur)  
      ?NTOM(nNum)=yCur  
      ?nNum=ROUND(MTON(yCur),4)  
      

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

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

      
      ?4/3*3  
      ?$4/3*3  
      

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

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

    Т.е. в памяти, результат деления 4/3=1.333333333333333 - до 16 значащих цифр. А результат деления $4/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.

    Последние обновление: 2004-04-16 16:46
    Опубликовал: Владимир Максимов


  • Вернуться к списку статей






    © 2000-2017 Fox Club 
    При размещении любых материалов с сайта на других ресурсах- прямая ссылка на www.foxclub.ru обязательна
    Яндекс.Метрика