如何利用 PostgreSQL 的 JSONB API 作为扩展的轻量级 JSON 解析器
前言
在基于 C 语言的 PostgreSQL 扩展开发中,您可能会遇到需要处理 JSON 等结构化数据的情况。通常,您可能会在扩展中引入第三方 JSON 解析库,例如 cJSON 或 libjansson。这些库功能强大、易于使用且提供了丰富的特性,但如果我们并未充分利用这些库的高级功能,引入它们则会显得多余。很多时候,我们只是希望从 JSON 中读取某个特定值或简单地遍历它。PostgreSQL 本身已经具备了处理 JSON 数据的足够能力,尽管这些功能可能不如第三方库那样直观。如果您能够充分掌握 PostgreSQL 已有的功能,或许可以避免引入第三方 JSON 库的成本。
在本文中,将向您展示如何使用 PostgreSQL 的 JSONB API 来解析、提取和遍历 JSON 结构。
解析和获取
要使用 PostgreSQL 的 JSONB API,您需要在 C 扩展中包含其头文件:
#include "utils/jsonb.h"
现在,我们可以开始使用 JSONB 了。假设我们有一个 char *
指针,指向一个完整的 JSON 结构内容。我们需要将其转换为 Jsonb *
,然后才能对其进行操作。
/* myjson points to a complete JSON content */
void jsonb_example(const char * myjson)
{
Datum jsonb_datum;
Jsonb * jb;
/* we first convert char * to datum representation */
jsonb_datum = DirectFunctionCall1(jsonb_in, CStringGetDatum(myjson));
/* then, we convert it to Jsonb * */
jb = DatumGetJsonbP(jsonb_datum);
}
假设我们的 JSON 如下所示:
{
"version": "1.0",
"payload": {
"name": "exampleapp",
"ts_ms": 1720811216000,
"db": "postgresql",
"table": "mytable",
"schema": "myschema"
},
"queries": [
{
"query": "select * from mytable"
},
{
"query": "update mytable set a = 1"
}
]
}
为了获取 payload 组下 db 的值,我们可以使用 JSONB 的 jsonb_get_element()
函数,函数原型如下:
Datum jsonb_get_element(Jsonb *jb, Datum *path, int npath,
bool *isnull, bool as_text);
该函数接受一个 JSONB
指针(即我们之前创建的表示整个 JSON 消息的指针),以及一个Datum
数组和 npath
,用于表示 JSON 元素的路径。请注意,此路径不必一直指向标量值,它可以停在另一个内部组或数组,具体取决于您的用例。它还接受一个 isnull
布尔指针,如果找不到元素,函数会将其设置为 false
。最后,as_text
布尔值指示函数是否将结果作为 Text Datum
或 Jsonb Datum
返回。我倾向于将其设置为 false
,以便返回 JSONB Datum,从而可以进一步操作。将其转换为字符串表示也很简单(通过 stringinfo
结构)。请参见以下示例。
/* myjson points to a complete JSON content */
void jsonb_example(const char * myjson)
{
Datum jsonb_datum;
Jsonb * jb;
/* variables needed for fetching element */
Datum datum_elems[2];
Datum res;
int numpath = 2;
bool isnull;
StringInfoData strinfo;
/* we first convert char * to datum representation */
jsonb_datum = DirectFunctionCall1(jsonb_in, CStringGetDatum(myjson));
/* then, we convert it to Jsonb * */
jb = DatumGetJsonbP(jsonb_datum);
/* prepare element paths to fetch, from outer to inner */
initStringInfo(&strinfo);
datum_elems[0] = CStringGetTextDatum("payload");
datum_elems[1] = CStringGetTextDatum("db");
/* fetch it */
res = jsonb_get_element(jb, datum_elems, numPaths, &isnull, false);
if (isnull)
{
/* write NULL if element does not exist */
resetStringInfo(&strinfoo);
appendStringInfoString(&strinfoo, "NULL");
}
else
{
Jsonb *resjb = DatumGetJsonbP(res);
resetStringInfo(strinfoout);
JsonbToCString(&strinfo, &resjb->root, VARSIZE(resjb));
}
/* strinfo contains the value of the element at this point. Print it */
elog(WARNING, "data = %s", strinfo.data);
}
现在,如果我们想从数组中的特定索引处获取特定值。例如,queries
数组下索引为 1 的 query
值(update mytable set a = 1
)。我们只需要修改描述此路径的 datum_elems
。我们可以在数组元素后的 datum_elems
中直接放入一个数字(作为字符串),以告诉函数我们要获取特定索引。请参见以下示例:
/* myjson points to a complete JSON content */
void jsonb_example(const char * myjson)
{
Datum jsonb_datum;
Jsonb * jb;
/* variables needed for fetching element */
Datum datum_elems[3];
Datum res;
int numpath = 3;
bool isnull;
StringInfoData strinfo;
/* we first convert char * to datum representation */
jsonb_datum = DirectFunctionCall1(jsonb_in, CStringGetDatum(myjson));
/* then, we convert it to Jsonb * */
jb = DatumGetJsonbP(jsonb_datum);
/* prepare element paths to fetch, from outer to inner */
initStringInfo(&strinfo);
datum_elems[0] = CStringGetTextDatum("queries");
datum_elems[1] = CStringGetTextDatum("1");
datum_elems[2] = CStringGetTextDatum("query");
/* fetch it */
res = jsonb_get_element(jb, datum_elems, numPaths, &isnull, false);
if (isnull)
{
/* write NULL if element does not exist */
resetStringInfo(&strinfoo);
appendStringInfoString(&strinfoo, "NULL");
}
else
{
Jsonb *resjb = DatumGetJsonbP(res);
resetStringInfo(strinfoout);
JsonbToCString(&strinfo, &resjb->root, VARSIZE(resjb));
}
/* strinfo contains the value of the element at this point. Print it */
elog(WARNING, "data = %s", strinfo.data);
}
如你所见,获取特定元素非常简单。我们只需要准备正确的 datum_elems
数组来描述通向某个值的路径,其他部分保持不变。我们可以编写一个辅助函数,通过从单个字符串自动创建 datum_elems
来简化此过程,该字符串用点分隔每个层次结构(例如:“payload.name”,“queries.0.query” 等)。
void getPathElementString(Jsonb * jb, char * path, StringInfoData strinfoout)
{
Datum * datum_elems = NULL;
char * str_elems = NULL, * p = path;
int numPaths = 0, curr = 0;
char * pathcopy = pstrdup(path);
Datum res;
bool isnull;
if (!strinfoout)
{
elog(WARNING, "strinfo is null");
return -1;
}
/* Count the number of elements in the path */
if (strstr(pathcopy, "."))
{
while (*p != '\0')
{
if (*p == '.')
{
numPaths++;
}
p++;
}
numPaths++; /* Add the last one */
}
else
{
numPaths = 1;
}
datum_elems = palloc0(sizeof(Datum) * numPaths);
/* Parse the path into elements */
if (strstr(pathcopy, "."))
{
str_elems= strtok(pathcopy, ".");
if (str_elems)
{
datum_elems[curr] = CStringGetTextDatum(str_elems);
curr++;
while ((str_elems = strtok(NULL, ".")))
{
datum_elems[curr] = CStringGetTextDatum(str_elems);
curr++;
}
}
}
else
{
/* only one level, just use pathcopy*/
datum_elems[curr] = CStringGetTextDatum(pathcopy);
}
/* Get the element from JSONB */
res = jsonb_get_element(jb, datum_elems, numPaths, &isnull, false);
if (isnull)
{
resetStringInfo(strinfoout);
appendStringInfoString(strinfoout, "NULL");
}
else
{
Jsonb *resjb = DatumGetJsonbP(res);
resetStringInfo(strinfoout);
JsonbToCString(strinfoout, &resjb->root, VARSIZE(resjb));
}
pfree(datum_elems);
pfree(pathcopy);
}
/* myjson points to a complete JSON content */
void jsonb_example(const char * myjson)
{
Datum jsonb_datum;
Jsonb * jb;
StringInfoData strinfo;
/* we first convert char * to datum representation */
jsonb_datum = DirectFunctionCall1(jsonb_in, CStringGetDatum(myjson));
/* then, we convert it to Jsonb * */
jb = DatumGetJsonbP(jsonb_datum);
initStringInfo(&strinfo);
getPathElementString(jb, "payload.db", &strinfo);
elog(WARNING, "payload.db= %s", strinfo.data);
getPathElementString(jb, "queries.0.query", &strinfo);
elog(WARNING, "queries.0.query= %s", strinfo.data);
}
遍历整个 JSON 结构
现在,我们已经知道如何在知道目标的情况下从 JSON 中获取特定值。有时,我们需要遍历整个 JSON 以构建内部数据结构以满足某些需求。在这种情况下,我们可以利用 JSONB 的迭代函数。以下示例代码将创建一个 JSONB 迭代器,然后尝试遍历其中的每个元素。它会在迭代器即将进入组或数组时以及即将退出组或数组时进行指示。您可以根据需要保存键和值。JSONB 读取的值可以表示为不同的数据类型,例如字符串、二进制、数字等。以下示例尝试将它们转换为字符串(二进制除外)以进行输出。
/* myjson points to a complete JSON content */
void jsonb_iterate_example(const char * myjson)
{
Datum jsonb_datum;
Jsonb * jb;
/* iterator related */
JsonbIterator *it;
JsonbValue v;
JsonbIteratorToken r;
char * key = NULL;
char * value = NULL;
/* we first convert char * to datum representation */
jsonb_datum = DirectFunctionCall1(jsonb_in, CStringGetDatum(myjson));
/* then, we convert it to Jsonb * */
jb = DatumGetJsonbP(jsonb_datum);
it = JsonbIteratorInit(&jb->root);
while ((r = JsonbIteratorNext(&it, &v, false)) != WJB_DONE)
{
switch (r)
{
case WJB_BEGIN_OBJECT:
elog(WARNING, "begin group --------------------");
break;
case WJB_END_OBJECT:
elog(WARNING, "end group --------------------");
break;
case WJB_BEGIN_ARRAY:
elog(WARNING, "begin array --------------------");
break;
case WJB_END_ARRAY:
elog(WARNING, "end array --------------------");
break;
case WJB_KEY:
key = pnstrdup(v.val.string.val, v.val.string.len);
elog(WARNING, "key: %s", key);
break;
case WJB_VALUE:
case WJB_ELEM:
switch (v.type)
{
case jbvNull:
elog(WARNING, "value: NULL");
break;
case jbvString:
value = pnstrdup(v.val.string.val, v.val.string.len);
elog(WARNING, "string value: %s", value);
break;
case jbvNumeric:
{
value = DatumGetCString(DirectFunctionCall1(numeric_out, PointerGetDatum(v.val.numeric)));
elog(WARNING, "numeric value: %s", value);
break;
}
case jbvBool:
elog(WARNING, "boolean value: %s", v.val.boolean ? "true" : "false");
if (v.val.boolean)
value = pnstrdup("true", strlen("true"));
else
value = pnstrdup("false", strlen("false"));
break;
case jbvBinary:
elog(WARNING, "binary value");
break;
default:
elog(WARNING, "unknown value type: %d", v.type);
break;
}
break;
default:
elog(WARNING, "Unknown token: %d", r);
break;
}
if (key != NULL && value != NULL)
{
pfree(key);
pfree(value);
key = NULL;
value = NULL;
}
}
总结
PostgreSQL 中的 JSONB API 能做的不仅仅是简单的获取和遍历。例如,它还可以将额外的值推送到现有的 JSONB 结构中。今天我们主要关注获取和遍历,在我看来,这是处理 JSON 时最常见的用例。我希望这里分享的代码示例能对你有所帮助,并防止你在扩展开发中引入第三方 JSON 解析器,因为那可能是多余的。
关于 IvorySQL
lvorySQL 是由瀚高股份主导研发的一款开源的兼容 Oracle 的 PostgreSQL。IvorySQL 与 PostgreSQL 国际社区紧密合作,保持与最新 PG 版本内核同步,为用户提供便捷的升级体验。基于双 Parser 架构设计,100% 与原生 PostgreSQL 兼容,支持丰富的 PostgreSQL 周边工具和扩展,并根据用户需求提供定制化工具。同时,IvorySQL 4.0 提供更全面灵活的 Oracle 兼容功能,具备高度的 SQL 和 PL/SQL 兼容性能够为企业构建更加高效、稳定和灵活的数据库解决方案。
- 官网:https://www.ivorysql.org
- GitHub(欢迎点击 star 收藏哦):https://github.com/IvorySQL/IvorySQL