Как известно СУБД Firebird не имеет встроенной поддержки JSON. Однако если что-то сильно нужно, то всегда можно найти решение. Обычно предлагается собирать и собирать JSON на стороне клиентского приложения. Здесь я расскажу как это можно сделать на стороне сервера.
Попытка решить это исключительно в рамках PSQL может вызвать много проблем.
При сборке больших JSON на стороне сервера их приходится помещать в BLOB.
Как известно конкатенация строк с BLOB приводит к созданию множества временных BLOB. Когда размер таких BLOB не превышает одну страницу, то они будут находиться в оперативной памяти, в противном случае страницы BLOB приходится сбрасывать в файл БД, что приводит к "распуханию" вашей БД.
Для того чтобы уменьшить негативное влияние конкатенации BLOB можно накапливать результаты сборки в промежуточный буфер VARCHAR(8191) CHARACTER SET UTF8
.
А затем присоединять такой буфер к переменной типа BLOB. Причём для конкатенации строк к BLOB желательно использовать агрегатную функцию LIST, если это возможно.
В HQBird 3.0 была добавлена встроенная функция BLOB_APPEND
, которая решает проблему конкатенации BLOB, однако в Firebird такая функция пока отсутствует.
В Firebird 5.0 (а также в HQBird 3.0) ввели специальный системный пакет RDB$BLOB_UTILS
, который так же позволяет бороться с проблемой конкатенации BLOB. В настоящий момент Firebird 5.0 на начальной стадии разработки, и пока не может быть использован в промышленных системах.
Вторая проблема которую вам необходимо решить — экранирование значений перед тем как использовать их внутри JSON. В принципе это можно успешно решить в рамках PSQL, однако учтите, что скорее всего подобные функции для символьных типов данных будут содержать множество вызов REPLACE, что негативно повлияет на производительность.
-- экранирование строк
function Escape_String(AString varchar(8191) character set utf8)
returns varchar(8191) character set utf8
as
begin
AString = REPLACE(AString, '\', '\\');
AString = REPLACE(AString, ASCII_CHAR(0x08), '\b');
AString = REPLACE(AString, ASCII_CHAR(0x09), '\t');
AString = REPLACE(AString, ASCII_CHAR(0x0A), '\r');
AString = REPLACE(AString, ASCII_CHAR(0x0C), '\f');
AString = REPLACE(AString, ASCII_CHAR(0x0D), '\n');
AString = REPLACE(AString, '"', '\"');
AString = REPLACE(AString, '/', '\/');
RETURN AString;
end
Разбор JSON на стороне PSQL ещё более сложная задача, и хотя она вполне решаема, производительность таких решений будет желать лучшего.
Можно попробовать пойти другим путём и написать UDR на внешнем языке программирования. И тут тоже есть два варианта:
- Делать сборку/разбор JSON для одного заранее известного формата
- Написать универсальную библиотеку для сборки/разбора JSON любого формата
Первый вариант будет наиболее производителен, но при изменении формата или необходимости разобрать другой JSON вам скорее всего придётся переписывать вашу UDR.
Второй вариант написать намного сложнее. Далее я расскажу об одной из таких UDR библиотек под названием udr-lkJSON. Библиотека с полностью открытыми исходными кодами под лицензией MIT и свободна для использования. Она написана на языке Free Pascal. Её автор Максим Филатов, является сотрудником Московской Биржи.
Установить UDR lkJSON можно начиная с Firebird 3.0 и выше (Firebird 2.5 не поддерживал UDR). Вы можете собрать библиотеку скачав исходные код по ссылке выше, либо скачать готовую библиотеку под нужную вам платформу по адресу https://github.com/mnf71/udr-lkJSON/tree/main/lib.
После скачивания или сборки готовую библиотеку необходимо разместить в каталог
- в Windows —
Firebird30\plugins\udr
, где Firebird30 — корневой каталог установки Firebird - в Linux —
/firebird/plugins/udr
Далее библиотеку необходимо зарегистрировать в вашей базе данных. Для этого необходимо выполнить следующий скрипт udrJSON.sql.
Замечание.
Библиотека разрабатывалась с учётом того, что она будет работать с однобайтовой кодировкой, такой как WIN1251.
Если ваша база создана в кодировке UTF8, то необходимо модифицировать скрипт регистрации заменив в нём VARCHAR(32)
на VARCHAR(8)
,
VARCHAR(128)
— VARCHAR(32)
, VARCHAR(32765)
— VARCHAR(32765) CHARACTER SET NONE
.
В последнем случае нельзя заменить на VARCHAR(8191)
, поскольку 8191 * 4 = 32764, что не соответствует внутренней структуре, в которой отведено 32765 байт.
Установочный скрипт для базы данных созданной в кодировке UTF8 и исправленной ошибкой доступен по ссылке udrJSON-utf8.sql
isql "inet4://localhost/test" -user SYSDBA -password masterkey -i udrJSON.sql
После установки UDR её предлагается проверить с помощью скрипта verify.sql
В скрипте происходит вызов функции для разбора JSON и его сборка обратно в строку. Если исходный JSON будет такой же, как вновь собранный, то всё в порядке. В реальности полностью совпадать строки не будут, так как сборка JSON происходит без учёта красивого форматирования. Но содержимое должно быть идентичным.
Проверка происходит для двух наборов (процедура + функция)
js$func.ParseText
— разбор JSON заданного в виде BLOB.js$func.GenerateText
— сборка JSON с возвратом BLOB.js$func.ParseString
— разбор JSON заданного в виде VARCHAR(N).js$func.GenerateString
— сборка JSON с возвратом VARCHAR(N).
Библиотека udr-lkJSON основана на свободной библиотеки lkJSON для генерирования и разбора JSON. Поэтому чтобы хорошо представлять себе как работать с UDR-lkJSON желательно ознакомится с библиотекой lkjson.
При разборе JSON часть элементов могут быть простыми типами, которые существуют в Firebird (INTEGER, DOUBLE PRECISION, VARCHAR(N), BOOLEAN), а часть сложными — объекты и массивы.
Сложные объекты возвращаются как указатель на внутренний объект из библиотеки lkJSON. Указатель отображается в домен TY$POINTER
. Этот домен определён следующим образом:
CREATE DOMAIN TY$POINTER AS
CHAR(8) CHARACTER SET OCTETS;
Кроме того, если в JSON встречается NULL, то он не будет возвращён в простые типы! Вам придётся распознавать это значение отдельно. Это связано с тем, что библиотека UDR-lkJSON просто копирует методы классов библиотеки lkJSON в PSQL пакеты. А как известно простые типы в Pascal не имеют отдельного состояния для NULL.
Заголовок этого пакета выглядит следующим образом:
CREATE OR ALTER PACKAGE JS$BASE
AS
BEGIN
/* TlkJSONbase = class
TlkJSONtypes =
(jsBase, jsNumber, jsString, jsBoolean, jsNull, jsList, jsObject);
0 1 2 3 4 5 6
*/
FUNCTION Dispose(Self TY$POINTER) RETURNS SMALLINT; /* 0 - succes */
FUNCTION Field(Self TY$POINTER, Name VARCHAR(128) CHARACTER SET NONE /* = Idx */) RETURNS TY$POINTER;
FUNCTION Count_(Self TY$POINTER) RETURNS INTEGER;
FUNCTION Child(Self TY$POINTER, Idx INTEGER, Obj TY$POINTER = NULL /* Get */) RETURNS TY$POINTER;
FUNCTION Value_(Self TY$POINTER, Val VARCHAR(32765) CHARACTER SET NONE = NULL /* Get */) RETURNS VARCHAR(32765) CHARACTER SET NONE;
FUNCTION WideValue_(Self TY$POINTER, WVal BLOB SUB_TYPE TEXT = NULL /* Get */) RETURNS BLOB SUB_TYPE TEXT;
FUNCTION SelfType(Self TY$POINTER = NULL /* NULL - class function */) RETURNS SMALLINT;
FUNCTION SelfTypeName(Self TY$POINTER = NULL /* NULL - class function */) RETURNS VARCHAR(32) CHARACTER SET NONE;
END
Как видно из комментария этот пакет является калькой с класса TlkJSONbase
. Он содержит базовые функции для работы с JSON.
Функция Dispose
предназначена для освобождения указателя на JSON объект. Указатели, которые надо принудительно освобождать, появляются в результате парсинга или создания JSON.
Не следует вызывать его для промежуточных объектов при разборе или сборке JSON. Он требуется только для объекта верхнего уровня.
Функция Field
возвращает указатель на поле объекта. Первым параметром задаётся указатель на объект, вторым — имя поля. Если поля не существует, то функция вернёт пустой указатель (Это не NULL, а x'0000000000000000'
).
Функция Count_
возвращает количество элементов в списке или полей в объекте. В качестве параметра задаётся указатель на объект или список.
Функция Child
возвращает или устанавливает значение для элемента с индексом Idx в объекте или списке Self. Если параметр Obj не задан, то возвращает указатель на элемент с индексов Idx.
Если Obj указан, то устанавливает его значение в элемент с индексов Idx. Обратите внимание Obj это указатель на один из потомков TlkJSONbase
.
Функция Value_
возвращает или устанавливает в виде JSON строки (VARCHAR
) значение для объекта заданного в параметре Self. Если параметр Val не задан, то значение возвращается, в противном случае устанавливается.
Функция WideValue_
возвращает или устанавливает в виде JSON строки (BLOB SUB_TYPE TEXT
) значение для объекта заданного в параметре Self. Если параметр Val не задан, то значение возвращается, в противном случае устанавливается.
Функция SelfType
возвращает тип объекта для указателя заданного в параметре Self. Тип объекта возвращается как число, где
- 0 — jsBase
- 1 — jsNumber
- 2 — jsString
- 3 — jsBoolean
- 4 — jsNull
- 5 — jsList
- 6 — jsObject
Если параметр Self не задан, то вернёт 0.
Функция SelfTypeName
возвращает тип объекта для указателя заданного в параметре Self. Тип объекта возвращается как строка. Если параметр Self не задан, то вернёт 'jsBase'
.
Заголовок этого пакета выглядит следующим образом:
CREATE OR ALTER PACKAGE JS$BOOL
AS
BEGIN
/* TlkJSONbase = class
TlkJSONboolean = class(TlkJSONbase)
*/
FUNCTION Value_(Self TY$POINTER, Bool BOOLEAN = NULL /* Get */) RETURNS BOOLEAN;
FUNCTION Generate(Self TY$POINTER = NULL /* NULL - class function */, Bool BOOLEAN = TRUE) RETURNS TY$POINTER;
FUNCTION SelfType(Self TY$POINTER = NULL /* NULL - class function */) RETURNS SMALLINT;
FUNCTION SelfTypeName(Self TY$POINTER = NULL /* NULL - class function */) RETURNS VARCHAR(32) CHARACTER SET NONE;
END
Как видно из комментария этот пакет является калькой с класса TlkJSONboolean
. Он предназначен для работы с типом BOOLEAN
.
Функция Value_
возвращает или устанавливает в значение логического типа для объекта заданного в параметре Self. Если параметр Bool не задан, то значение будет возвращено, если задан — установлено.
Обратите внимание, NULL не возвращается и не может быть установлено этим методом, для этого существует отдельный пакет JS$NULL
.
Функция Generate
возвращает указатель на новый объект TlkJSONboolean
, который представляет собой значение логического типа в JSON.
Параметр Self — указатель на JSON объект на основе которого создаётся объект TlkJSONboolean
. Значение логического типа указывается в параметре Bool.
Функция SelfType
возвращает тип объекта для указателя заданного в параметре Self. Тип объекта возвращается как число. Если параметр Self не задан, то вернёт 3.
Функция SelfTypeName
возвращает тип объекта для указателя заданного в параметре Self. Тип объекта возвращается как строка. Если параметр Self не задан, то вернёт 'jsBoolean'
.
Заголовок этого пакета выглядит следующим образом:
CREATE OR ALTER PACKAGE JS$CUSTLIST
AS
BEGIN
/* TlkJSONbase = class
TlkJSONcustomlist = class(TlkJSONbase)
*/
PROCEDURE ForEach
(Self TY$POINTER) RETURNS (Idx Integer, Name VARCHAR(128) CHARACTER SET NONE, Obj TY$POINTER /* js$Base */);
FUNCTION Field(Self TY$POINTER, Name VARCHAR(128) CHARACTER SET NONE /* = Idx */) RETURNS TY$POINTER;
FUNCTION Count_(Self TY$POINTER) RETURNS INTEGER;
FUNCTION Child(Self TY$POINTER, Idx INTEGER, Obj TY$POINTER = NULL /* Get */) RETURNS TY$POINTER;
FUNCTION GetBoolean(Self TY$POINTER, Idx INTEGER) RETURNS BOOLEAN;
FUNCTION GetDouble(Self TY$POINTER, Idx INTEGER) RETURNS DOUBLE PRECISION;
FUNCTION GetInteger(Self TY$POINTER, Idx INTEGER) RETURNS INTEGER;
FUNCTION GetString(Self TY$POINTER, Idx INTEGER) RETURNS VARCHAR(32765) CHARACTER SET NONE;
FUNCTION GetWideString(Self TY$POINTER, Idx INTEGER) RETURNS BLOB SUB_TYPE TEXT;
END
Как видно из комментария этот пакет является калькой с класса TlkJSONcustomlist
. Этот тип является базовым при работе с объектами и списками.
Все процедуры и функции этого пакета можно использовать как JSON типа объект, так и JSON типа список.
Процедура ForEach
извлекает каждый элемент списка или каждое поле объекта из указателя на JSON заданного в Self.
Возвращаются следующие значения:
- Idx — индекс элемента списка или номер поля в объекте. Начинается с 0.
- Name — имя очередного поля, если Self — объект. Или индекс элемента списка, начиная с 0, если Self — список.
- Obj — указатель на очередной элемент списка или поля объекта.
Функция Field
возвращает указатель на поле по его имени из объекта заданного в Self.
Вместо имени поля можно задать номер элемента в списке или номер поля. Нумерация начинается с 0.
Функция Count_
возвращает количество элементов в списке или полей в объекте, заданного в параметре Self.
Функция Child
возвращает или устанавливает значение для элемента с индексом Idx в объекте или списке Self. Индексация начинается с 0. Если параметр Obj не задан, то возвращает указатель на элемент с индексов Idx.
Если Obj указан, то устанавливает его значение в элемент с индексов Idx. Обратите внимание Obj это указатель на один из потомков TlkJSONbase
.
Функция GetBoolean
возвращает логическое значение поля объекта или элемента массива с индексом Idx. Индексация начинается с 0.
Функция GetDouble
возвращает значение с плавающей точкой поля объекта или элемента массива с индексом Idx. Индексация начинается с 0.
Функция GetInteger
возвращает целочисленное значение поля объекта или элемента массива с индексом Idx. Индексация начинается с 0.
Функция GetString
возвращает символьное значение (VARCHAR
) поля объекта или элемента массива с индексом Idx. Индексация начинается с 0.
Функция GetWideString
возвращает значение типа BLOB SUN_TYPE TEXT
поля объекта или элемента массива с индексом Idx. Индексация начинается с 0.
Обратите внимание! Функции GetBoolean
, GetDouble
, GetInteger
, GetString
, GetWideString
не могу вернуть значение NULL.
Для обработки значения NULL существует отдельный набор функций в пакете JS$NULL
.
Заголовок этого пакета выглядит следующим образом:
CREATE OR ALTER PACKAGE JS$FUNC
AS
BEGIN
FUNCTION ParseText(Text BLOB SUB_TYPE TEXT, Conv BOOLEAN = FALSE) RETURNS TY$POINTER;
FUNCTION ParseString(String VARCHAR(32765) CHARACTER SET NONE, Conv BOOLEAN = FALSE) RETURNS TY$POINTER;
FUNCTION GenerateText(Obj TY$POINTER, Conv BOOLEAN = FALSE) RETURNS BLOB SUB_TYPE TEXT;
FUNCTION GenerateString(Obj TY$POINTER, Conv BOOLEAN = FALSE) RETURNS VARCHAR(32765) CHARACTER SET NONE;
FUNCTION ReadableText(Obj TY$POINTER, Level INTEGER = 0, Conv BOOLEAN = FALSE)
RETURNS BLOB SUB_TYPE TEXT;
END
Этот пакет содержит набор функций для разбора JSON или преобразование JSON в строку.
Функция ParseText
разбирает JSON заданный в виде строки типа BLOB SUB_TYPE TEXT
в параметре Text. Если в параметре Conv
передать значение TRUE, то текст JSON строки будет преобразован из кодировки UTF8 в обычную.
Функция ParseString
разбирает JSON заданный в виде строки типа VARCHAR(N)
в параметре String. Если в параметре Conv
передать значение TRUE, то текст JSON строки будет преобразован из кодировки UTF8 в обычную.
Функция GenerateText
возвращает JSON в виде строки типа BLOB SUB_TYPE TEXT
. Если в параметре Conv передать значение TRUE,
то текст возвращаемой этой функцией будет преобразован в UTF8.
Функция GenerateString
возвращает JSON в виде строки типа VARCHAR(N)
. Если в параметре Conv передать значение TRUE,
то текст возвращаемой этой функцией будет преобразован в UTF8.
Функция ReadableText
возвращает JSON в виде человекочитаемой строки типа BLOB SUB_TYPE TEXT
.
Параметр Level - задаёт количество отступов для первого уровня. Это требуется если генерируемая строка является частью другого JSON.
Если в параметре Conv передать значение TRUE, то текст возвращаемой этой функцией будет преобразован в UTF8.
Использование параметра Conv со значением TRUE оставлено для совместимости с исходной библиотекой lkJSON. Особой необходимости в нем нет, так как внешние сервисы самостоятельно умеют конвертировать исходную строку в нужный для СУБД формат и обратно.
Заголовок этого пакета выглядит следующим образом:
CREATE OR ALTER PACKAGE JS$LIST
AS
BEGIN
/* TlkJSONbase = class
TlkJSONcustomlist = class(TlkJSONbase)
TlkJSONlist = class(TlkJSONcustomlist)
*/
PROCEDURE ForEach
(Self TY$POINTER) RETURNS (Idx Integer, Name VARCHAR(128) CHARACTER SET NONE, Obj TY$POINTER /* js$Base */);
FUNCTION Add_(Self TY$POINTER, Obj TY$POINTER) RETURNS INTEGER;
FUNCTION AddBoolean(Self TY$POINTER, Bool BOOLEAN) RETURNS INTEGER;
FUNCTION AddDouble(Self TY$POINTER, Dbl DOUBLE PRECISION) RETURNS INTEGER;
FUNCTION AddInteger(Self TY$POINTER, Int_ INTEGER) RETURNS INTEGER;
FUNCTION AddString(Self TY$POINTER, Str VARCHAR(32765) CHARACTER SET NONE) RETURNS INTEGER;
FUNCTION AddWideString(Self TY$POINTER, WStr BLOB SUB_TYPE TEXT) RETURNS INTEGER;
FUNCTION Delete_(Self TY$POINTER, Idx Integer) RETURNS SMALLINT;
FUNCTION IndexOfObject(Self TY$POINTER, Obj TY$POINTER) RETURNS INTEGER;
FUNCTION Field(Self TY$POINTER, Name VARCHAR(128) CHARACTER SET NONE /* = Idx */) RETURNS TY$POINTER;
FUNCTION Count_(Self TY$POINTER) RETURNS INTEGER;
FUNCTION Child(Self TY$POINTER, Idx INTEGER, Obj TY$POINTER = NULL /* Get */) RETURNS TY$POINTER;
FUNCTION Generate(Self TY$POINTER = NULL /* NULL - class function */) RETURNS TY$POINTER;
FUNCTION SelfType(Self TY$POINTER = NULL /* NULL - class function */) RETURNS SMALLINT;
FUNCTION SelfTypeName(Self TY$POINTER = NULL /* NULL - class function */) RETURNS VARCHAR(32) CHARACTER SET NONE;
END
Как видно из комментария этот пакет является калькой с класса TlkJSONlist
. Он предназначен для работы со списком.
Процедура ForEach
извлекает каждый элемент списка или каждое поле объекта из указателя на JSON заданного в Self.
Возвращаются следующие значения:
- Idx — индекс элемента списка или номер поля в объекте. Начинается с 0.
- Name — имя очередного поля, если Self — объект. Или индекс элемента списка, начиная с 0, если Self — список.
- Obj — указатель на очередной элемент списка или поля объекта.
Функция Add_
добавляет новый элемент в конец списка, указатель на который указан в параметре Self.
Добавляемый элемент указывается в параметре Obj, который должен быть указателем на один из потомков TlkJSONbase
.
Функция возвращает индекс вновь добавленного элемента.
Функция AddBoolean
добавляет новый элемент логического типа в конец списка, указатель на который указан в параметре Self.
Функция возвращает индекс вновь добавленного элемента.
Функция AddDouble
добавляет новый элемент вещественного типа в конец списка, указатель на который указан в параметре Self.
Функция возвращает индекс вновь добавленного элемента.
Функция AddInteger
добавляет новый элемент целочисленного типа в конец списка, указатель на который указан в параметре Self.
Функция возвращает индекс вновь добавленного элемента.
Функция AddString
добавляет новый элемент строкового типа (VARCHAR(N)
) в конец списка, указатель на который указан в параметре Self.
Функция возвращает индекс вновь добавленного элемента.
Функция AddWideString
добавляет новый элемент типа BLOB SUB_TYPE TEXT
в конец списка, указатель на который указан в параметре Self.
Функция возвращает индекс вновь добавленного элемента.
Функция Delete_
удаляет элемент из списка с индексом Idx. Функция возвращает 0.
Функция IndexOfObject
возвращает индекс элемента в списке. Указатель на список задаётся в параметре Self.
В параметре Obj задаётся указатель на элемент индекс которого определяется.
Функция Field
возвращает указатель на поле по его имени из объекта заданного в Self.
Вместо имени поля можно задать номер элемента в списке или номер поля. Нумерация начинается с 0.
Функция Count_
возвращает количество элементов в списке или полей в объекте, заданного в параметре Self.
Функция Child
возвращает или устанавливает значение для элемента с индексом Idx в объекте или списке Self. Индексация начинается с 0. Если параметр Obj не задан, то возвращает указатель на элемент с индексов Idx.
Если Obj указан, то устанавливает его значение в элемент с индексов Idx. Обратите внимание Obj это указатель на один из потомков TlkJSONbase
.
Функция Generate
возвращает указатель на новый объект TlkJSONlist
, который представляет собой пустой список.
Параметр Self — указатель на JSON объект на основе которого создаётся TlkJSONlist
.
Функция SelfType
возвращает тип объекта для указателя заданного в параметре Self. Тип объекта возвращается как число. Если параметр Self не задан, то вернёт 5.
Функция SelfTypeName
возвращает тип объекта для указателя заданного в параметре Self. Тип объекта возвращается как строка. Если параметр Self не задан, то вернёт 'jsList'
.
Заголовок этого пакета выглядит следующим образом:
CREATE OR ALTER PACKAGE JS$METH
AS
BEGIN
/* TlkJSONbase = class
TlkJSONobjectmethod = class(TlkJSONbase)
*/
FUNCTION MethodObjValue(Self TY$POINTER) RETURNS TY$POINTER;
FUNCTION MethodName(Self TY$POINTER, Name VARCHAR(128) CHARACTER SET NONE = NULL /* Get */) RETURNS VARCHAR(128) CHARACTER SET NONE;
FUNCTION MethodGenerate(Self TY$POINTER, Name VARCHAR(128) CHARACTER SET NONE, Obj TY$POINTER /* js$Base */)
RETURNS TY$POINTER /* js$Meth */;
END
Как видно из комментария этот пакет является калькой с класса TlkJSONobjectmethod
. Он представляет собой пару ключ — значение.
Функция MethodObjValue
возвращает указатель на значение из пары ключ-значение, указанной в параметре Self.
Функция MethodName
возвращает или устанавливает имя ключа для пары ключ-значение, указанной в параметре Self.
Если параметр Name не указан, то возвращает имя ключа, если указан, то устанавливает новое имя ключа.
Функция MethodGenerate
создаёт новую пару ключ-значение и возвращает указатель на неё. В параметре Name указывается имя ключа, в параметре Obj — указатель на значение ключа.
Заголовок этого пакета выглядит следующим образом:
CREATE OR ALTER PACKAGE JS$NULL
AS
BEGIN
/* TlkJSONbase = class
TlkJSONnull = class(TlkJSONbase)
*/
FUNCTION Value_(Self TY$POINTER) RETURNS SMALLINT;
FUNCTION Generate(Self TY$POINTER = NULL /* NULL - class function */) RETURNS TY$POINTER;
FUNCTION SelfType(Self TY$POINTER = NULL /* NULL - class function */) RETURNS SMALLINT;
FUNCTION SelfTypeName(Self TY$POINTER = NULL /* NULL - class function */) RETURNS VARCHAR(32) CHARACTER SET NONE;
END
Как видно из комментария этот пакет является калькой с класса TlkJSONnull
. Он предназначен для обработки значения NULL.
Функция Value_
- возвращает 0, если значение объекта в Self представляет собой значение null (jsNull), и 1 в противном случае.
Функция Generate
возвращает указатель на новый объект TlkJSONnull
, который представляет собой значение null.
Параметр Self — указатель на JSON объект на основе которого создаётся TlkJSONnull
.
Функция SelfType
возвращает тип объекта для указателя заданного в параметре Self. Тип объекта возвращается как число. Если параметр Self не задан, то вернёт 4.
Функция SelfTypeName
возвращает тип объекта для указателя заданного в параметре Self. Тип объекта возвращается как строка. Если параметр Self не задан, то вернёт 'jsNull'
.
Заголовок этого пакета выглядит следующим образом:
CREATE OR ALTER PACKAGE JS$NUM
AS
BEGIN
/* TlkJSONbase = class
TlkJSONnumber = class(TlkJSONbase)
*/
FUNCTION Value_(Self TY$POINTER, Num DOUBLE PRECISION = NULL /* Get */) RETURNS DOUBLE PRECISION;
FUNCTION Generate(Self TY$POINTER = NULL /* NULL - class function */, Num DOUBLE PRECISION = 0) RETURNS TY$POINTER;
FUNCTION SelfType(Self TY$POINTER = NULL /* NULL - class function */) RETURNS SMALLINT;
FUNCTION SelfTypeName(Self TY$POINTER = NULL /* NULL - class function */) RETURNS VARCHAR(32) CHARACTER SET NONE;
END
Как видно из комментария этот пакет является калькой с класса TlkJSONnumber
. Он предназначен для обработки числовых значений.
Функция Value_
возвращает или устанавливает в значение числового типа для объекта заданного в параметре Self. Если параметр Num не задан, то значение будет возвращено, если задан — установлено.
Обратите внимание, NULL не возвращается и не может быть установлено этим методом, для этого существует отдельный пакет JS$NULL
.
Функция Generate
возвращает указатель на объект TlkJSONnumber
, который представляет собой значение числового типа в JSON.
Параметр Self — указатель на JSON объект на основе которого создаётся объект TlkJSONnumber
.
В параметре Num передаётся значение числового типа.
Функция SelfType
возвращает тип объекта для указателя заданного в параметре Self. Тип объекта возвращается как число. Если параметр Self не задан, то вернёт 1.
Функция SelfTypeName
возвращает тип объекта для указателя заданного в параметре Self. Тип объекта возвращается как строка. Если параметр Self не задан, то вернёт 'jsNumber'
.
Заголовок этого пакета выглядит следующим образом:
CREATE OR ALTER PACKAGE JS$OBJ
AS
BEGIN
/* TlkJSONbase = class
TlkJSONcustomlist = class(TlkJSONbase)
TlkJSONobject = class(TlkJSONcustomlist)
*/
FUNCTION New_(UseHash BOOLEAN = TRUE) RETURNS TY$POINTER;
FUNCTION Dispose(Self TY$POINTER) RETURNS SMALLINT; /* 0 - succes */
PROCEDURE ForEach(Self TY$POINTER) RETURNS (Idx INTEGER, Name VARCHAR(128) CHARACTER SET NONE, Obj TY$POINTER /* js$Meth */);
FUNCTION Add_(Self TY$POINTER, Name VARCHAR(128) CHARACTER SET NONE, Obj TY$POINTER) RETURNS INTEGER;
FUNCTION AddBoolean(Self TY$POINTER, Name VARCHAR(128) CHARACTER SET NONE, Bool BOOLEAN) RETURNS INTEGER;
FUNCTION AddDouble(Self TY$POINTER, Name VARCHAR(128) CHARACTER SET NONE, Dbl DOUBLE PRECISION) RETURNS INTEGER;
FUNCTION AddInteger(Self TY$POINTER, Name VARCHAR(128) CHARACTER SET NONE, Int_ INTEGER) RETURNS INTEGER;
FUNCTION AddString(Self TY$POINTER, Name VARCHAR(128) CHARACTER SET NONE, Str VARCHAR(32765) CHARACTER SET NONE) RETURNS INTEGER;
FUNCTION AddWideString(Self TY$POINTER, Name VARCHAR(128) CHARACTER SET NONE, WStr BLOB SUB_TYPE TEXT) RETURNS INTEGER;
FUNCTION Delete_(Self TY$POINTER, Idx Integer) RETURNS SMALLINT;
FUNCTION IndexOfName(Self TY$POINTER, Name VARCHAR(128) CHARACTER SET NONE) RETURNS INTEGER;
FUNCTION IndexOfObject(Self TY$POINTER, Obj TY$POINTER) RETURNS INTEGER;
FUNCTION Field(Self TY$POINTER, Name VARCHAR(128) CHARACTER SET NONE /* = Idx */, Obj TY$POINTER = NULL /* Get */) RETURNS TY$POINTER;
FUNCTION Count_(Self TY$POINTER) RETURNS INTEGER;
FUNCTION Child(Self TY$POINTER, Idx INTEGER, Obj TY$POINTER = NULL /* Get */) RETURNS TY$POINTER;
FUNCTION Generate(Self TY$POINTER = NULL /* NULL - class function */, UseHash BOOLEAN = TRUE) RETURNS TY$POINTER;
FUNCTION SelfType(Self TY$POINTER = NULL /* NULL - class function */) RETURNS SMALLINT;
FUNCTION SelfTypeName(Self TY$POINTER = NULL /* NULL - class function */) RETURNS VARCHAR(32) CHARACTER SET NONE;
FUNCTION FieldByIndex(Self TY$POINTER, Idx INTEGER, Obj TY$POINTER = NULL /* Get */) RETURNS TY$POINTER;
FUNCTION NameOf(Self TY$POINTER, Idx INTEGER) RETURNS VARCHAR(128) CHARACTER SET NONE;
FUNCTION GetBoolean(Self TY$POINTER, Idx INTEGER) RETURNS BOOLEAN;
FUNCTION GetDouble(Self TY$POINTER, Idx INTEGER) RETURNS DOUBLE PRECISION;
FUNCTION GetInteger(Self TY$POINTER, Idx INTEGER) RETURNS INTEGER;
FUNCTION GetString(Self TY$POINTER, Idx INTEGER) RETURNS VARCHAR(32765) CHARACTER SET NONE;
FUNCTION GetWideString(Self TY$POINTER, Idx INTEGER) RETURNS BLOB SUB_TYPE TEXT;
FUNCTION GetBooleanByName(Self TY$POINTER, Name VARCHAR(128) CHARACTER SET NONE) RETURNS BOOLEAN;
FUNCTION GetDoubleByName(Self TY$POINTER, Name VARCHAR(128) CHARACTER SET NONE) RETURNS DOUBLE PRECISION;
FUNCTION GetIntegerByName(Self TY$POINTER, Name VARCHAR(128) CHARACTER SET NONE) RETURNS INTEGER;
FUNCTION GetStringByName(Self TY$POINTER, Name VARCHAR(128) CHARACTER SET NONE) RETURNS VARCHAR(32765) CHARACTER SET NONE;
FUNCTION GetWideStringByName(Self TY$POINTER, Name VARCHAR(128) CHARACTER SET NONE) RETURNS BLOB SUB_TYPE TEXT;
END
Как видно из комментария этот пакет является калькой с класса TlkJSONobject
. Он предназначен для обработки объектных значений.
Функция New_
создаёт и возвращает указатель на новый пустой объект. Если UseHash установлен в TRUE (значение по умолчанию), то для поиска полей внутри объекта будет использована HASH таблица, в противном случае поиск будет осуществляться простым перебором.
Функция Dispose
предназначена для освобождения указателя на JSON объект. Указатели, которые надо принудительно освобождать, появляются в результате парсинга или создания JSON.
Не следует вызывать его для промежуточных объектов при разборе или сборке JSON. Он требуется только для объекта верхнего уровня.
Процедура ForEach
извлекает каждое поле объекта из указателя на JSON заданного в Self.
Возвращаются следующие значения:
- Idx — индекс элемента списка или номер поля в объекте. Начинается с 0.
- Name — имя очередного поля, если Self — объект. Или индекс элемента списка, начиная с 0, если Self — список.
- Obj — указатель на пару ключ-значение (для обработки такой пары необходимо использовать пакет
JS$METH
).
Функция Add_
добавляет новое поле в объект, указатель на который указан в параметре Self.
Добавляемый элемент указывается в параметре Obj, который должен быть указателем на один из потомков TlkJSONbase
.
Имя поля указывается в параметре Name. Функция возвращает индекс вновь добавленного поля.
Функция AddBoolean
добавляет новое поле логического типа в объект, указатель на который указан в параметре Self.
Имя поля указывается в параметре Name. Значение поля указывается в параметре Bool. Функция возвращает индекс вновь добавленного поля.
Функция AddDouble
добавляет новое поле вещественного типа в объект, указатель на который указан в параметре Self.
Имя поля указывается в параметре Name. Значение поля указывается в параметре Dbl. Функция возвращает индекс вновь добавленного поля.
Функция AddInteger
добавляет новое поле целочисленного типа в объект, указатель на который указан в параметре Self.
Имя поля указывается в параметре Name. Значение поля указывается в параметре Int_. Функция возвращает индекс вновь добавленного поля.
Функция AddString
добавляет новое поле строкового типа (VARCHAR(N)
) в объект, указатель на который указан в параметре Self.
Имя поля указывается в параметре Name. Значение поля указывается в параметре Int_. Функция возвращает индекс вновь добавленного поля.
Функция AddWideString
добавляет новое поле типа BLOB SUB_TYPE TEXT
в объект, указатель на который указан в параметре Self.
Имя поля указывается в параметре Name. Значение поля указывается в параметре Int_. Функция возвращает индекс вновь добавленного поля.
Функция Delete_
удаляет поле из объекта с индексом Idx. Функция возвращает 0.
Функция IndexOfName
возвращает индекс поля по его имени. Указатель на объект задаётся в параметре Self.
В параметре Obj задаётся указатель на элемент индекс которого определяется.
Функция IndexOfObject
возвращает индекс значения поля в объекте. Указатель на объект задаётся в параметре Self.
В параметре Obj задаётся указатель на значения поля индекс которого определяется.
Функция Field
возвращает или устанавливает значение поля по его имени. Указатель на объект задаётся в параметре Self.
Имя поля указывается в параметре Name.
Вместо имени поля можно задать номер элемента в списке или номер поля. Нумерация начинается с 0.
Если в параметре Obj указано значение отличное от NULL, то новое значение будет прописано в поле, в
противном случае функция вернёт указатель на значение поля.
Функция Count_
возвращает количество полей в объекте, заданного в параметре Self.
Функция Child
возвращает или устанавливает значение для элемента с индексом Idx в объекте Self. Индексация начинается с 0. Если параметр Obj не задан, то возвращает указатель на элемент с индексов Idx.
Если Obj указан, то устанавливает его значение в элемент с индексов Idx. Обратите внимание Obj это указатель на один из потомков TlkJSONbase
.
Функция Generate
возвращает указатель на объект TlkJSONobject
, который представляет собой объект в JSON.
Если UseHash установлен в TRUE (значение по умолчанию), то для поиска полей внутри объекта будет использована HASH таблица, в противном случае поиск будет осуществляться простым перебором. В параметре Self передаётся указатель на объект на основе которого создаётся новый объект типа TlkJSONobject
.
Функция SelfType
возвращает тип объекта для указателя заданного в параметре Self. Тип объекта возвращается как число. Если параметр Self не задан, то вернёт 6.
Функция SelfTypeName
возвращает тип объекта для указателя заданного в параметре Self. Тип объекта возвращается как строка. Если параметр Self не задан, то вернёт 'jsObject'
.
Функция FieldByIndex
возвращает или устанавливает свойство как пару ключ-значение по заданному индексу Idx. Указатель на объект задаётся в параметре Self.
Для обработки пары ключ-значение необходимо использовать пакет JS$METH
. Если в параметре Obj указано значение отличное от NULL, то новое значение будет поле будет записано по заданному индексу, в противном случае функция вернёт указатель на поле.
Функция NameOf
возвращает имя поля по его индексу заданному в параметре Idx. Указатель на объект задаётся в параметре Self.
Функция GetBoolean
возвращает логическое значение поля объекта с индексом Idx. Индексация начинается с 0.
Функция GetDouble
возвращает значение с плавающей точкой поля объекта с индексом Idx. Индексация начинается с 0.
Функция GetInteger
возвращает целочисленное значение поля объекта с индексом Idx. Индексация начинается с 0.
Функция GetString
возвращает символьное значение (VARCHAR
) поля объекта с индексом Idx. Индексация начинается с 0.
Функция GetWideString
возвращает значение типа BLOB SUN_TYPE TEXT
поля объекта с индексом Idx. Индексация начинается с 0.
Функция GetBooleanByName
возвращает логическое значение поля объекта по его имени Name.
Функция GetDoubleByName
возвращает значение с плавающей точкой поля объекта по его имени Name.
Функция GetIntegerByName
возвращает целочисленное значение поля объекта по его имени Name.
Функция GetStringByName
возвращает символьное значение (VARCHAR
) поля объекта по его имени Name.
Функция GetWideStringByName
возвращает значение типа BLOB SUN_TYPE TEXT
поля объекта по его имени Name.
Заголовок этого пакета выглядит следующим образом:
CREATE OR ALTER PACKAGE JS$PTR
AS
BEGIN
FUNCTION New_
(UsePtr CHAR(3) CHARACTER SET NONE /* Tra - Transaction, Att - Attachment */, UseHash BOOLEAN = TRUE)
RETURNS TY$POINTER;
FUNCTION Dispose(UsePtr CHAR(3) CHARACTER SET NONE) RETURNS SMALLINT;
FUNCTION Tra RETURNS TY$POINTER;
FUNCTION Att RETURNS TY$POINTER;
FUNCTION isNull(jsPtr TY$POINTER) RETURNS BOOLEAN;
END
Этот пакет помогает следить за указателями, которые возникают при создании объектов JSON.
Функция New_
создаёт и возвращает указатель на новый пустой объект.
Если в параметр UsePtr передано значение 'Tra', то указатель будет привязан к транзакции, и по её завершении он будет автоматически удалён.
Если в параметр UsePtr передано значение 'Att', то указатель будет привязан к соединению, и при его закрытии он будет автоматически удалён.
Если UseHash установлен в TRUE (значение по умолчанию), то для поиска полей внутри объекта будет использована HASH таблица, в противном случае поиск будет осуществляться простым перебором.
Функция Dispose
удаляет указатель на JSON объект привязанный к транзакции или соединению.
Если в параметр UsePtr передано значение 'Tra', то будет удалён указатель привязанный к транзакции.
Если в параметр UsePtr передано значение 'Att', то будет удалён указатель привязанный к соединению.
Функция Tra
возвращает указатель привязанный к транзакции.
Функция Att
возвращает указатель привязанный к соединению.
Функция isNull
проверяет не является ли указатель нулевым (с нулевым адресом). Нулевой указатель возвращает функции js$func.ParseText
и js$func.ParseString
в случае некорректного JSON на входе, обращение к несуществующему полю через метод Field и другое. Эут функцию можно использовать для детектирования таких ошибок.
Заголовок этого пакета выглядит следующим образом:
CREATE OR ALTER PACKAGE JS$STR
AS
BEGIN
/* TlkJSONbase = class
TlkJSONstring = class(TlkJSONbase)
*/
FUNCTION Value_(Self TY$POINTER, Str VARCHAR(32765) CHARACTER SET NONE = NULL /* Get */) RETURNS VARCHAR(32765) CHARACTER SET NONE;
FUNCTION WideValue_(Self TY$POINTER, WStr BLOB SUB_TYPE TEXT = NULL /* Get */) RETURNS BLOB SUB_TYPE TEXT;
FUNCTION Generate(Self TY$POINTER = NULL /* NULL - class function */, Str VARCHAR(32765) CHARACTER SET NONE = '') RETURNS TY$POINTER;
FUNCTION WideGenerate(Self TY$POINTER = NULL /* NULL - class function */, WStr BLOB SUB_TYPE TEXT = '') RETURNS TY$POINTER;
FUNCTION SelfType(Self TY$POINTER = NULL /* NULL - class function */) RETURNS SMALLINT;
FUNCTION SelfTypeName(Self TY$POINTER = NULL /* NULL - class function */) RETURNS VARCHAR(32) CHARACTER SET NONE;
END
Как видно из комментария этот пакет является калькой с класса TlkJSONstring
. Он предназначен для обработки строковых значений.
Функция Value_
возвращает или устанавливает в значение строкового типа (VARCHAR(N)
) для объекта заданного в параметре Self. Если параметр Str не задан, то значение будет возвращено, если задан — установлено.
Обратите внимание, NULL не возвращается и не может быть установлено этим методом, для этого существует отдельный пакет JS$NULL
.
Функция WideValue__
возвращает или устанавливает в значение типа BLOB SUB_TYPE TEXT
для объекта заданного в параметре Self. Если параметр Str не задан, то значение будет возвращено, если задан — установлено.
Обратите внимание, NULL не возвращается и не может быть установлено этим методом, для этого существует отдельный пакет JS$NULL
.
Функция Generate
возвращает указатель на объект TlkJSONstring
, который представляет собой значение строкового типа в JSON.
Параметр Self — указатель на JSON объект на основе которого создаётся новый объект TlkJSONstring
.
Строковое значение задаётся в параметре Str.
Функция WideGenerate
возвращает указатель на объект TlkJSONstring
, который представляет собой значение строкового типа в JSON.
Параметр Self — указатель на JSON объект для которого устанавливается длинное строковое значение (BLOB SUB_TYPE TEXT
) в параметре Str.
Значение параметра Self будет возращено функцией, если оно отлично от NULL, в противном случае вернёт указатель на новый объект TlkJSONstring
.
Функция SelfType
возвращает тип объекта для указателя заданного в параметре Self. Тип объекта возвращается как число. Если параметр Self не задан, то вернёт 2.
Функция SelfTypeName
возвращает тип объекта для указателя заданного в параметре Self. Тип объекта возвращается как строка. Если параметр Self не задан, то вернёт 'jsString'
.
Исходный код процедур и функций примеров вы можете скачать по ссылке examples.sql.
Для примера возьмём базу данных employee.
Замечание
Я в своих примерах использовал модифицированную базу данных employee преобразованную в кодировку UTF8.
Функция MAKE_JSON_DEPARTMENT_TREE
выводит список подразделений в формате JSON в иерархическом виде.
CREATE OR ALTER FUNCTION MAKE_JSON_DEPARTMENT_TREE
RETURNS BLOB SUB_TYPE TEXT
AS
DECLARE VARIABLE JSON_TEXT BLOB SUB_TYPE TEXT;
DECLARE VARIABLE JSON TY$POINTER;
DECLARE VARIABLE JSON_SUB_DEPS TY$POINTER;
BEGIN
JSON = JS$OBJ.NEW_();
FOR
WITH RECURSIVE R
AS (SELECT
:JSON AS JSON,
CAST(NULL AS TY$POINTER) AS PARENT_JSON,
D.DEPT_NO,
D.DEPARTMENT,
D.HEAD_DEPT,
D.MNGR_NO,
D.BUDGET,
D.LOCATION,
D.PHONE_NO
FROM DEPARTMENT D
WHERE D.HEAD_DEPT IS NULL
UNION ALL
SELECT
JS$OBJ.NEW_() AS JSON,
R.JSON,
D.DEPT_NO,
D.DEPARTMENT,
D.HEAD_DEPT,
D.MNGR_NO,
D.BUDGET,
D.LOCATION,
D.PHONE_NO
FROM DEPARTMENT D
JOIN R
ON D.HEAD_DEPT = R.DEPT_NO)
SELECT
JSON,
PARENT_JSON,
DEPT_NO,
DEPARTMENT,
HEAD_DEPT,
MNGR_NO,
BUDGET,
LOCATION,
PHONE_NO
FROM R AS CURSOR C_DEP
DO
BEGIN
-- для каждого нового подразделения заполняем значение полей JSON объекта
JS$OBJ.ADDSTRING(C_DEP.JSON, 'dept_no', C_DEP.DEPT_NO);
JS$OBJ.ADDSTRING(C_DEP.JSON, 'department', C_DEP.DEPARTMENT);
IF (C_DEP.HEAD_DEPT IS NOT NULL) THEN
JS$OBJ.ADDSTRING(C_DEP.JSON, 'head_dept', C_DEP.HEAD_DEPT);
ELSE
JS$OBJ.ADD_(C_DEP.JSON, 'head_dept', JS$NULL.GENERATE());
IF (C_DEP.MNGR_NO IS NOT NULL) THEN
JS$OBJ.ADDINTEGER(C_DEP.JSON, 'mngr_no', C_DEP.MNGR_NO);
ELSE
JS$OBJ.ADD_(C_DEP.JSON, 'mngr_no', JS$NULL.GENERATE());
-- тут возможно ADDSTRING лучше, так как гарантированно сохранит точность
JS$OBJ.ADDDOUBLE(C_DEP.JSON, 'budget', C_DEP.BUDGET);
JS$OBJ.ADDSTRING(C_DEP.JSON, 'location', C_DEP.LOCATION);
JS$OBJ.ADDSTRING(C_DEP.JSON, 'phone_no', C_DEP.PHONE_NO);
-- в каждое подразделение добавляем список, в который будут
-- вносится подчинённые подразделения
JS$OBJ.ADD_(C_DEP.JSON, 'departments', JS$LIST.GENERATE());
IF (C_DEP.PARENT_JSON IS NOT NULL) THEN
BEGIN
-- там где есть подразделения, есть и объект родительского объекта JSON
-- получаем из этого родительского объекта поле со списком
JSON_SUB_DEPS = JS$OBJ.FIELD(C_DEP.PARENT_JSON, 'departments');
-- и добавляем в него текущее подразделение
JS$LIST.ADD_(JSON_SUB_DEPS, C_DEP.JSON);
END
END
-- генерируем JSON в виде текста
JSON_TEXT = JS$FUNC.READABLETEXT(JSON);
-- не забываем очистить указтель
JS$OBJ.DISPOSE(JSON);
RETURN JSON_TEXT;
WHEN ANY DO
BEGIN
-- если была ошибка всё равно очищаем указатель
JS$OBJ.DISPOSE(JSON);
EXCEPTION;
END
END
Здесь мы применили следующую хитрость: на самом верхнем уровне рекурсивного запроса используется указатель на ранее
созданный корневой объект JSON. Во рекурсивной части запроса, мы выводим JSON объект для родительского подразделения PARENT_JSON
и JSON объект для
текущего подразделения PARENT_JSON
. Таким образом, мы всегда знаем в какой JSON объект добавлять подчинённое подразделение.
Далее пробегаем циклом по курсору и на каждой итерации добавляем значения полей дл текущего подразделения.
Обратите внимание для того, чтобы добавить значение NULL, приходится использовать вызов JS$NULL.GENERATE()
.
Если вы не будете делать этого, то при вызове JS$OBJ.ADDSTRING(C_DEP.JSON, 'head_dept', C_DEP.HEAD_DEPT)
, когда
C_DEP.HEAD_DEPT
равно NULL поле head_dept
просто не будет добавлено.
Также для каждого подразделения необходимо добавить JSON список, в который будут добавляться подчинённые подразделения.
Если JSON объект родительского подразделения не NULL, то получаем разнее добавленный для него список с помощью функции JS$OBJ.FIELD
и добавляем в него текущий объект JSON.
Далее JSON объекта самого верхнего уровня можно сгененрировать текст, после чего сам объект нам больше не нужен и
необходимо очистить выделенный для него указатель с помощью функции JS$OBJ.DISPOSE
.
Обратите внимание на блок обработки исключений WHEN ANY DO
. Он обязателен, поскольку даже когда произошла нам надо
освободить указатель, чтобы избежать утечки памяти.
Разбирать JSON несколько сложнее, чем собирать его. Дело в том, что вам надо учитывать, что на вход может поступить некорректный JSON, не только сам по себе, но и со структурой не отвечающей вашей логике.
Предположим у вас есть JSON в котором содержится список людей с их характеристиками.
Этот JSON выглядит следующим образом:
[
{"id": 1, "name": "Вася"},
{"id": 2, "name": null}
]
Напишем хранимую процедуру, которая возвращает список людей из этого JSON
create exception e_custom_error 'custom error';
set term ^;
CREATE OR ALTER PROCEDURE PARSE_PEOPLES_JSON (
JSON_STR BLOB SUB_TYPE TEXT)
RETURNS (
ID INTEGER,
NAME VARCHAR(120))
AS
declare variable json TY$POINTER;
declare variable jsonId TY$POINTER;
declare variable jsonName TY$POINTER;
begin
json = js$func.parsetext(json_str);
-- если JSON некорректный js$func.parsetext не сгененрирует исключение,
-- а вернёт нулевой указатель
-- поэтому надо обработать такой случай самостоятельно
if (js$ptr.isNull(json)) then
exception e_custom_error 'invalid json';
-- Опять же функции из этой библиотеки не проверяют корректность типов элементов
-- и не возвращают ошибку понятную. Нам надо проверить тот ли тип мы обрабатываем.
-- Иначе js$list.foreach вернёт "Access violation"
if (js$base.SelfTypeName(json) != 'jsList') then
exception e_custom_error 'Invalid JSON format. The top level of the JSON item must be a list. ';
for
select Obj
from js$list.foreach(:json)
as cursor c
do
begin
-- Проверяем, что элемент массива - это объект, иначе
-- js$obj.GetIntegerByName вернёт "Access violation"
if (js$base.SelfTypeName(c.Obj) != 'jsObject') then
exception e_custom_error 'Element of list is not object';
-- js$obj.GetIntegerByName не проверяет существования элемента с заданным именем
-- она просто молча вернёт 0!!!!! Надо самому проверить
-- А js$obj.Field вернёт нулевой указатель
if (js$obj.indexofname(c.Obj, 'id') < 0) then
exception e_custom_error 'Field "id" not found in object';
jsonId = js$obj.Field(c.Obj, 'id');
if (js$base.selftypename(jsonId) = 'jsNull') then
id = null;
else if (js$base.selftypename(jsonId) = 'jsNumber') then
id = js$obj.GetIntegerByName(c.Obj, 'id');
else
exception e_custom_error 'Field "id" is not number';
if (js$obj.indexofname(c.Obj, 'name') < 0) then
exception e_custom_error 'Field "name" not found in object';
jsonName = js$obj.Field(c.Obj, 'name');
if (js$str.selftypename(jsonName) = 'jsNull') then
name = null;
else
name = js$str.value_(jsonName);
suspend;
end
js$base.dispose(json);
when any do
begin
js$base.dispose(json);
exception;
end
end^
set term ;^
Для проверки правильности выполните следующий запрос
select id, name
from parse_peoples_json( '[{"id": 1, "name": "Вася"}, {"id": 2, "name": null}]' )
Посмотрим внимательно на скрипт разбора JSON. Первая особенность состоит в том, что функция js$func.parsetext
не сгенерирует исключение, если вместо JSON на вход подана любая другая строка. Она просто вернёт пустой указатель.
Но, это не NULL как вам казалось, а указатель с содержимым x'0000000000000000'
. Поэтому после выполнения
этой функции надо проверить, а что же вам было возвращено, иначе вызовы последующий функций будут возвращать ошибку
"Access violation".
Далее важно проверять, какого типа объект JSON был возвращён. Если на входе вместо списка окажется объект или любой
другой тип, то вызов js$list.foreach
вернёт "Access violation". То же самое произойдёт если вы вызовите любую другую
функцию, которая ожидает указатель на другой, не предназначенный для неё тип.
Следующая особенность состоит в том, что всегда надо проверять наличие полей (свойств объекта). Если поля с заданным именем нет, то
в некоторых случаях может быть возвращено не корректное значение (как в случае с js$obj.GetIntegerByName
),
в других приведёт к ошибке преобразования типа.
Обратите внимание, функции вроде js$obj.GetIntegerByName
или js$obj.GetSrtingByName
не могут вернуть значение NULL.
Для распознавания значения NULL, вам надо проверять тип поля функцией js$base.selftypename
.
Как и в случае со сборкой JSON не забывайте освобождать указатель на JSON верхнего уровня, а также делать это в блоке обработки исключений
WHEN ANY DO
.
Далее приведём пример разбора JSON, который был собран функцией MAKE_JSON_DEPARTMENT_TREE
в примере выше.
В тексте примера приведены комментарии поясняющие принцип разбора.
SET TERM ^ ;
CREATE OR ALTER PACKAGE JSON_PARSE_DEPS
AS
BEGIN
PROCEDURE PARSE_DEPARTMENT_TREE (
JSON_TEXT BLOB SUB_TYPE TEXT)
RETURNS (
DEPT_NO CHAR(3),
DEPARTMENT VARCHAR(25),
HEAD_DEPT CHAR(3),
MNGR_NO SMALLINT,
BUDGET DECIMAL(18,2),
LOCATION VARCHAR(15),
PHONE_NO VARCHAR(20));
END^
RECREATE PACKAGE BODY JSON_PARSE_DEPS
AS
BEGIN
PROCEDURE GET_DEPARTMENT_INFO (
JSON TY$POINTER)
RETURNS (
DEPT_NO CHAR(3),
DEPARTMENT VARCHAR(25),
HEAD_DEPT CHAR(3),
MNGR_NO SMALLINT,
BUDGET DECIMAL(18,2),
LOCATION VARCHAR(15),
PHONE_NO VARCHAR(20),
JSON_LIST TY$POINTER);
PROCEDURE PARSE_DEPARTMENT_TREE (
JSON_TEXT BLOB SUB_TYPE TEXT)
RETURNS (
DEPT_NO CHAR(3),
DEPARTMENT VARCHAR(25),
HEAD_DEPT CHAR(3),
MNGR_NO SMALLINT,
BUDGET DECIMAL(18,2),
LOCATION VARCHAR(15),
PHONE_NO VARCHAR(20))
AS
DECLARE VARIABLE JSON TY$POINTER;
BEGIN
JSON = JS$FUNC.PARSETEXT(JSON_TEXT);
-- если JSON некорректный js$func.parsetext не сгененрирует исключение,
-- а просто вернёт нулевой указатель
-- поэтому надо обработать такой случай самостоятельно
IF (JS$PTR.ISNULL(JSON)) THEN
EXCEPTION E_CUSTOM_ERROR 'invalid json';
FOR
SELECT
INFO.DEPT_NO,
INFO.DEPARTMENT,
INFO.HEAD_DEPT,
INFO.MNGR_NO,
INFO.BUDGET,
INFO.LOCATION,
INFO.PHONE_NO
FROM JSON_PARSE_DEPS.GET_DEPARTMENT_INFO(:JSON) INFO
INTO
:DEPT_NO,
:DEPARTMENT,
:HEAD_DEPT,
:MNGR_NO,
:BUDGET,
:LOCATION,
:PHONE_NO
DO
SUSPEND;
JS$OBJ.DISPOSE(JSON);
WHEN ANY DO
BEGIN
JS$OBJ.DISPOSE(JSON);
EXCEPTION;
END
END
PROCEDURE GET_DEPARTMENT_INFO (
JSON TY$POINTER)
RETURNS (
DEPT_NO CHAR(3),
DEPARTMENT VARCHAR(25),
HEAD_DEPT CHAR(3),
MNGR_NO SMALLINT,
BUDGET DECIMAL(18,2),
LOCATION VARCHAR(15),
PHONE_NO VARCHAR(20),
JSON_LIST TY$POINTER)
AS
BEGIN
IF (JS$OBJ.INDEXOFNAME(JSON, 'dept_no') < 0) THEN
EXCEPTION E_CUSTOM_ERROR 'field "dept_no" not found';
DEPT_NO = JS$OBJ.GETSTRINGBYNAME(JSON, 'dept_no');
IF (JS$OBJ.INDEXOFNAME(JSON, 'department') < 0) THEN
EXCEPTION E_CUSTOM_ERROR 'field "department" not found';
DEPARTMENT = JS$OBJ.GETSTRINGBYNAME(JSON, 'department');
IF (JS$OBJ.INDEXOFNAME(JSON, 'head_dept') < 0) THEN
EXCEPTION E_CUSTOM_ERROR 'field "head_dept" not found';
IF (JS$BASE.SELFTYPENAME(JS$OBJ.FIELD(JSON, 'head_dept')) = 'jsNull') THEN
HEAD_DEPT = NULL;
ELSE
HEAD_DEPT = JS$OBJ.GETSTRINGBYNAME(JSON, 'head_dept');
IF (JS$OBJ.INDEXOFNAME(JSON, 'mngr_no') < 0) THEN
EXCEPTION E_CUSTOM_ERROR 'field "mngr_no" not found';
IF (JS$BASE.SELFTYPENAME(JS$OBJ.FIELD(JSON, 'mngr_no')) = 'jsNull') THEN
MNGR_NO = NULL;
ELSE
MNGR_NO = JS$OBJ.GETINTEGERBYNAME(JSON, 'mngr_no');
IF (JS$OBJ.INDEXOFNAME(JSON, 'budget') < 0) THEN
EXCEPTION E_CUSTOM_ERROR 'field "budget" not found';
BUDGET = JS$OBJ.GETDOUBLEBYNAME(JSON, 'budget');
IF (JS$OBJ.INDEXOFNAME(JSON, 'location') < 0) THEN
EXCEPTION E_CUSTOM_ERROR 'field "location" not found';
LOCATION = JS$OBJ.GETSTRINGBYNAME(JSON, 'location');
IF (JS$OBJ.INDEXOFNAME(JSON, 'phone_no') < 0) THEN
EXCEPTION E_CUSTOM_ERROR 'field "phone_no" not found';
PHONE_NO = JS$OBJ.GETSTRINGBYNAME(JSON, 'phone_no');
IF (JS$OBJ.INDEXOFNAME(JSON, 'departments') >= 0) THEN
BEGIN
-- получаем список подчинённых подразделений
JSON_LIST = JS$OBJ.FIELD(JSON, 'departments');
IF (JS$BASE.SELFTYPENAME(JSON_LIST) != 'jsList') THEN
EXCEPTION E_CUSTOM_ERROR 'Invalid JSON format. Field "departments" must be list';
SUSPEND;
-- обходим этот список и рекурсивно вызываем для него процедуру извлечения
-- информации о каждом подраздении
FOR
SELECT
INFO.DEPT_NO,
INFO.DEPARTMENT,
INFO.HEAD_DEPT,
INFO.MNGR_NO,
INFO.BUDGET,
INFO.LOCATION,
INFO.PHONE_NO,
INFO.JSON_LIST
FROM JS$LIST.FOREACH(:JSON_LIST) L
LEFT JOIN JSON_PARSE_DEPS.GET_DEPARTMENT_INFO(L.OBJ) INFO
ON TRUE
INTO
:DEPT_NO,
:DEPARTMENT,
:HEAD_DEPT,
:MNGR_NO,
:BUDGET,
:LOCATION,
:PHONE_NO,
:JSON_LIST
DO
SUSPEND;
END
ELSE
EXCEPTION E_CUSTOM_ERROR 'Invalid JSON format. Field "departments" not found' || DEPT_NO;
END
END
^
SET TERM ; ^