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

Утилиты

Цель данного раздела — это не ознакомление со всеми существующими в 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). Но данная команда оставлена только для совместимости с более ранними версиями и ее использование сопряжено с большим количеством ограничений. Так что, лучше ее не использовать.