Apache Jena SPARQL 查询完全指南:入门与实战案例
在本教程中,我们将详细介绍如何使用 Apache Jena 框架来执行 SPARQL 查询。SPARQL 是从 RDF 数据中检索和处理信息的标准语言,而 Apache Jena 则是 Java 中构建语义网和知识图谱应用的首选工具包。
本教程将带你从 SPARQL 的基础语法开始,逐步学习如何在 Jena 中创建数据模型、加载数据,并执行从简单到复杂的各种查询,包括 FILTER、OPTIONAL、ASK 和 CONSTRUCT。我们还将特别介绍 ResultSetFormatter 工具类的便捷用途。
文章目录
- [Apache Jena SPARQL 查询完全指南:入门与实战案例](#Apache Jena SPARQL 查询完全指南:入门与实战案例)
-
- [1\. 什么是 SPARQL?](#1. 什么是 SPARQL?)
- [2\. 准备工作](#2. 准备工作)
- [3\. SPARQL "Hello World":在 Jena 中执行查询](#3. SPARQL "Hello World":在 Jena 中执行查询)
-
- [3.1. 创建模型并加载数据](#3.1. 创建模型并加载数据)
- [3.2. SPARQL 查询语法基础](#3.2. SPARQL 查询语法基础)
-
- [3.2.1. 深入理解 `WHERE` 子句:图模式匹配](#3.2.1. 深入理解
WHERE子句:图模式匹配) -
- [变量命名差异:`?` vs `\`](#变量命名差异:`?` vs ``)
- 详细解释
-
- [1. 规范和兼容性](#1. 规范和兼容性)
- [2. 惯例和可读性](#2. 惯例和可读性)
- [3.2.2. `WHERE` 子句中的关键字和语法分解](#3.2.2.
WHERE子句中的关键字和语法分解) - [3.2.3. 其他重要语法:`;` (分号)](#3.2.3. 其他重要语法:
;(分号))
- [3.2.1. 深入理解 `WHERE` 子句:图模式匹配](#3.2.1. 深入理解
- [3.3. 执行查询并处理结果](#3.3. 执行查询并处理结果)
- [3.3. 执行查询并处理结果](#3.3. 执行查询并处理结果)
- [3.4. `ResultSetFormatter` 详解](#3.4.
ResultSetFormatter详解)
- [4\. SPARQL 查询实战案例](#4. SPARQL 查询实战案例)
-
- [4.1. 案例 1: SELECT 与 FILTER](#4.1. 案例 1: SELECT 与 FILTER)
- [4.2. 案例 2: OPTIONAL](#4.2. 案例 2: OPTIONAL)
- [4.3. 案例 3: ASK](#4.3. 案例 3: ASK)
- [4.4. 案例 4: CONSTRUCT](#4.4. 案例 4: CONSTRUCT)
- [5\. 序列化和反序列化](#5. 序列化和反序列化)
-
- [5.1. 序列化 (保存) 模型](#5.1. 序列化 (保存) 模型)
- [5.2. 反序列化 (加载) 模型](#5.2. 反序列化 (加载) 模型)
- [6\. 总结](#6. 总结)
- [7\. 示例代码完整版](#7. 示例代码完整版)
- [8\. 进一步探索](#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:
xml
<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 查询引擎):
xml
3. SPARQL "Hello World":在 Jena 中执行查询
让我们从一个最简单的例子开始:创建一个内存模型(Model),加载一些数据,然后执行一个 SPARQL 查询。
3.1. 创建模型并加载数据
首先,我们创建一个 Model 并使用 Turtle (TTL) 语法加载一些示例 RDF 数据。
java
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 查询结构如下:
sparql
# 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):
我们的 RDF 数据图 a (rdf:type) foaf:name vcard:FN vcard:hasEmail a (rdf:type) foaf:name vcard:FN foaf:Person \John Doe\ \John D.\ \Jane Smith\ \Jane S.\
然后,这是我们 WHERE 子句中定义的查询模式 (Query Pattern):
我们的 SPARQL 查询模式 a (rdf:type) foaf:name foaf:Person ?person ?name
匹配过程:
引擎会尝试将 ?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) ,你可以使用分号 (;) 来简化查询。
我们上面的查询:
sparql
WHERE {
?person a foaf:Person .
?person foaf:name ?name .
}
可以被等价地重写为:
sparql
WHERE {
?person a foaf:Person ; # 注意这里是分号
foaf:name ?name . # 这里是句点,因为是最后一
}
这种写法更简洁,可读性更强,尤其是在一个主语有非常多属性需要匹配时。
3.3. 执行查询并处理结果
执行 SELECT 查询后,你会得到一个 ResultSet 对象。Jena 提供了两种主要方式来处理它:
java
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.out
ResultSetFormatter.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 提供了两种主要方式来处理它:
java
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.out
ResultSetFormatter.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 字符串:
java
// 假设 'results' 是一个有效的 ResultSet
// ByteArrayOutputStream out = new ByteArrayOutputStream();
// ResultSetFormatter.outputAsJSON(out, results);
// String jsonResults = out.toString();
// System.out.println(jsonResults);
4. SPARQL 查询实战案例
为了演示更高级的查询,我们使用一个稍有不同的数据集,包含年龄信息。
java
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):
java
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 执行代码 (使用手动迭代):
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 执行代码:
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 执行代码:
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 查询返回一个新的 Model
Model 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. 序列化 (保存) 模型
java
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. 反序列化 (加载) 模型
java
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 类:
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)。