Halo Tech

HaloJDBC 探秘:如何操作 Oracle 的 BFILE 外部文件

在 Java 应用中使用Halo数据库,难免会遇到对大对象(LOB)的处理。其中,BFILE 是一种特殊的大对象类型,它允许 Halo 将文件内容存储在数据库外部的操作系统中,而数据库中仅保存指向文件的逻辑指针。这种设计在管理海量文件(如图片、视频、日志)时非常实用,既减轻了数据库的存储压力,又能利用文件系统的性能。
今天,我们就来看看 HaloJDBC 驱动 是如何实现对 OracleBFILE 的兼容,并通过一段完整的 Java 代码,演示从插入、导出到元数据查看的全过程。

HaloJDBC 对于BFILE,HaloJDBC 提供了 oracle.sql.BFILE 类,封装了 Oracle 的 BFILE 操作 API。通过这个类,我们可以:

  • 判断文件是否存在:bfile.fileExists()

  • 打开/关闭文件流:bfile.openFile() / bfile.closeFile()

  • 获取文件元数据:文件名、目录别名、文件大小等

  • 以流的方式读取文件内容:bfile.getBinaryStream()

下面,我们通过一个完整的示例,看看如何在实际代码中运用这些能力。

环境准备

假设我们已经创建了一张测试表:

sql
CREATE TABLE bfile_jdbc_demo (    id        NUMBER PRIMARY KEY,    filename  VARCHAR2(100),    bfiledir  BFILE);
create directory bfile_dir as '/usr/java';
并已经在 Halo 服务器上创建了目录对象 bfile_dir,并将该目录指向了服务器上的某个实际文件夹(例如 /u01/bfile_demo),同时在该文件夹中放置了 test1.txt 和 sample.html 两个文件。 

1. 引入依赖与建立连接

我们需要 HaloJDBC 驱动 JAR 包,并在代码中加载驱动:

Class.forName("com.halo.Driver");Connection conn = DriverManager.getConnection("jdbc:halo://10.120.130.181:1521/oracle", "halo", "halo");

2. 插入 BFILE 记录

使用 BFILENAME 函数创建 BFILE 定位器并插入表中:

public void insertBFILE(int id, String fileName) throws SQLException {    String sql = "INSERT INTO bfile_jdbc_demo VALUES (?, ?, BFILENAME('bfile_dir', ?))";    try (PreparedStatement ps = conn.prepareStatement(sql)) {        ps.setInt(1, id);        ps.setString(2, fileName);        ps.setString(3, fileName);  // BFILENAME 需要目录别名和文件名        ps.executeUpdate();    }}
Image
注意:BFILENAME 的第一个参数是数据库中的目录对象名称,第二个参数是文件名。文件必须事先存在于服务器对应的目录中,否则后续操作会报“文件不存在”。
3.导出单个BFILE到本地
查询指定 ID 的 BFILE,并将其内容导出到本地文件:
public void exportBFILE(int id, Path targetPath) throws SQLException, IOException {    String sql = "SELECT bfiledir FROM bfile_jdbc_demo WHERE id = ? FOR UPDATE";    try (PreparedStatement ps = conn.prepareStatement(sql)) {        ps.setInt(1, id);        try (ResultSet rs = ps.executeQuery()) {            if (!rs.next()) throw new SQLException("ID 未找到");            BFILE bfile = (BFILE) rs.getObject(1);
            if (!bfile.fileExists()) {                throw new FileNotFoundException("服务器端文件不存在");            }
            Files.createDirectories(targetPath.getParent());            bfile.openFile();            try (InputStream in = bfile.getBinaryStream();                 OutputStream out = Files.newOutputStream(targetPath)) {                byte[] buf = new byte[8192];                int len;                while ((len = in.read(buf)) != -1) {                    out.write(buf, 0, len);                }            } finally {                bfile.closeFile();            }        }    }}
Image
  • bfile.openFile() 必须在读取前调用,读取后必须 closeFile() 释放资源。

  • 通过 getBinaryStream() 获得输入流,即可像普通文件一样拷贝数据。

4.批量导出所有 BFILE 文件

遍历表中所有记录,将每个 BFILE 导出到指定文件夹:

public void exportAll(Path outputFolder) throws SQLException, IOException {    String sql = "SELECT * FROM bfile_jdbc_demo FOR UPDATE";    try (Statement st = conn.createStatement(); ResultSet rs = st.executeQuery(sql)) {        while (rs.next()) {            int    id   = rs.getInt(1);            String name = rs.getString(2);            BFILE  bf   = (BFILE) rs.getObject(3);            Path   out  = outputFolder.resolve(name);            if (!bf.fileExists()) continue;            Files.createDirectories(out.getParent());            bf.openFile();            try (InputStream in = bf.getBinaryStream();                 OutputStream outStream = Files.newOutputStream(out)) {                byte[] buf = new byte[8192];                int len;                while ((len = in.read(buf)) != -1) {                    outStream.write(buf, 0, len);                }            } finally {                bf.closeFile();            }            System.out.println("导出完成 -> " + out);        }    }}
Image

5. 查看 BFILE 元数据

通过 BFILE 对象提供的方法,可以获取文件是否存在、大小、目录别名和文件名等信息

public void printMeta(int id) throws SQLException {    String sql = "SELECT bfiledir FROM bfile_jdbc_demo WHERE id = ?";    try (PreparedStatement ps = conn.prepareStatement(sql)) {        ps.setInt(1, id);        try (ResultSet rs = ps.executeQuery()) {            if (!rs.next()) return;            BFILE bf = (BFILE) rs.getObject(1);            System.out.println("文件存在 : " + bf.fileExists());            System.out.println("文件大小 : " + bf.length());            System.out.println("目录别名 : " + bf.getDirAlias());            System.out.println("文件名称 : " + bf.getName());        }    }}
Image
  • 服务器端文件必须存在:BFILE 仅仅是指针,如果文件被移动或删除,fileExists() 会返回 false,读取时会报错。

  • 目录对象权限:数据库用户需要拥有对目录对象的 READ 权限,否则无法访问文件。

总结

通过 HaloJDBC 对 OracleBFILE 的兼容,我们可以在 Java 代码中像操作普通文件流一样处理数据库外部的文件,同时利用 SQL 管理文件元数据。无论是单个文件的导入导出,还是批量处理,HaloJDBC 都提供了简洁而强大的 API。