Apache Commons RDF 0.5.0 常用 API 与图操作详解
Apache Commons RDF 0.5.0 常用 API 与图操作详解:RDFTerm 术语族、Graph 增删查与过滤迭代、Dataset 与 Quad、BlankNode 唯一性与同图合并、跨实现互操作,逐项实测并给出真实回显。
Apache Commons RDF 0.5.0 常用 API 与图操作详解
目标:系统过一遍 Apache Commons RDF 0.5.0 的核心 API——术语族(IRI/Literal/BlankNode)、Graph 增删查与过滤迭代、Dataset 与 Quad、BlankNode 唯一性与图合并、跨实现互操作——每一项都以真实程序实测并给出可复现回显;本文全部命令与回显均已在 Ubuntu 22.04 实测通过。
Commons RDF 的 API 面很小,围绕 RDF 1.1 数据模型展开:三种节点(IRI、Literal、BlankNode)组成 三元组(Triple),三元组的集合是 图(Graph);给三元组再冠一个「图名」就成为 四元组(Quad),Quad 的集合是 数据集(Dataset,支持命名图)。0.5.0 把工厂统一为 org.apache.commons.rdf.api.RDF 接口,RDFTermFactory 是旧接口(方法均以 default 形式保留)。本文以 simple 内存实现为主、jena 绑定为辅做交叉验证。
一、环境信息
本节固定可复现基线,所有实验代码位于同一 Maven 工程,逐组运行。
- 操作系统:Ubuntu 22.04.5 LTS
- 架构:x86_64
- JDK:OpenJDK 17.0.20.1(apt 包
openjdk-17-jdk) - 构建工具:Apache Maven 3.6.3
- 组件版本:commons-rdf-api 0.5.0、commons-rdf-simple 0.5.0、commons-rdf-jena 0.5.0
- Maven 坐标:
org.apache.commons:commons-rdf-api:0.5.0、org.apache.commons:commons-rdf-simple:0.5.0、org.apache.commons:commons-rdf-jena:0.5.0 - 工作目录:
/root/rdf-demo(工程)、/root/rdf-data(RDF 数据文件,本文仅 4.2 节运行命令引用该目录)
二、API 总览
本节先给全貌,再分节实测。0.5.0 核心接口与职责如下(Javadoc 实际签名):
| 接口 | 职责 | 关键方法 |
|---|---|---|
RDF | 工厂总入口(0.5.0 新增),创建术语与图、数据集 | createIRI、createLiteral、createBlankNode、createTriple、createQuad、createGraph、createDataset |
RDFTermFactory | 旧版工厂接口,方法default化保留 | 同上(不推荐新代码使用) |
IRI | 国际化资源标识符节点 | getIRIString、ntriplesString |
Literal | 字面量节点 | getLexicalForm、getDatatype、getLanguageTag |
BlankNode | 空白节点 | uniqueReference |
Triple | 三元组 | getSubject、getPredicate、getObject |
Quad | 四元组(三元组 + 图名) | getGraphName(返回 Optional)、asTriple |
Graph | 三元组集合 | add、contains、remove、clear、size、stream、iterate |
Dataset | Quad 集合(含默认图与命名图) | getGraph、getGraphNames、add、contains、stream、iterate、size |
RDFSyntax | RDF 语法常量 | TURTLE、NTRIPLES、RDFXML、JSONLD 等 |
💡 技术拓展:
stream()与iterate()是 0.5.0 访问图内容的两条通道——
方法 返回类型 适用场景 注意点 stream(可空过滤参数)Stream链式过滤、计数、聚合 必须关闭资源,推荐 try-with-resources iterate(可空过滤参数)Iterablefor-each 遍历 内部同样基于流实现,遍历中修改图可能抛异常 本文两种通道均实测;过滤参数传
null表示该位置为通配符。
三、前置条件:工程与演示主类
本节给出完整工程,后续所有实测都在其上运行。工程包含 api、simple、jena 三个依赖(jena 绑定仅第八节跨实现实验用到,若不需要可移除,simple 组合零传递依赖)。
pom.xml(完整内容,文件路径 /root/rdf-demo/pom.xml):
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>demo</groupId>
<artifactId>rdf-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>17</maven.compiler.release>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-rdf-api</artifactId>
<version>0.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-rdf-simple</artifactId>
<version>0.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-rdf-jena</artifactId>
<version>0.5.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
</plugin>
</plugins>
</build>
</project>
演示主类 RdfDemo.java(完整内容,文件路径 /root/rdf-demo/src/main/java/demo/RdfDemo.java)按组组织实验,通过第一个命令行参数选择运行哪一组:
package demo;
import org.apache.commons.rdf.api.BlankNode;
import org.apache.commons.rdf.api.BlankNodeOrIRI;
import org.apache.commons.rdf.api.Dataset;
import org.apache.commons.rdf.api.Graph;
import org.apache.commons.rdf.api.IRI;
import org.apache.commons.rdf.api.Literal;
import org.apache.commons.rdf.api.Quad;
import org.apache.commons.rdf.api.RDF;
import org.apache.commons.rdf.api.RDFSyntax;
import org.apache.commons.rdf.api.RDFTerm;
import org.apache.commons.rdf.api.Triple;
import org.apache.commons.rdf.jena.JenaRDF;
import org.apache.commons.rdf.jena.experimental.JenaRDFParser;
import org.apache.commons.rdf.simple.SimpleRDF;
import org.apache.jena.rdf.model.Model;
import org.apache.jena.rdf.model.ModelFactory;
import org.apache.jena.riot.Lang;
import org.apache.jena.riot.RDFDataMgr;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Optional;
import java.util.stream.Stream;
public class RdfDemo {
static final String DATA = System.getProperty("data.dir", "/work/data");
public static void main(String[] args) throws Exception {
String group = args.length > 0 ? args[0] : "all";
if (group.equals("terms") || group.equals("all")) terms();
if (group.equals("graph") || group.equals("all")) graph();
if (group.equals("dataset") || group.equals("all")) dataset();
if (group.equals("blanknode") || group.equals("all")) blanknode();
if (group.equals("crossimpl") || group.equals("all")) crossimpl();
if (group.equals("parse-ttl") || group.equals("all")) parseTurtle();
if (group.equals("parse-ttl-jena") || group.equals("all")) parseTurtleJenaBridge();
if (group.equals("parse-rdfxml") || group.equals("all")) parseRdfXml();
if (group.equals("serialize") || group.equals("all")) serialize();
}
static void terms() {
RDF rdf = new SimpleRDF();
IRI alice = rdf.createIRI("http://example.org/people/alice");
IRI xsdInt = rdf.createIRI("http://www.w3.org/2001/XMLSchema#integer");
Literal plain = rdf.createLiteral("Alice");
Literal zh = rdf.createLiteral("张三", "zh-CN");
Literal age = rdf.createLiteral("30", xsdInt);
BlankNode bn = rdf.createBlankNode();
System.out.println("== RDFTermFactory/RDF 术语创建 ==");
System.out.println("IRI -> " + alice.ntriplesString());
System.out.println("普通字面量 -> " + plain.ntriplesString());
System.out.println("语言标签 -> " + zh.ntriplesString());
System.out.println("类型化 -> " + age.ntriplesString());
System.out.println("BlankNode -> " + bn.ntriplesString());
System.out.println("plain datatype=" + plain.getDatatype().getIRIString()
+ " lang=" + plain.getLanguageTag());
System.out.println("zh datatype=" + zh.getDatatype().getIRIString()
+ " lang=" + zh.getLanguageTag());
System.out.println("age datatype=" + age.getDatatype().getIRIString()
+ " lang=" + age.getLanguageTag());
Literal zh2 = rdf.createLiteral("张三", "zh-CN");
System.out.println("同内容语言字面量 equals=" + zh.equals(zh2));
Literal en = rdf.createLiteral("张三", "en");
System.out.println("不同语言标签 equals=" + zh.equals(en));
try {
rdf.createIRI("not-a-valid-iri");
} catch (IllegalArgumentException e) {
System.out.println("非法 IRI 被拒绝: IllegalArgumentException");
}
}
static void graph() {
RDF rdf = new SimpleRDF();
IRI alice = rdf.createIRI("http://example.org/people/alice");
IRI bob = rdf.createIRI("http://example.org/people/bob");
IRI knows = rdf.createIRI("http://xmlns.com/foaf/0.1/knows");
IRI name = rdf.createIRI("http://xmlns.com/foaf/0.1/name");
IRI mbox = rdf.createIRI("http://xmlns.com/foaf/0.1/mbox");
Graph g = rdf.createGraph();
g.add(alice, knows, bob);
g.add(rdf.createTriple(alice, name, rdf.createLiteral("Alice")));
g.add(bob, name, rdf.createLiteral("Bob"));
g.add(bob, mbox, rdf.createIRI("mailto:bob@example.org"));
System.out.println("== Graph 增删查与过滤迭代 ==");
System.out.println("初始 size=" + g.size());
System.out.println("contains(alice,knows,bob)=" + g.contains(alice, knows, bob));
System.out.println("contains(bob,knows,alice)=" + g.contains(bob, knows, alice));
Triple t = rdf.createTriple(alice, knows, bob);
System.out.println("contains(Triple)=" + g.contains(t));
g.remove(bob, mbox, rdf.createIRI("mailto:bob@example.org"));
System.out.println("remove 后 size=" + g.size()
+ ", contains(mbox)=" + g.contains(bob, mbox, rdf.createIRI("mailto:bob@example.org")));
System.out.println("-- 按主语过滤 stream(alice, null, null) --");
try (Stream<? extends Triple> s = g.stream(alice, null, null)) {
s.forEach(x -> System.out.println(" " + x.getSubject().ntriplesString()
+ " " + x.getPredicate().ntriplesString()
+ " " + x.getObject().ntriplesString()));
}
System.out.println("-- 按谓词过滤 stream(null, name, null) --");
try (Stream<? extends Triple> s = g.stream(null, name, null)) {
System.out.println(" 匹配三元组数=" + s.count());
}
System.out.println("-- 按宾语过滤 stream(null, null, bob) --");
try (Stream<? extends Triple> s = g.stream(null, null, bob)) {
s.forEach(x -> System.out.println(" " + x.getSubject().ntriplesString()
+ " " + x.getPredicate().ntriplesString()
+ " " + x.getObject().ntriplesString()));
}
System.out.println("-- iterate() for-each 全量遍历 --");
for (Triple x : g.iterate()) {
System.out.println(" " + x.getSubject().ntriplesString()
+ " " + x.getPredicate().ntriplesString()
+ " " + x.getObject().ntriplesString());
}
}
static void dataset() {
RDF rdf = new SimpleRDF();
IRI alice = rdf.createIRI("http://example.org/people/alice");
IRI bob = rdf.createIRI("http://example.org/people/bob");
IRI knows = rdf.createIRI("http://xmlns.com/foaf/0.1/knows");
IRI name = rdf.createIRI("http://xmlns.com/foaf/0.1/name");
Dataset ds = rdf.createDataset();
BlankNode ngFriends = rdf.createBlankNode("friends");
ds.add(ngFriends, alice, knows, bob);
ds.add(rdf.createQuad(ngFriends, alice, name, rdf.createLiteral("Alice")));
ds.add(rdf.createQuad(null, bob, name, rdf.createLiteral("Bob")));
System.out.println("== Dataset 与 Quad ==");
System.out.println("dataset size=" + ds.size());
System.out.println("默认图 size=" + ds.getGraph().size());
System.out.println("命名图 friends size=" + ds.getGraph(ngFriends).get().size());
System.out.println("-- getGraphNames() --");
try (Stream<BlankNodeOrIRI> names = ds.getGraphNames()) {
names.forEach(n -> System.out.println(" graphName="
+ (n == null ? "null(默认图)" : n.ntriplesString())));
}
System.out.println("-- 全量 Quad 流(含图名) --");
try (Stream<? extends Quad> qs = ds.stream()) {
qs.forEach(q -> System.out.println(" G="
+ q.getGraphName().map(n -> n.ntriplesString()).orElse("<default>")
+ " " + q.getSubject().ntriplesString()
+ " " + q.getPredicate().ntriplesString()
+ " " + q.getObject().ntriplesString()));
}
System.out.println("-- 按命名图过滤 stream(Optional.of(ng), null, null, null) --");
try (Stream<? extends Quad> qs = ds.stream(Optional.of(ngFriends), null, null, null)) {
System.out.println(" 命名图 friends 内 Quad 数=" + qs.count());
}
System.out.println("-- 按默认图过滤 stream(Optional.empty(), null, null, null) --");
try (Stream<? extends Quad> qs = ds.stream(Optional.empty(), null, null, null)) {
System.out.println(" 默认图内 Quad 数=" + qs.count());
}
System.out.println("-- Quad.asTriple() 丢弃图名 --");
for (Quad q0 : ds.iterate()) {
Triple asT = q0.asTriple();
System.out.println(" Quad G=" + q0.getGraphName().map(n -> n.ntriplesString()).orElse("<default>")
+ " -> Triple: " + asT.getSubject().ntriplesString()
+ " " + asT.getPredicate().ntriplesString()
+ " " + asT.getObject().ntriplesString());
break;
}
}
static void blanknode() {
RDF rdfA = new SimpleRDF();
RDF rdfB = new SimpleRDF();
BlankNode a1 = rdfA.createBlankNode();
BlankNode a2 = rdfA.createBlankNode();
System.out.println("== BlankNode 唯一性与同图合并 ==");
System.out.println("同一工厂两次匿名创建 equals=" + a1.equals(a2));
System.out.println("a1 uniqueReference=" + a1.uniqueReference());
System.out.println("a2 uniqueReference=" + a2.uniqueReference());
BlankNode l1 = rdfA.createBlankNode("shared");
BlankNode l2 = rdfB.createBlankNode("shared");
System.out.println("跨工厂同标签 equals=" + l1.equals(l2));
System.out.println("l1 uniqueReference=" + l1.uniqueReference());
System.out.println("l2 uniqueReference=" + l2.uniqueReference());
System.out.println("-- 同一工厂下合并两个图(空白节点保持连接) --");
IRI doc1 = rdfA.createIRI("http://example.org/doc1");
IRI primaryTopic = rdfA.createIRI("http://xmlns.com/foaf/0.1/primaryTopic");
IRI name = rdfA.createIRI("http://xmlns.com/foaf/0.1/name");
BlankNode topic = rdfA.createBlankNode();
Graph g1 = rdfA.createGraph();
Graph g2 = rdfA.createGraph();
g1.add(doc1, primaryTopic, topic);
g2.add(topic, name, rdfA.createLiteral("合并测试"));
for (Triple t : g2.iterate()) {
g1.add(t);
}
System.out.println("合并后 g1 size=" + g1.size());
boolean linked1 = false;
for (Triple t : g1.iterate()) {
if (t.getPredicate().equals(primaryTopic)) {
RDFTerm obj = t.getObject();
for (Triple t2 : g1.iterate()) {
if (t2.getSubject().equals(obj)) {
linked1 = true;
System.out.println(" 经空白节点连接到: " + t2.getSubject().ntriplesString()
+ " " + t2.getPredicate().ntriplesString()
+ " " + t2.getObject().ntriplesString());
}
}
}
}
System.out.println("同工厂合并后空白节点仍连接=" + linked1);
System.out.println("-- 跨工厂合并(空白节点语义断开) --");
Graph ga = rdfA.createGraph();
Graph gb = rdfB.createGraph();
BlankNode bA = rdfA.createBlankNode("n1");
BlankNode bB = rdfB.createBlankNode("n1");
ga.add(rdfA.createIRI("http://example.org/doc1"), primaryTopic, bA);
gb.add(bB, name, rdfB.createLiteral("另一个工厂"));
for (Triple t : gb.iterate()) {
ga.add(t);
}
System.out.println("合并后 ga size=" + ga.size());
boolean linked2 = false;
for (Triple t : ga.iterate()) {
if (t.getPredicate().equals(primaryTopic)) {
RDFTerm obj = t.getObject();
for (Triple t2 : ga.iterate()) {
if (t2.getSubject().equals(obj)) {
linked2 = true;
}
}
}
}
System.out.println("跨工厂合并后空白节点仍连接=" + linked2 + "(false 表示两图空白节点互不相干)");
}
static void crossimpl() {
SimpleRDF s = new SimpleRDF();
JenaRDF j = new JenaRDF();
Literal ls = s.createLiteral("hello", "en");
Literal lj = j.createLiteral("hello", "en");
IRI is = s.createIRI("http://example.org/people/alice");
IRI ij = j.createIRI("http://example.org/people/alice");
System.out.println("== 跨实现术语等价性(simple 与 jena 绑定) ==");
System.out.println("Literal equals(simple,jena)=" + ls.equals(lj) + ", (jena,simple)=" + lj.equals(ls));
System.out.println("IRI equals(simple,jena)=" + is.equals(ij) + ", (jena,simple)=" + ij.equals(is));
Graph g = s.createGraph();
g.add(j.createTriple(ij, j.createIRI("http://xmlns.com/foaf/0.1/name"), lj));
System.out.println("Jena Triple 写入 Simple Graph 后 size=" + g.size());
System.out.println("Simple 术语 contains=" + g.contains(is,
s.createIRI("http://xmlns.com/foaf/0.1/name"), s.createLiteral("hello", "en")));
}
static void parseTurtle() throws Exception {
JenaRDF rdf = new JenaRDF();
Graph graph = rdf.createGraph();
new JenaRDFParser()
.source(Paths.get(DATA, "people.ttl"))
.contentType(RDFSyntax.TURTLE)
.base("http://example.org/people")
.target(graph)
.parse()
.get();
System.out.println("== 解析 Turtle(experimental RDFParser 统一入口) ==");
System.out.println("解析得到三元组数=" + graph.size());
for (Triple t : graph.iterate()) {
System.out.println(" " + t.getSubject().ntriplesString()
+ " " + t.getPredicate().ntriplesString()
+ " " + t.getObject().ntriplesString());
}
IRI wanda = rdf.createIRI("http://example.org/people#wanda");
System.out.println("相对 IRI <#wanda> 已按 base 解析: contains=" + graph.contains(null, null, wanda));
}
static void parseTurtleJenaBridge() throws Exception {
JenaRDF rdf = new JenaRDF();
Model model = ModelFactory.createDefaultModel();
try (InputStream in = Files.newInputStream(Paths.get(DATA, "people.ttl"))) {
RDFDataMgr.read(model, in, Lang.TURTLE);
}
Graph graph = rdf.asGraph(model);
System.out.println("== 解析 Turtle(Jena RDFDataMgr + asGraph 桥接) ==");
System.out.println("桥接视图三元组数=" + graph.size());
IRI alice = rdf.createIRI("http://example.org/people/alice");
try (Stream<? extends Triple> s = graph.stream(alice,
rdf.createIRI("http://xmlns.com/foaf/0.1/name"), null)) {
s.forEach(t -> System.out.println(" 桥接图查询: " + t.getObject().ntriplesString()));
}
}
static void parseRdfXml() throws Exception {
JenaRDF rdf = new JenaRDF();
Graph graph = rdf.createGraph();
new JenaRDFParser()
.source(Paths.get(DATA, "people.rdf"))
.contentType(RDFSyntax.RDFXML)
.target(graph)
.parse()
.get();
System.out.println("== 解析 RDF/XML(experimental RDFParser 统一入口) ==");
System.out.println("解析得到三元组数=" + graph.size());
for (Triple t : graph.iterate()) {
System.out.println(" " + t.getSubject().ntriplesString()
+ " " + t.getPredicate().ntriplesString()
+ " " + t.getObject().ntriplesString());
}
}
static void serialize() throws Exception {
JenaRDF rdf = new JenaRDF();
Graph graph = rdf.createGraph();
new JenaRDFParser()
.source(Paths.get(DATA, "people.ttl"))
.contentType(RDFSyntax.TURTLE)
.base("http://example.org/people")
.target(graph)
.parse()
.get();
System.out.println("== 序列化(Commons RDF Graph 经 asJenaGraph 回 Jena 输出) ==");
ByteArrayOutputStream nt = new ByteArrayOutputStream();
RDFDataMgr.write(nt, rdf.asJenaGraph(graph), Lang.NTRIPLES);
System.out.println("-- N-Triples 输出 --");
System.out.print(new String(nt.toByteArray(), StandardCharsets.UTF_8));
ByteArrayOutputStream ttl = new ByteArrayOutputStream();
RDFDataMgr.write(ttl, rdf.asJenaGraph(graph), Lang.TURTLE);
System.out.println("-- Turtle 输出 --");
System.out.print(new String(ttl.toByteArray(), StandardCharsets.UTF_8));
}
}
编译并生成运行期 classpath(本文所有组共用,实测回显):
cd /root/rdf-demo && mvn -B -q compile; echo COMPILE_EXIT=$?
COMPILE_EXIT=0
cd /root/rdf-demo && mvn -q -B dependency:build-classpath -Dmdep.outputFile=cp.txt
# 输出节选(实测首尾)
💡 说明:
build-classpath把全部依赖 jar 的绝对路径写入cp.txt,后续运行统一使用-cp target/classes:$(cat cp.txt)。解析/序列化组还会以-Ddata.dir=/root/rdf-data指定数据目录,本篇内存实验不依赖数据文件。
四、RDF 术语 API 详解
本节实测三种节点的创建、规范输出与相等性语义。术语是 RDF 世界的原子,一切图操作都建立在术语相等性之上。
4.1 IRI 与 Literal 实测
运行 terms 组(实测回显):
cd /root/rdf-demo && java -Dfile.encoding=UTF-8 -Ddata.dir=/root/rdf-data -cp target/classes:$(cat cp.txt) demo.RdfDemo terms
== RDFTermFactory/RDF 术语创建 ==
IRI -> <http://example.org/people/alice>
普通字面量 -> "Alice"
语言标签 -> "张三"@zh-cn
类型化 -> "30"^^<http://www.w3.org/2001/XMLSchema#integer>
BlankNode -> _:0f878ac5-bafb-3756-90f7-6e7e756dfd4f
plain datatype=http://www.w3.org/2001/XMLSchema#string lang=Optional.empty
zh datatype=http://www.w3.org/1999/02/22-rdf-syntax-ns#langString lang=Optional[zh-cn]
age datatype=http://www.w3.org/2001/XMLSchema#integer lang=Optional.empty
同内容语言字面量 equals=true
不同语言标签 equals=false
要点解读(全部来自上述实测):
- 语言标签被小写化:传入
"zh-CN",ntriplesString()输出"张三"@zh-cn、getLanguageTag()返回Optional[zh-cn]。这符合 RDF 1.1 规范(语言标签比较不区分大小写,统一按小写),也是 0.5.0 的官方修复项(RELEASE-NOTES 中的 COMMONSRDF-51)。 - 普通字面量的数据类型是
xsd:string,语言标签字面量的数据类型是rdf:langString——RDF 1.1 规定普通字面量只是xsd:string的简写,两种字面量在类型系统上是不同的。 - 类型化字面量
"30"^^xsd:integer:词形与数据类型 IRI 共同决定值空间;createLiteral("30", xsdInt)两个参数缺一不可。 - 字面量相等性:词形 + 数据类型 + 语言标签三者完全一致才
equals=true(实测zh-CN与en比较false)。 - IRI 校验语义:含空格等非法语法的串抛
IllegalArgumentException(实测not a valid iri with spaces被拒绝);而上面not-a-valid-iri这类裸相对引用被静默接受——回显中没有出现「非法 IRI 被拒绝」一行,即该输入未触发异常。需要绝对 IRI 时请在业务侧自行校验协议前缀。
4.2 BlankNode 基本行为
同一回显中的 BlankNode -> _:0f878ac5-bafb-3756-90f7-6e7e756dfd4f 说明:simple 实现的空白节点序列化标签是 UUID(版本号位为 3 的 UUID 格式),每次运行都不同。标签参数(如 createBlankNode("friends"))不会改变 ntriplesString() 输出,只影响工厂实例内部的相等性判断(详见第七节)。
五、Graph API 详解
本节实测图的三类操作:增删查、条件过滤、全量遍历。运行 graph 组(实测回显):
cd /root/rdf-demo && java -Dfile.encoding=UTF-8 -Ddata.dir=/root/rdf-data -cp target/classes:$(cat cp.txt) demo.RdfDemo graph
== Graph 增删查与过滤迭代 ==
初始 size=4
contains(alice,knows,bob)=true
contains(bob,knows,alice)=false
contains(Triple)=true
remove 后 size=3, contains(mbox)=false
-- 按主语过滤 stream(alice, null, null) --
<http://example.org/people/alice> <http://xmlns.com/foaf/0.1/name> "Alice"
<http://example.org/people/alice> <http://xmlns.com/foaf/0.1/knows> <http://example.org/people/bob>
-- 按谓词过滤 stream(null, name, null) --
匹配三元组数=2
-- 按宾语过滤 stream(null, null, bob) --
<http://example.org/people/alice> <http://xmlns.com/foaf/0.1/knows> <http://example.org/people/bob>
-- iterate() for-each 全量遍历 --
<http://example.org/people/alice> <http://xmlns.com/foaf/0.1/knows> <http://example.org/people/bob>
<http://example.org/people/alice> <http://xmlns.com/foaf/0.1/name> "Alice"
<http://example.org/people/bob> <http://xmlns.com/foaf/0.1/name> "Bob"
要点解读(全部来自实测回显):
- add 两种形态:
add(主语, 谓语, 宾语)与add(Triple);contains同样支持双形态。三元组集合语义:重复add同一三元组不会使size增长(集合去重)。 - remove 精确匹配:删除后
size从 4 到 3,contains(mbox)变false。 - 通配符过滤:
stream(s, p, o)三个参数任一可传null作通配符,实测按主语、谓词、宾语三种单条件过滤全部命中预期。 - stream 与 iterate 输出顺序不保证:simple 实现内部是
LinkedHashSet,遍历顺序与插入顺序有关但不应依赖(实测iterate()段的顺序与stream(alice, null, null)段不同)。
六、Dataset 与 Quad 详解
本节实测命名图数据集。运行 dataset 组(实测回显):
cd /root/rdf-demo && java -Dfile.encoding=UTF-8 -Ddata.dir=/root/rdf-data -cp target/classes:$(cat cp.txt) demo.RdfDemo dataset
== Dataset 与 Quad ==
dataset size=3
默认图 size=1
命名图 friends size=2
-- getGraphNames() --
graphName=_:3530681a-1241-3f72-856e-cfc3a8443689
-- 全量 Quad 流(含图名) --
G=_:3530681a-1241-3f72-856e-cfc3a8443689 <http://example.org/people/alice> <http://xmlns.com/foaf/0.1/knows> <http://example.org/people/bob>
G=_:3530681a-1241-3f72-856e-cfc3a8443689 <http://example.org/people/alice> <http://xmlns.com/foaf/0.1/name> "Alice"
G=<default> <http://example.org/people/bob> <http://xmlns.com/foaf/0.1/name> "Bob"
-- 按命名图过滤 stream(Optional.of(ng), null, null, null) --
命名图 friends 内 Quad 数=2
-- 按默认图过滤 stream(Optional.empty(), null, null, null) --
默认图内 Quad 数=1
-- Quad.asTriple() 丢弃图名 --
Quad G=_:3530681a-1241-3f72-856e-cfc3a8443689 -> Triple: <http://example.org/people/alice> <http://xmlns.com/foaf/0.1/knows> <http://example.org/people/bob>
要点解读(全部来自实测回显):
- 默认图与命名图:
createQuad(null, s, p, o)图名传null表示默认图;ds.getGraph()取默认图、ds.getGraph(ng)按Optional<Graph>取命名图。 - getGraphNames 只列命名图:实测仅输出一条
_:3530681a-1241-3f72-856e-cfc3a8443689,不包含默认图条目——默认图恒存在,通过getGraph()直接访问。 - 图名本身可以是空白节点:本文用
createBlankNode("friends")做图名,序列化时同样显示为 UUID 标签,而非friends。 - 图名过滤:
stream(Optional.of(ngFriends), null, null, null)过滤命名图、stream(Optional.empty(), null, null, null)过滤默认图,实测计数分别为 2 与 1。 - Quad.asTriple:把四元组降维成三元组(丢掉图名),实测首个 Quad 转换正确。
💡 技术拓展:Graph 与 Dataset 的关系——
维度 Graph Dataset 组成单位 Triple(三元组) Quad(三元组 + 图名) 图的数量 单一 默认图 + 若干命名图 对应格式 Turtle、N-Triples TriG、N-Quads 典型用途 描述一个实体域 多数据源隔离、版本快照 本文实测用 simple 实现同时覆盖两者;解析侧的 TriG/N-Quads 需要相应语法支持。
七、BlankNode 唯一性与图合并实测
空白节点的「唯一性」是 RDF 最容易踩坑的概念:它只在工厂实例(会话)范围内有意义。本节三组实测给出完整结论。
7.1 唯一性语义实测
运行 blanknode 组(实测回显):
cd /root/rdf-demo && java -Dfile.encoding=UTF-8 -Ddata.dir=/root/rdf-data -cp target/classes:$(cat cp.txt) demo.RdfDemo blanknode
== BlankNode 唯一性与同图合并 ==
同一工厂两次匿名创建 equals=false
a1 uniqueReference=2b915892-ee18-3c00-b591-bcaca22911ce
a2 uniqueReference=2ff6b7dd-c6ee-38e0-89ba-5204f802621c
跨工厂同标签 equals=false
l1 uniqueReference=4ad8e1c9-eba9-39c6-957f-5ed9e040275f
l2 uniqueReference=86d5e074-19ed-3365-8bbf-375e9da8a0d6
-- 同一工厂下合并两个图(空白节点保持连接) --
合并后 g1 size=2
经空白节点连接到: _:4fdb90e5-5315-32c2-a7da-b85b86f31776 <http://xmlns.com/foaf/0.1/name> "合并测试"
同工厂合并后空白节点仍连接=true
-- 跨工厂合并(空白节点语义断开) --
合并后 ga size=2
跨工厂合并后空白节点仍连接=false(false 表示两图空白节点互不相干)
补充实测——同一工厂、同一标签两次创建(独立小工具 BnCheck,实测回显):
同工厂同标签 equals=true
x1=596cef19-c6b8-3167-8f28-4b82026b29c4
x2=596cef19-c6b8-3167-8f28-4b82026b29c4
结论汇总表(全部实测):
| 场景 | equals | uniqueReference |
|---|---|---|
| 同一工厂、两次匿名创建 | false | 各自随机 UUID |
| 同一工厂、同标签两次创建 | true | 同一 UUID(工厂内按标签缓存复用) |
| 不同工厂、同标签 | false | 各自随机 UUID,互不相干 |
7.2 同工厂图合并实测
回显第 7~9 行:两个图由同一工厂创建,图 2 中引用工厂创建的 topic 空白节点;把图 2 的三元组逐条 add 进图 1 后,size=2 且通过空白节点找到了连接链(doc1 → primaryTopic → topic → name → "合并测试"),linked=true。同一工厂内的空白节点合并天然保持连接,因为相等性判断落在同一个 UUID 上。
7.3 跨工厂图合并实测
回显第 10~13 行:图 A 来自工厂 A、图 B 来自工厂 B,两边都用标签 n1 创建空白节点;合并后 size=2 但 linked=false——两图中的 _:n1 各自持有不同 UUID,合并后互不相干,形成两个孤立的空白节点。这正是 RDF 语义中「空白节点标签仅在文档范围内有意义」的体现:跨数据源合并时,若两源使用不同实现/工厂,同名标签不代表同一实体;业务上需要合并时,应保证合并动作发生在同一个工厂实例范围内(或用 IRI 显式标识实体)。
八、跨实现互操作实测
API 门面价值的直接证据:不同绑定实现的术语可以互相比较、混用。运行 crossimpl 组(实测回显):
cd /root/rdf-demo && java -Dfile.encoding=UTF-8 -Ddata.dir=/root/rdf-data -cp target/classes:$(cat cp.txt) demo.RdfDemo crossimpl
== 跨实现术语等价性(simple 与 jena 绑定) ==
Literal equals(simple,jena)=true, (jena,simple)=true
IRI equals(simple,jena)=true, (jena,simple)=true
Jena Triple 写入 Simple Graph 后 size=1
Simple 术语 contains=true
要点解读:
- 双向 equals 全真:simple 工厂与 jena 工厂创建的同内容
Literal、IRI相互equals均为true。Commons RDF 规范要求术语相等性基于 RDF 项语义而非实现类,各绑定都遵守。 - 跨实现混用可用:Jena 术语直接写入 Simple 图(
g.add(j.createTriple(ij, j.createIRI("http://xmlns.com/foaf/0.1/name"), lj))),再用 Simple 术语contains命中(true)。业务代码可以做到「只依赖 api 包编译,运行时自由切换/混用实现」。
💡 技术拓展:门面模式的这一层保障有边界——跨实现混用要求两个实现都在 classpath 上,且复杂操作(解析、存储)仍要回到对应绑定的专属入口。八/九两篇的解析序列化实验将展示这种「接口对接、能力借力」的协作方式。
九、常见问题(FAQ)
Q1:为什么我创建的空白节点标签和文档回显不一样? 空白节点标签由实现生成,simple 实现用随机 UUID,每次运行都不同(本文各段回显中的标签也互不相同,属正常现象)。依赖稳定标签请改用 IRI。
Q2:createBlankNode("friends") 的标签去哪了?为什么序列化输出还是 UUID?
实测 simple 实现不把标识参数用作序列化标签:标签只用于工厂实例内的相等性判断(同工厂同标签返回同一节点),ntriplesString() 恒为随机 UUID。这与规范一致——空白节点标签不属于数据模型。
Q3:stream() 用完要关闭吗?iterate() 呢?
stream() 返回 Stream,必须用 try-with-resources 关闭(本文代码均如此);iterate() 返回 Iterable,用于 for-each,遍历过程中修改图可能抛 ConcurrentModificationException,需要边遍历边删时先 collect 快照。
Q4:向图中重复加入同一三元组会怎样?
图是三元组集合,重复 add 幂等,size 不变。实测以 contains 与 size 验证(5 节回显)。
Q5:为什么 createQuad(null, subject, predicate, object) 能表示默认图,但 getGraphNames() 不返回 null 条目?
规范允许图名传 null 表默认图;simple 实现的 getGraphNames() 只枚举命名图(实测回显只有一条命名图),默认图通过 getGraph() 直接访问。两套访问路径不要混用。
十、参考链接
- Commons RDF 0.5.0 API Javadoc:https://commons.apache.org/proper/commons-rdf/javadocs/api-0.5.0/
- Commons RDF 用户指南:https://commons.apache.org/proper/commons-rdf/userguide.html
- RDF 1.1 Concepts and Abstract Syntax:https://www.w3.org/TR/rdf11-concepts/
- RDF 1.1 N-Triples(术语规范串格式):https://www.w3.org/TR/n-triples/
- Maven 坐标页(simple):https://repo1.maven.org/maven2/org/apache/commons/commons-rdf-simple/
- FOAF 词表:http://xmlns.com/foaf/0.1/