Spring Data JPA 存储 BLOB

15

使用spring-data-jpa存储带有blob的实体的“最佳”或标准方法是什么?

@Entity
public class Entity {
  @Id
  private Long id;
  @Lob()
  private Blob blob;
}

public interface Repository extends CrudRepository<Entity,  Long> {
}

你解决了这个问题吗?我也遇到了这种情况,正在寻找答案。 - fatiherdem
不用Spring-data。如果我用了,我会和你分享的。;) - user482745
6个回答

16

简要概括

你可以在我的 GitHub 上查看示例项目。该项目展示了如何将数据流传输到/从数据库。

问题

所有有关将@Lob映射为byte[]的建议都不利于(在我看来) blobs 的主要优势 – 流式处理。使用byte[]会将所有内容加载到内存中。虽然这可能没问题,但如果您使用大型对象,则可能需要流式处理。

解决方案

映射

@Entity
public class MyEntity {

    @Lob
    private Blob data;

    ...

}

配置

暴露 Hibernate 的 SessionFactoryCurrentSession,以便您可以获取 LobCreator。在 application.properties 中:

spring.jpa.properties.hibernate.current_session_context_class=org.springframework.orm.hibernate5.SpringSessionContext

将会话工厂公开为Bean:

@Bean // Need to expose SessionFactory to be able to work with BLOBs
public SessionFactory sessionFactory(HibernateEntityManagerFactory hemf) {
    return hemf.getSessionFactory();
}

创建 Blob

@Service
public class LobHelper {

    private final SessionFactory sessionFactory;

    @Autowired
    public LobHelper(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public Blob createBlob(InputStream content, long size) {
        return sessionFactory.getCurrentSession().getLobHelper().createBlob(content, size);
    }

    public Clob createClob(InputStream content, long size, Charset charset) {
        return sessionFactory.getCurrentSession().getLobHelper().createClob(new InputStreamReader(content, charset), size);
    }
}

还有一点,正如评论中所指出的那样,只要你使用包含流的@Blob,你需要在事务内工作。只需标记工作部分为@Transactional即可。


它在Hibernate 5.2中无法工作。HibernateEntityManagerFactory现已被弃用。 - Michał Stochmal
它可能仍然可以工作,但如果您阅读过时的Javadoc,就很清楚需要做什么才能摆脱已弃用的类... - Jan Zyka
读取 Blob 怎么样?读取 Blob 需要底层 JDBC 连接处于打开状态,但是除非您的代码在事务中运行,否则 JPA 不会对此提供任何保证。 - Guillaume Polet
没错,它将需要在事务内被调用。这是个问题吗?不过说得好,我应该提到一下... - Jan Zyka
对于PostgreSQL一切正常。即使我将Spring Boot升级到最新版本3.2.1-RELEASE(进行了一些修改),也没有问题。但是对于MariaDB,我无法让它工作(在任何版本中都不行)。写入数据库可以正常工作,但读取时会出现OutOfMemoryError。显然,blob一次性从数据库加载到内存中。 - Bart Weber
@BartWeber:我不确定MariaDB是否支持BLOBs,但很有可能它不支持。一些较旧的问题表明MariDB驱动程序不支持流式传输:https://stackoverflow.com/questions/42555652/how-to-stream-data-to-mariadb-over-jdbc - Jan Zyka

6

自动装配您的仓库接口,并调用save方法传递您的实体对象。

我有一个类似的设置,它工作得非常好:

@Autowired
Repository repository;

repository.save(entity);

@Entity
@Table(name = "something")
public class Message {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Lob
    @Column
    private byte[] data;

你是如何创建The Blob的? - user482745
请查看我的修改后的答案。 - Harish Kadamudi
3
这会破坏流式处理的目的,如果处理的数据更大,你很可能会遇到OOM问题。 - Jan Zyka
为什么使用 byte[] 会遇到 OOM(内存不足)问题,而使用 String 则不会? - Mustafa

5

Spring Data 不处理 BLOB ,但是 Spring Content 可以。具体来说,Spring Content JPA 将内容存储为 BLOB,并通过注释将该内容与实体关联到数据库中。

pom.xml

   <!-- Java API -->
   <dependency>
      <groupId>com.github.paulcwarren</groupId>
      <artifactId>spring-content-jpa-boot-starter</artifactId>
      <version>0.0.11</version>
   </dependency>
   <!-- REST API -->
   <dependency>
      <groupId>com.github.paulcwarren</groupId>
      <artifactId>spring-content-rest-boot-starter</artifactId>
      <version>0.0.11</version>
   </dependency>

Entity.java

@Entity
public class Entity {
   @Id
   @GeneratedValue
   private long id;

