如何利用 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 > 本文由博客一文多发平台 OpenWrite 发布!

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
-
上一篇
基于ANTLR4的大数据SQL编辑器解析引擎实践|得物技术
一、背景 随着得物离线业务的快速增长,为了脱离全托管服务的一些限制和享受技术发展带来的成本优化,公司提出了大数据Galaxy开源演进项目,将离线业务从全托管且封闭的环境迁移到一个开源且自主可控的生态系统中,而离线开发治理套件是Galaxy自研体系中一个核心的项目,在数据开发IDE中最核心的就是SQL编辑器,我们需要一个SQL解析引擎在SQL编辑提供适配得物自研Spark引擎的语法定义,实时语法解析,语法补全,语法校验等能力,结合业内dataworks和dataphin的实践,我们最终选用ANTLR作为SQL解析引擎底座。 二、ANTLR4 简介 ANTLR(一种语法解析引擎工具)是一个功能强大的解析器生成器,用于读取、处理、执行或翻译结构化文本或二进制文件。它广泛用于构建语言、工具和框架。ANTLR可以根据语法规则文件生成一个可以构建和遍历解析树的解析器。 ANTLR4 特性 ANTLR4 是一个强大的工具,适合用于语言处理、编译器构建、代码分析等多种场景。它的易用性、灵活性和强大的特性使得它成为开发者的热门选择。 强大的文法定义:ANTLR4 允许用户使用简单且易读的文法语法来定义语...
-
下一篇
DeepSearcher深度解读:Agentic RAG的出现,传统RAG的黄昏
前言 准备好迎接搜索3.0时代了吗? 随着这几年AI技术的革新,“搜索应用”成为了AI应用层的第一个共识。 从海外的OpenAI、微软Bing Copilot、Perplexity AI,再到国内的豆包、Kimi,都是这一共识下的代表产品。 技术上,从传统的关键词检索,到RAG,大家已经不满足于只是生成对应的简单回答 而是期待大语言模型能够更好地应用于企业级场景,产生更大的价值。 不久前,OpenAI推出了最新的深度内容生成神器“DeepResearch”,用户只需一个"特斯拉的合理市值是多少"的提问, DeepResearch就能生成一份包括企业财务、业务增长分析,再到最后的市值推演的专业分析报告。 而这,也指明了搜索AGI的发展方向。 在此背景下,如何基于“DeepResearch”理念,对其做定制化改造,成为了近一个月来的市场热门话题。 当然,Zilliz也是其中非常幸运的一员。不久前我们推出了DeepSearcher开源项目 ,不到半个月,在GitHub收获的star数量就已经超过3100! 感谢全球开发者和各位读者们的火热支持! 尝鲜链接:https://github.com...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- MySQL数据库在高并发下的优化方案
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- CentOS7,8上快速安装Gitea,搭建Git服务器
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- CentOS8编译安装MySQL8.0.19
- Dcoker安装(在线仓库),最新的服务器搭配容器使用