使用JPA / Hibernate从SQL Server流式传输二进制数据

时间:2017-09-20 14:19:18

标签: java hibernate jpa spring-data-jpa mssql-jdbc

我在SQL Server上有大型文件存储,我需要将它们作为InputStream传递给客户端,而不会耗尽内存。我使用的是Hibernate 5.2.11,WildFly 10.1和Microsoft JDBC Driver 6.2.1(支持流式传输到/来自SQL Server)。

要将InputStream映射到我的实体,我需要创建一个自定义的Hibernate类型,因为遗憾的是Hibernate没有提供这样的映射。

import java.io.InputStream;
import java.io.Serializable;
import java.sql.Blob;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Objects;

import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.type.BlobType;
import org.hibernate.type.SerializationException;
import org.hibernate.usertype.UserType;

public class InputStreamType implements UserType {

    private static final int[] SQL_TYPES = { Types.LONGVARBINARY };

    @Override
    public int[] sqlTypes() {
        return SQL_TYPES;
    }

    @Override
    public Class<?> returnedClass() {
        return InputStream.class;
    }

    @Override
    public boolean equals(Object x, Object y) throws HibernateException {
        return Objects.equals(x, y);
    }

    @Override
    public int hashCode(Object x) throws HibernateException {
        return Objects.hashCode(x);
    }

    @Override
    public Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner)
            throws HibernateException, SQLException {
        Blob blob = (Blob) BlobType.INSTANCE.nullSafeGet(rs, names, session, owner);
        return blob == null ? null : blob.getBinaryStream();
    }

    @Override
    public void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor session)
            throws HibernateException, SQLException {
        if (value == null) {
            st.setNull(index, SQL_TYPES[0]);
        } else {
            st.setBinaryStream(index, (InputStream) value);
        }
    }

    @Override
    public Object deepCopy(Object value) throws HibernateException {
        return value;
    }

    @Override
    public boolean isMutable() {
        return false;
    }

    @Override
    public Serializable disassemble(Object value) throws HibernateException {
        throw new SerializationException("Cannot serialize " + InputStream.class.getName(), null);
    }

    @Override
    public Object assemble(Serializable cached, Object owner) throws HibernateException {
        return cached;
    }

    @Override
    public Object replace(Object original, Object target, Object owner) throws HibernateException {
        return original;
    }

}

然后,我注释我的实体字段以使用此类型:

@Entity
public class User {

    @Id
    Long id;

    @Lob
    @Type(type = "InputStreamType")
    InputStream picture;

    // ...

    InputStream getPicture() {
        return this.picture;
    }

    // ...

}

但是当我尝试读取流时,我得到一个例外:

@Service
public class UserService {

    @PersistenceContext
    EntityManager em;

    @Transactional
    public void testReadInputStream() {
        InputStream picture = em.find(User.class, 1L).getPicture();
        System.out.println(picture.getClass().getName());
        // prints: com.microsoft.sqlserver.jdbc.PLPInputStream
        picture.read();
        // throws: IOException The TDS protocol stream is not valid.
        //         at com.microsoft.sqlserver.jdbc.PLPInputStream.readBytes(PLPInputStream.java:304)
        //         at com.microsoft.sqlserver.jdbc.PLPInputStream.read(PLPInputStream.java:244)
    }

}

我试图读取InputStreamType.nullSafeGet内的流,并且它不会抛出任何异常。

那么,流从InputStreamType.nullSafeGet返回后会发生什么?我怎么能让它变得可用?

更新1

我将案例简化为:

import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class Main {

    public static void main(String[] args) throws Exception {
        Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver");
        Connection connection = DriverManager.getConnection(
                "jdbc:sqlserver://localhost:1433;DatabaseName=test");
        Statement statement = connection.createStatement();
        ResultSet resultSet = statement.executeQuery("select picture from [User] where id = 1");
        resultSet.next();
        InputStream inputStream = resultSet.getBlob(1).getBinaryStream();
        System.out.println(inputStream.getClass().getName());
        // prints: com.microsoft.sqlserver.jdbc.PLPInputStream

        inputStream.read();
        // no exception

        statement.close();
        inputStream.read();
        // throws: IOException: The TDS protocol stream is not valid.
    }

}

可能是从CustomType语句返回语句已关闭且流无法访问?如果是这样,我怎样才能在JPA中克服这个问题?

更新2

我的最后一个发现:如果我返回blob的流,它会在会话结束时关闭;但是,如果我返回Blob本身,当会话关闭时,流会以某种方式加载到内存中,耗尽大流(我认为这种行为是由Blob上的Hibernate代理触发的)。

有没有办法让会话从存储库方法返回时打开,让流可访问并通过HTTP刷新数据而不会耗尽内存?

1 个答案:

答案 0 :(得分:0)

如果我没记错的话,SQL设计根本没有数据流。要么所有数据都已返回给SQL客户端,要么根本没有。这是一种原子的东西。

您可以做的是将二进制数据拆分为多个较小的块并一个接一个地拉出块。然后,您可以在将每个数据块发送到HTTP客户端时关闭与数据库的网络连接。

或者,您可以将二进制数据存储为文件系统上的文件并从那里进行流式处理。但这意味着您必须自己处理文件系统和数据库之间的一致性。