Apache Jena SPARQL 查询完全指南:入门与实战案例

Apache Jena SPARQL 查询完全指南:入门与实战案例

在本教程中,我们将详细介绍如何使用 Apache Jena 框架来执行 SPARQL 查询。SPARQL 是从 RDF 数据中检索和处理信息的标准语言,而 Apache Jena 则是 Java 中构建语义网和知识图谱应用的首选工具包。

本教程将带你从 SPARQL 的基础语法开始,逐步学习如何在 Jena 中创建数据模型、加载数据,并执行从简单到复杂的各种查询,包括 FILTEROPTIONALASKCONSTRUCT。我们还将特别介绍 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.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 模块(包括 corearq 等)的 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 这两个"通配符"绑定到数据图中的实际节点,以使查询模式的结构与数据图的某个子图完全重合。

  1. 第一次匹配:

    • 引擎尝试将 ?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"
  2. 第二次匹配:

    • 引擎尝试将 ?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"
  3. 其他尝试:

    • 引擎尝试将 ?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 详解

ResultSetFormatterorg.apache.jena.query 包中的一个实用类,专门用于将 ResultSet 转换为人类可读的字符串或写入到输出流。

它的主要用途是:

  1. 调试:在开发过程中快速查看 SPARQL 查询是否返回了预期的数据。
  2. 简单输出:在命令行工具或简单应用中直接展示结果。
  3. 格式转换:它不仅能输出表格,还可以将结果集序列化为 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 的核心流程:

  1. 准备 Model :使用 ModelFactory.createDefaultModel() 创建模型并加载数据。
  2. 创建 Query :使用 QueryFactory.create() 将 SPARQL 字符串解析为 Query 对象。
  3. 创建 QueryExecution :使用 QueryExecutionFactory.create(query, model) 将查询和数据模型绑定。
  4. 执行查询
    • qexec.execSelect() 用于 SELECT (返回 ResultSet)。
    • qexec.execConstruct() 用于 CONSTRUCT (返回 Model)。
    • qexec.execAsk() 用于 ASK (返回 boolean)。
  5. 处理结果
    • 快速调试 :使用 ResultSetFormatter.out(System.out, results) 快速打印表格。
    • 应用逻辑 :使用 while(results.hasNext())QuerySolution 对象来手动迭代,这种方式更灵活,特别是处理 OPTIONAL 变量时(需使用 soln.get("varName") 检查 null)。
  6. 关闭资源 :始终使用 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)。
相关推荐
向上的车轮16 小时前
数据中台工作流编排引擎:Apache Airflow
apache
雾迟sec16 小时前
Web安全-文件上传漏洞-黑白名单及其它绕过思路(附思维导图)
javascript·安全·web安全·网络安全·apache·安全威胁分析
悟乙己17 小时前
LangExtract + 知识图谱 — Google 用于 NLP 任务的新库
人工智能·自然语言处理·知识图谱
yumgpkpm19 小时前
CMP(类Cloudera CDP 7.3 404版华为泰山Kunpeng)和Apache Doris的对比
大数据·hive·hadoop·spark·apache·hbase·cloudera
zhangkaixuan45620 小时前
Apache Paimon 查询全流程深度分析
java·apache·paimon
A-刘晨阳2 天前
时序数据库选型指南:从大数据视角切入,聚焦 Apache IoTDB
大数据·apache·时序数据库·iotdb
迦蓝叶2 天前
使用 Apache Jena 构建 Java 知识图谱
java·apache·知识图谱·图搜索·关系查询·关系推理
zhangkaixuan4562 天前
Apache Paimon 写入流程
java·大数据·apache·paimon
DolphinScheduler社区2 天前
Apache DolphinScheduler 3.3.2 正式发布!性能与稳定性有重要更新
大数据·开源·apache·任务调度·海豚调度·发版