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

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

本教程将带你从 SPARQL 的基础语法开始,逐步学习如何在 Jena 中创建数据模型、加载数据,并执行从简单到复杂的各种查询,包括 FILTEROPTIONALASKCONSTRUCT。我们还将特别介绍 ResultSetFormatter 工具类的便捷用途。


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:

<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 查询语法基础

一个典型的 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):

我们的 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),你可以使用分号 (;) 来简化查询。

我们上面的查询:

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 ."
    + "  ?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 字符串:

// 假设 '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 查询返回一个新的 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. 序列化 (保存) 模型

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 的核心流程:

  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 类:

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)。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