   @ContentId
   private String contentId;

   @ContentLength
   private long contentLength = 0L;

   // if you have rest endpoints
   @MimeType
   private String mimeType = "text/plain";

DataContentStore.java

@StoreRestResource(path="data")
public interface DataContentStore extends ContentStore<Data, String> {
}

这种方法的优势在于开发人员不需要担心任何样板代码(即接受答案中的“服务”)。 BLOB也作为Spring资源公开,提供了自然的编程接口。 或者可以通过REST接口自动导出。 但是,除了Java配置和存储接口之外,开发人员不需要进行任何编码。

这对于存储1小时长的视频有没有问题?@Paul Warren,随着内容变得越来越大,它不会变慢吗?我是指在存储后检索它。 - valik
它不应该 @vilak。Spring Content REST通过应用服务器的内存空间流式传输视频,因此意图是将客户端连接到视频流,并让客户端从数据库中拉出流而不在途中存储它。然而,特别是涉及关系型数据库时,这通常取决于底层数据库客户端支持BLOB流式传输的情况如何。如果您有特定的数据库/客户端,请告知,我很乐意进行优化(如果可能的话)。 - Paul Warren

2
使用Hibernate.getLobCreator并传递EntityManager可以为您解包的会话,您可以使用单个语句(下面的4)完成此操作:
// 1. Get entity manager and repository
EntityManager em = .... // get/inject someway the EntityManager
EntityRepository repository = ...// get/inject your Entity repository

// 2. Instantiate your Entity
Entity entity = new Entity();

// 3. Get an input stream (you shall also know its length)
File inFile = new File("/somepath/somefile");
InputStream inStream = new FileInputStream(inFile);

// 4. Now copy to the BLOB
Blob blob =
  Hibernate.getLobCreator(em.unwrap(Session.class))
           .createBlob(inStream, inFile.length());

// 5. And finally save the BLOB
entity.setBlob(blob);
entityRepository.save(f);

1
当然,但是你在回答中根本没有使用spring-data-jpa ;) - user482745
1
一点也不?EntityManager和Repository是什么?对Hibernate的唯一调用是提供一个适当的Blob,这取决于实现。为了保持100%的独立性,您可以通过Spring注入此单个语句。 - Franco G

0

您还可以直接从DataSource创建Blob

@Component
public class LobHelper {

    private final DataSource ds;

    public LobHelper(@Autowired DataSource ds){
         this.ds = ds;
    }

    public Blob createBlob(byte[] content) {
        try (Connection conn = ds.getConnection()) {
            Blob b = conn.createBlob();
            try (OutputStream os = b.setBinaryStream(1);
                 InputStream is = new ByteArrayInputStream(content)) {
                byte[] buffer = new byte[500000];
                int len;
                while ((len = is.read(buffer)) > 0) {
                    os.write(buffer, 0, len);
                }
                return b;
            }
        } catch (Exception e) {
            log.error("Error while creating blob.", e);
        }
        return null;
    }

}

为什么要使用字节数组而不是流?为什么不用 try-with-resources? - user482745
@user482745 可以随意重构这段代码。在我的情况下,我需要使用 byte[] 而不是流,但更改它非常简单。我不想嵌套 try-with-resources 语句,但这就是 - 我重构了这段代码。 - Michał Stochmal

0

我在获取当前会话工厂方面遇到了一些问题,就像上面的答案一样(例如出现错误:无法为当前线程获取事务同步会话没有进行中的事务)。最终(在Spring Boot应用程序中,目前使用的是2.3.1.RELEASE版本,Hibernate 5.4.1),我采用了以下方法来解决我的问题。

@Component
public class SomeService {

    /**
     * inject entity manager
     */
    @PersistenceContext 
    private EntityManager entityManager;

    @Transactional
    public void storeMethod(File file) {
       // ...
       FileInputStream in = new FileInputStream(file);

       Session session = entityManager.unwrap(Session.class);
       Blob blob = session.getLobHelper().createBlob(in, file.length());
       // ...
       entity.setData(blob);
       repo.save(entity);
    }
}

LobHelper 可以是这样的:

@Service
public class LobHelper {

    @PersistenceContext
    private EntityManager entityManager;

    public Blob createBlob(InputStream content, long size) {
        return ((Session)entityManager).getLobHelper().createBlob(content, size);
    }

    // ...

}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接