Apache Jena SPARQL 查询完全指南:入门与实战案例
Apache Jena SPARQL 查询完全指南:入门与实战案例
在本教程中,我们将详细介绍如何使用 Apache Jena 框架来执行 SPARQL 查询。SPARQL 是从 RDF 数据中检索和处理信息的标准语言,而 Apache Jena 则是 Java 中构建语义网和知识图谱应用的首选工具包。
本教程将带你从 SPARQL 的基础语法开始,逐步学习如何在 Jena 中创建数据模型、加载数据,并执行从简单到复杂的各种查询,包括 FILTER、OPTIONAL、ASK 和 CONSTRUCT。我们还将特别介绍 ResultSetFormatter 工具类的便捷用途。
文章目录
- Apache Jena SPARQL 查询完全指南:入门与实战案例
- 1\. 什么是 SPARQL?
- 2\. 准备工作
- 3\. SPARQL "Hello World":在 Jena 中执行查询
- 3.1. 创建模型并加载数据
- 3.2. SPARQL 查询语法基础
- 3.2.1. 深入理解 `WHERE` 子句:图模式匹配
- 变量命名差异:`?` vs `$`
- 详细解释
- 1. 规范和兼容性
- 2. 惯例和可读性
- 3.2.2. `WHERE` 子句中的关键字和语法分解
- 3.2.3. 其他重要语法:`;` (分号)
- 3.3. 执行查询并处理结果
- 3.3. 执行查询并处理结果
- 3.4. `ResultSetFormatter` 详解
- 4\. SPARQL 查询实战案例
- 4.1. 案例 1: SELECT 与 FILTER
- 4.2. 案例 2: OPTIONAL
- 4.3. 案例 3: ASK
- 4.4. 案例 4: CONSTRUCT
- 5\. 序列化和反序列化
- 5.1. 序列化 (保存) 模型
- 5.2. 反序列化 (加载) 模型
- 6\. 总结
- 7\. 示例代码完整版
- 8\. 进一步探索
1. 什么是 SPARQL?
SPARQL(SPARQL Protocol and RDF Query Language)是为 RDF(资源描述框架)设计的标准查询语言。它在知识图谱和语义网中的地位,就如同 SQL 在关系数据库中的地位。
SPARQL 允许你:
- 从 RDF 图中查询匹配特定模式的数据。
- 检索未知的数据(例如,“找出所有…的人”)。
- 从异构的、分布式的 RDF 数据源中组合数据。
- 转换数据,从现有的 RDF 数据构造出新的 RDF 图。
2. 准备工作
在开始之前,确保你的项目中已经添加了 Apache Jena 的依赖。如果你使用的是 Maven,可以在 pom.xml 文件中添加以下依赖项。
推荐使用 apache-jena-libs,这是一个聚合了所有核心 Jena 模块(包括 core、arq 等)的 POM:
<dependency><groupId>org.apache.jena</groupId><artifactId>apache-jena-libs</artifactId><type>pom</type><version>4.10.0</version>
</dependency>
如果你更喜欢单独添加,你至少需要 jena-core(用于模型)和 jena-arq(用于 SPARQL 查询引擎):
3. SPARQL “Hello World”:在 Jena 中执行查询
让我们从一个最简单的例子开始:创建一个内存模型(Model),加载一些数据,然后执行一个 SPARQL 查询。
3.1. 创建模型并加载数据
首先,我们创建一个 Model 并使用 Turtle (TTL) 语法加载一些示例 RDF 数据。
import org.apache.jena.rdf.model.Model;
import org.apache.jena.rdf.model.ModelFactory;
import java.io.StringReader;// 1. 创建一个内存模型
Model model = ModelFactory.createDefaultModel();// 2. 准备一些 Turtle 格式的 RDF 数据
String rdfData = "@prefix vcard: <http://www.w3.org/2001/vcard-rdf/3.0#> ."+ "@prefix foaf: <http://xmlns.com/foaf/0.1/> ."+ ""+ "<http://example.org/person/1>"+ " a foaf:Person ;"+ " foaf:name \"John Doe\" ;"+ " vcard:FN \"John D.\" ;"+ " vcard:hasEmail <mailto:john.doe@example.com> ."+ ""+ "<http://example.org/person/2>"+ " a foaf:Person ;"+ " foaf:name \"Jane Smith\" ;"+ " vcard:FN \"Jane S.\" ." // Jane 没有 Email;// 3. 将数据读入模型
model.read(new StringReader(rdfData), null, "TURTLE");
你提出了一个非常棒的观点!
3.2. 这一节确实是 SPARQL 的核心,值得我们深入讲解。仅仅展示语法是不够的,理解为什么这么写以及它的工作原理才是关键。
我将按照你的要求,使用 Mermaid 图来可视化这个概念,并详细分解 WHERE 子句中“图模式匹配”的每一个元素和关键字。
这是为你重写和扩展的 3.2. 和 3.3. 部分。
3.2. SPARQL 查询语法基础
一个典型的 SPARQL SELECT 查询结构如下:
# 1. 前缀 (Prefixes) - 简化 URI
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX vcard: <http://www.w3.org/2001/vcard-rdf/3.0#># 2. 查询类型 (Query Type) - 我们想要获取表格数据
SELECT ?person ?name
# 3. 图模式 (Graph Pattern) - 我们要匹配的数据
WHERE {?person a foaf:Person .?person foaf:name ?name .
}
# 4. 修饰符 (Modifiers) - 可选
ORDER BY ?name
LIMIT 10
3.2.1. 深入理解 WHERE 子句:图模式匹配
SPARQL 的核心思想是图模式匹配 (Graph Pattern Matching)。
- 你的 RDF 数据集是一个(可能非常庞大的)数据图 (Data Graph)。
- 你的
WHERE子句定义了一个查询图 (Query Graph),也称为“图模式”。这个模式中可以包含变量(以?或$开头)。
变量命名差异:? vs $
| 前缀 | 常见用途/惯例 | 是否为规范要求? |
|---|---|---|
? (问号) | 标准、最常用。用于大多数 SELECT, CONSTRUCT, DESCRIBE 查询中的普通变量。 | 是(规范推荐) |
$ (美元符号) | 不常见,但在某些实现中(如 Jena ARQ)可以被接受为变量前缀。它在某些数据库查询语言(如 MongoDB 的聚合框架)中更常见。 | 否(通常被接受,但不像 ? 那样是通用惯例) |
详细解释
1. 规范和兼容性
根据 SPARQL 1.1 规范的定义,一个变量必须以一个**词头(PREFIX)**开头,这个词头要么是 ? 符号,要么是 $ 符号,后跟一个合法的 VarName(变量名)。
- 因此,在技术上,两者都是有效的变量前缀。
2. 惯例和可读性
尽管两者都有效,但在社区和文档中:
-
?变量(例如?person,?name)是 绝对的主流。几乎所有关于 SPARQL 的教程、标准文档和商业产品都使用?作为变量前缀。使用它能确保你的查询具有最高的可读性和兼容性。 -
$变量(例如$person,$name)则很少见。如果你在一个系统(如 Jena)中使用了$变量,它通常可以工作,但如果把查询迁移到另一个完全遵循严格 SPARQL 惯例的系统上,可能会导致解析错误或可读性问题。
SPARQL 引擎的工作就是:在庞大的“数据图”中,寻找所有能够匹配你的“查询图”结构的子图。
可视化理解
让我们用 Mermaid 图来展示这个过程。
首先,这是我们加载到 model 中的数据图(Data Graph):
然后,这是我们 WHERE 子句中定义的查询模式 (Query Pattern):
匹配过程:
引擎会尝试将 ?person 和 ?name 这两个“通配符”绑定到数据图中的实际节点,以使查询模式的结构与数据图的某个子图完全重合。
-
第一次匹配:
- 引擎尝试将
?person绑定到person/1。 - 检查模式 1:
?person a foaf:Person是否成立?- 即
person/1 a foaf:Person是否在数据图中?是的。
- 即
- 检查模式 2:
?person foaf:name ?name是否成立?- 即
person/1 foaf:name ?name是否在数据图中?是的,并且?name被绑定到"John Doe"。
- 即
- 结果: 找到一个解!
?person = person/1,?name = "John Doe"。
- 引擎尝试将
-
第二次匹配:
- 引擎尝试将
?person绑定到person/2。 - 检查模式 1:
?person a foaf:Person是否成立?- 即
person/2 a foaf:Person是否在数据图中?是的。
- 即
- 检查模式 2:
?person foaf:name ?name是否成立?- 即
person/2 foaf:name ?name是否在数据图中?是的,并且?name被绑定到"Jane Smith"。
- 即
- 结果: 找到第二个解!
?person = person/2,?name = "Jane Smith"。
- 引擎尝试将
-
其他尝试:
- 引擎尝试将
?person绑定到"John Doe"。 - 检查模式 1:
"John Doe" a foaf:Person是否成立?否。匹配失败。
- 引擎尝试将
这就是 SELECT 语句最终返回这两行结果的原因。
3.2.2. WHERE 子句中的关键字和语法分解
WHERE 子句由一个或多个三元组模式 (Triple Patterns) 组成。
一个三元组模式就是 主语 谓语 宾语 (Subject Predicate Object) 的结构。
1. 约束 ?person 的类型
?person a foaf:Person .
?person:这是一个变量 (Variable)。它充当主语 (Subject)。因为它是一个变量,所以我们是在“寻找”匹配它的东西。a:这是一个 SPARQL 关键字,它是rdf:type这个特殊 URI 的缩写。rdf:type是 RDF 中用于“指定类型”的标准谓语。foaf:Person:这是一个常量 (Constant) / URI。它充当宾语 (Object)。引擎必须精确匹配这个 URI。.(句点):这是一个语句分隔符。它表示这个三元组模式结束了。
所以,这行代码的字面意思是:“找到数据图中的任何一个主语(我们将其命名为 ?person),这个主语必须有一条 rdf:type 属性,且该属性的值必须是 foaf:Person。”
2. 约束 ?person 的属性并绑定 ?name
?person foaf:name ?name .
?person:这是同一个变量。这是最关键的一点!通过在多个三元组模式中使用相同的变量名,你就创建了一个隐式的“连接”(JOIN) 或 “AND” 条件。foaf:name:这是一个常量 (Constant) / URI。它充当谓语 (Predicate)。引擎必须精确匹配这个属性。?name:这是一个新的变量 (Variable)。它充当宾语 (Object)。我们不关心宾语具体是什么,我们只想“获取”它。
所以,这行代码的字面意思是:“对于刚刚匹配到的那个 ?person,它还必须(AND)有一条属性是 foaf:name。如果找到了,请把这条属性的值(宾语)取出来,并将其赋值给我们命名的 ?name 变量。”
3.2.3. 其他重要语法:; (分号)
在 SPARQL 中,如果多个三元组模式共享同一个主语 (Subject),你可以使用分号 (;) 来简化查询。
我们上面的查询:
WHERE {?person a foaf:Person .?person foaf:name ?name .
}
可以被等价地重写为:
WHERE {?person a foaf:Person ; # 注意这里是分号foaf:name ?name . # 这里是句点,因为是最后一
}
这种写法更简洁,可读性更强,尤其是在一个主语有非常多属性需要匹配时。
3.3. 执行查询并处理结果
执行 SELECT 查询后,你会得到一个 ResultSet 对象。Jena 提供了两种主要方式来处理它:
import org.apache.jena.query.*;
import org.apache.jena.rdf.model.Literal;
import org.apache.jena.rdf.model.Resource;// 4. 编写 SPARQL 查询字符串
String queryString = "PREFIX foaf: <http://xmlns.com/foaf/0.1/>"+ "SELECT ?person ?name "+ "WHERE {"// 我们使用 ; 语法糖来让查询更简洁+ " ?person a foaf:Person ;" + " foaf:name ?name ."+ "}";// 5. 创建查询对象
Query query = QueryFactory.create(query);// 6. 执行查询 (使用 try-with-resources 自动关闭)
try (QueryExecution qexec = QueryExecutionFactory.create(query, model)) {ResultSet results = qexec.execSelect();// --- 方式一: 使用 ResultSetFormatter (推荐用于调试和快速展示) ---System.out.println("--- 方式一:使用 ResultSetFormatter ---");// ResultSetFormatter 是一个工具类,用于将结果集格式化为多种输出// .out() 是一个便捷方法,可以直接将其以漂亮的表格形式打印到 System.outResultSetFormatter.out(System.out, results, query);
}// 7. 演示方式二(必须重新执行查询)
// **重要提示:** ResultSet 只能被迭代一次。
// 在上面的 .out() 方法消费了结果集后,它就不能再被使用了。
// 我们必须重新创建 QueryExecution 和 ResultSet 来进行手动迭代。try (QueryExecution qexec = QueryExecutionFactory.create(query, model)) {ResultSet results = qexec.execSelect();// --- 方式二: 手动迭代 (在应用程序中处理数据的标准方式) ---System.out.println("\n--- 方式二:手动迭代结果 ---");while (results.hasNext()) {// 获取一个查询解 (QuerySolution),代表一行结果QuerySolution soln = results.nextSolution();// 根据变量名 "?person" 获取资源 (Resource)Resource person = soln.getResource("person"); // 根据变量名 "?name" 获取字面量 (Literal)Literal name = soln.getLiteral("name"); // 打印结果// .getLocalName() 用于获取 URI 中 # 或 / 之后的部分System.out.println("Person: " + person.getLocalName() + ", Name: " + name.getString());}
}
预期输出:
--- Sposób pierwszy: Użycie ResultSetFormatter ---
---------------------------------------------
| person | name |
=============================================
| <http://example.org/person/1> | "John Doe" |
| <http://example.org/person/2> | "Jane Smith" |
------------------------------------------------ Sposób drugi: Ręczna iteracja po wynikach ---
Person: person/1, Name: John Doe
Person: person/2, Name: Jane Smith
3.3. 执行查询并处理结果
执行 SELECT 查询后,你会得到一个 ResultSet 对象。Jena 提供了两种主要方式来处理它:
import org.apache.jena.query.*;
import org.apache.jena.rdf.model.Literal;
import org.apache.jena.rdf.model.Resource;// 4. 编写 SPARQL 查询字符串
String queryString = "PREFIX foaf: <http://xmlns.com/foaf/0.1/>"+ "SELECT ?person ?name "+ "WHERE {"+ " ?person a foaf:Person ."+ " ?person foaf:name ?name ."+ "}";// 5. 创建查询对象
Query query = QueryFactory.create(queryString);// 6. 执行查询 (使用 try-with-resources 自动关闭)
try (QueryExecution qexec = QueryExecutionFactory.create(query, model)) {ResultSet results = qexec.execSelect();// --- 方式一: 使用 ResultSetFormatter (推荐用于调试和快速展示) ---System.out.println("--- 方式一:使用 ResultSetFormatter ---");// ResultSetFormatter 是一个工具类,用于将结果集格式化为多种输出// .out() 是一个便捷方法,可以直接将其以漂亮的表格形式打印到 System.outResultSetFormatter.out(System.out, results, query);
}// 7. 演示方式二(必须重新执行查询)
// **重要提示:** ResultSet 只能被迭代一次。
// 在上面的 .out() 方法消费了结果集后,它就不能再被使用了。
// 我们必须重新创建 QueryExecution 和 ResultSet 来进行手动迭代。try (QueryExecution qexec = QueryExecutionFactory.create(query, model)) {ResultSet results = qexec.execSelect();// --- 方式二: 手动迭代 (在应用程序中处理数据的标准方式) ---System.out.println("\n--- 方式二:手动迭代结果 ---");while (results.hasNext()) {// 获取一个查询解 (QuerySolution),代表一行结果QuerySolution soln = results.nextSolution();// 根据变量名 "?person" 获取资源 (Resource)Resource person = soln.getResource("person"); // 根据变量名 "?name" 获取字面量 (Literal)Literal name = soln.getLiteral("name"); // 打印结果System.out.println("Person: " + person.getLocalName() + ", Name: " + name.getString());}
}
预期输出:
--- 方式一:使用 ResultSetFormatter ---
---------------------------------------------
| person | name |
=============================================
| <http://example.org/person/1> | "John Doe" |
| <http://example.org/person/2> | "Jane Smith" |
------------------------------------------------ 方式二:手动迭代结果 ---
Person: person/1, Name: John Doe
Person: person/2, Name: Jane Smith
3.4. ResultSetFormatter 详解
ResultSetFormatter 是 org.apache.jena.query 包中的一个实用类,专门用于将 ResultSet 转换为人类可读的字符串或写入到输出流。
它的主要用途是:
- 调试:在开发过程中快速查看 SPARQL 查询是否返回了预期的数据。
- 简单输出:在命令行工具或简单应用中直接展示结果。
- 格式转换:它不仅能输出表格,还可以将结果集序列化为 XML、JSON、CSV 或 TSV 格式。
例如,将结果集转换为 JSON 字符串:
// 假设 'results' 是一个有效的 ResultSet
// ByteArrayOutputStream out = new ByteArrayOutputStream();
// ResultSetFormatter.outputAsJSON(out, results);
// String jsonResults = out.toString();
// System.out.println(jsonResults);
4. SPARQL 查询实战案例
为了演示更高级的查询,我们使用一个稍有不同的数据集,包含年龄信息。
import org.apache.jena.rdf.model.RDFNode; // 确保导入 RDFNode// 准备工作:创建一个共享模型
Model dataModel = ModelFactory.createDefaultModel();
String data = "@prefix vcard: <http://www.w3.org/2001/vcard-rdf/3.0#> ."+ "@prefix foaf: <http://xmlns.com/foaf/0.1/> ."+ "@prefix ex: <http://example.org/ns#> ."+ ""+ "ex:person1 a foaf:Person ;"+ " foaf:name \"John Doe\" ;"+ " ex:age 30 ;" // 自定义一个年龄+ " vcard:hasEmail <mailto:john.doe@example.com> ."+ ""+ "ex:person2 a foaf:Person ;"+ " foaf:name \"Jane Smith\" ;"+ " ex:age 25 ." // Jane 没有 Email;
dataModel.read(new StringReader(data), null, "TURTLE");
4.1. 案例 1: SELECT 与 FILTER
目标: 查找所有年龄(ex:age)大于 28 岁的人。
Java 执行代码 (使用 ResultSetFormatter):
String queryFilter = "PREFIX foaf: <http://xmlns.com/foaf/0.1/> "+ "PREFIX ex: <http://example.org/ns#> "+ "SELECT ?name ?age "+ "WHERE { ?person a foaf:Person ; foaf:name ?name ; ex:age ?age . FILTER (?age > 28) }";try (QueryExecution qexec = QueryExecutionFactory.create(queryFilter, dataModel)) {ResultSet rs = qexec.execSelect();System.out.println("\n--- 案例 1: 年龄大于 28 的人 (Formatter) ---");// 对于简单的 SELECT,Formatter 非常清晰ResultSetFormatter.out(System.out, rs);
}
输出:
--- 案例 1: 年龄大于 28 的人 (Formatter) ---
----------------------
| name | age |
======================
| "John Doe" | 30 |
----------------------
4.2. 案例 2: OPTIONAL
目标: 查找所有人的名字和他们 可选的 Email。
Java 执行代码 (使用手动迭代):
String queryOptional = "PREFIX foaf: <http://xmlns.com/foaf/0.1/> "+ "PREFIX vcard: <http://www.w3.org/2001/vcard-rdf/3.0#> "+ "SELECT ?name ?email "+ "WHERE { ?person a foaf:Person ; foaf:name ?name . OPTIONAL { ?person vcard:hasEmail ?email . } }";try (QueryExecution qexec = QueryExecutionFactory.create(queryOptional, dataModel)) {ResultSet rs = qexec.execSelect();System.out.println("\n--- 案例 2: 名字和可选的 Email (手动迭代) ---");// 注意:ResultSetFormatter 会为未绑定的变量打印空值。// 手动迭代让我们有能力自定义这种行为 (例如打印 "N/A")。while(rs.hasNext()) {QuerySolution soln = rs.nextSolution();Literal name = soln.getLiteral("name");// **处理可选变量 (OPTIONAL)**// 必须检查变量是否存在,否则 .getResource() 或 .getLiteral() 会在未绑定时抛出异常RDFNode emailNode = soln.get("email"); String email = "N/A"; // 默认值if (emailNode != null) {email = emailNode.toString(); // .toString() 对 Resource 和 Literal 都有效}System.out.println("Name: " + name.getString() + ", Email: " + email);}
}
输出:
--- 案例 2: 名字和可选的 Email (手动迭代) ---
Name: John Doe, Email: mailto:john.doe@example.com
Name: Jane Smith, Email: N/A
4.3. 案例 3: ASK
目标: 询问“是否存在一个叫 ‘Jane Smith’ 的人?” (返回布尔值 true/false)。
ASK 查询最简单,它不返回 ResultSet,而是直接返回一个布尔值。
Java 执行代码:
String queryAsk = "PREFIX foaf: <http://xmlns.com/foaf/0.1/> "+ "ASK { ?person foaf:name \"Jane Smith\" . }";try (QueryExecution qexec = QueryExecutionFactory.create(queryAsk, dataModel)) {boolean result = qexec.execAsk();System.out.println("\n--- 案例 3: 是否存在 'Jane Smith'? ---");System.out.println("查询结果: " + result);
}
输出:
--- 案例 3: 是否存在 'Jane Smith'? ---
查询结果: true
4.4. 案例 4: CONSTRUCT
目标: 构造一个 新的 RDF 图,只包含所有人(foaf:Person)和他们的名字(foaf:name)。
CONSTRUCT 查询返回一个新的 Model,而不是 ResultSet。
Java 执行代码:
String queryConstruct = "PREFIX foaf: <http://xmlns.com/foaf/0.1/> "+ "CONSTRUCT { ?person a foaf:Person . ?person foaf:name ?name . } "+ "WHERE { ?person a foaf:Person ; foaf:name ?name . }";try (QueryExecution qexec = QueryExecutionFactory.create(queryConstruct, dataModel)) {// CONSTRUCT 查询返回一个新的 ModelModel constructedModel = qexec.execConstruct();System.out.println("\n--- 案例 4: 构造只含人名的新图 ---");// 我们可以使用 Jena 的 .write() 方法打印这个新模型的内容constructedModel.write(System.out, "TURTLE");
}
输出:
--- 案例 4: 构造只含人名的新图 ---
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix ex: <http://example.org/ns#> .ex:person1 a foaf:Person ;foaf:name "John Doe" .ex:person2 a foaf:Person ;foaf:name "Jane Smith" .
5. 序列化和反序列化
为了持久化你的知识图谱(Model),你可以将其序列化为 RDF 文件。
5.1. 序列化 (保存) 模型
import org.apache.jena.riot.RDFDataMgr;
import org.apache.jena.riot.Lang;
import java.io.FileOutputStream;
import java.io.OutputStream;// ... 假设 'dataModel' 已经创建并填充了数据 ...
try (OutputStream out = new FileOutputStream("knowledge_graph.ttl")) {RDFDataMgr.write(out, dataModel, Lang.TURTLE);System.out.println("\n--- 5.1: 模型已保存到 knowledge_graph.ttl ---");
} catch (Exception e) {e.printStackTrace();
}
5.2. 反序列化 (加载) 模型
Model loadedModel = ModelFactory.createDefaultModel();
RDFDataMgr.read(loadedModel, "knowledge_graph.ttl");
System.out.println("\n--- 5.2: 模型已从文件加载 ---");
loadedModel.write(System.out, "TURTLE");
6. 总结
通过本教程,我们学习了在 Apache Jena 中使用 SPARQL 的核心流程:
- 准备
Model:使用ModelFactory.createDefaultModel()创建模型并加载数据。 - 创建
Query:使用QueryFactory.create()将 SPARQL 字符串解析为Query对象。 - 创建
QueryExecution:使用QueryExecutionFactory.create(query, model)将查询和数据模型绑定。 - 执行查询:
qexec.execSelect()用于SELECT(返回ResultSet)。qexec.execConstruct()用于CONSTRUCT(返回Model)。qexec.execAsk()用于ASK(返回boolean)。
- 处理结果:
- 快速调试:使用
ResultSetFormatter.out(System.out, results)快速打印表格。 - 应用逻辑:使用
while(results.hasNext())和QuerySolution对象来手动迭代,这种方式更灵活,特别是处理OPTIONAL变量时(需使用soln.get("varName")检查null)。
- 快速调试:使用
- 关闭资源:始终使用
try-with-resources语句或手动调用qexec.close()来释放资源。
ResultSetFormatter 是一个出色的调试工具,而手动迭代是构建健壮应用程序的基础。
7. 示例代码完整版
以下是本教程中所有案例的完整、可运行的 Java 类:
import org.apache.jena.rdf.model.*;
import org.apache.jena.query.*;
import org.apache.jena.riot.RDFDataMgr;
import org.apache.jena.riot.Lang;import java.io.StringReader;
import java.io.FileOutputStream;
import java.io.OutputStream;public class JenaSparqlGuideFull {// 创建一个共享的示例模型private static Model createSampleModel() {Model model = ModelFactory.createDefaultModel();String data = "@prefix vcard: <http://www.w3.org/2001/vcard-rdf/3.0#> ."+ "@prefix foaf: <http://xmlns.com/foaf/0.1/> ."+ "@prefix ex: <http://example.org/ns#> ."+ ""+ "ex:person1 a foaf:Person ;"+ " foaf:name \"John Doe\" ;"+ " ex:age 30 ;"+ " vcard:hasEmail <mailto:john.doe@example.com> ."+ ""+ "ex:person2 a foaf:Person ;"+ " foaf:name \"Jane Smith\" ;"+ " ex:age 25 .";model.read(new StringReader(data), null, "TURTLE");return model;}public static void main(String[] args) {Model dataModel = createSampleModel();// --- 案例 1: SELECT 与 FILTER (使用 ResultSetFormatter) ---System.out.println("--- 案例 1: 年龄大于 28 的人 ---");String queryFilter = "PREFIX foaf: <http://xmlns.com/foaf/0.1/> "+ "PREFIX ex: <http://example.org/ns#> "+ "SELECT ?name ?age "+ "WHERE { ?person a foaf:Person ; foaf:name ?name ; ex:age ?age . FILTER (?age > 28) }";try (QueryExecution qexec = QueryExecutionFactory.create(queryFilter, dataModel)) {ResultSet rs = qexec.execSelect();// 使用 Formatter 快速打印ResultSetFormatter.out(System.out, rs);}// --- 案例 2: OPTIONAL (使用手动迭代) ---System.out.println("\n--- 案例 2: 名字和可选的 Email ---");System.out.println("(使用手动迭代处理 'N/A')");String queryOptional = "PREFIX foaf: <http://xmlns.com/foaf/0.1/> "+ "PREFIX vcard: <http://www.w3.org/2001/vcard-rdf/3.0#> "+ "SELECT ?name ?email "+ "WHERE { ?person a foaf:Person ; foaf:name ?name . OPTIONAL { ?person vcard:hasEmail ?email . } }";try (QueryExecution qexec = QueryExecutionFactory.create(queryOptional, dataModel)) {ResultSet rs = qexec.execSelect();while(rs.hasNext()) {QuerySolution soln = rs.nextSolution();Literal name = soln.getLiteral("name");RDFNode emailNode = soln.get("email"); String email = (emailNode != null) ? emailNode.toString() : "N/A";System.out.println("Name: " + name.getString() + ", Email: " + email);}}// --- 案例 3: ASK ---System.out.println("\n--- 案例 3: 是否存在 'Jane Smith'? ---");String queryAsk = "PREFIX foaf: <http://xmlns.com/foaf/0.1/> "+ "ASK { ?person foaf:name \"Jane Smith\" . }";try (QueryExecution qexec = QueryExecutionFactory.create(queryAsk, dataModel)) {boolean result = qexec.execAsk();System.out.println("查询结果: " + result);}// --- 案例 4: CONSTRUCT ---System.out.println("\n--- 案例 4: 构造只含人名的新图 ---");String queryConstruct = "PREFIX foaf: <http://xmlns.com/foaf/0.1/> "+ "CONSTRUCT { ?person a foaf:Person . ?person foaf:name ?name . } "+ "WHERE { ?person a foaf:Person ; foaf:name ?name . }";try (QueryExecution qexec = QueryExecutionFactory.create(queryConstruct, dataModel)) {Model constructedModel = qexec.execConstruct();constructedModel.write(System.out, "TURTLE");}// --- 案例 5: 序列化 ---System.out.println("\n--- 5.1: 序列化模型... ---");try (OutputStream out = new FileOutputStream("knowledge_graph.ttl")) {RDFDataMgr.write(out, dataModel, Lang.TURTLE);System.out.println("模型已保存到 knowledge_graph.ttl");} catch (Exception e) {e.printStackTrace();}// --- 案例 6: 反序列化 ---System.out.println("\n--- 5.2: 从文件反序列化模型... ---");Model loadedModel = ModelFactory.createDefaultModel();try {RDFDataMgr.read(loadedModel, "knowledge_graph.ttl");System.out.println("模型加载成功,包含 " + loadedModel.size() + " 条三元组。");} catch (Exception e) {System.out.println("加载模型失败: " + e.getMessage());}}
}
8. 进一步探索
现在你已经掌握了 SPARQL 查询和两种结果处理方式,你可以进一步探索:
- 聚合查询:
GROUP BY,COUNT,SUM,AVG等。 - 属性路径 (Property Paths): 更灵活地查询关系,例如
ex:inherits*(查询任意层级的继承)。 - SPARQL
UPDATE: 在 Jena 中执行UpdateRequest来动态修改、插入或删除 RDF 数据。 - Jena 推理: 结合 RDFS 或 OWL 推理机 (
InfModel),查询推理得出的隐式知识。 - 连接远程端点: 使用
QueryExecutionFactory.sparqlService()查询远程 SPARQL 端点(如 DBpedia)。
