无法打开JPA EntityManager进行交易; EntityManager已关闭

时间:2017-11-07 07:42:59

标签: java spring multithreading hibernate jpa

我在最近几天上下搜索了互联网,并尝试了所有我能想到的。我现在认为自己没有选择。

在java 8 spring boot rest应用程序中,我有一个

JPA存储库:

@Transactional
public interface PageRepository extends JpaRepository<ListPage, Long> {

    static final String CLAUSE_AND_PAGE_NR = " and lp.pageNumber = :pageNumber";
    static final String BY_LIST_ID = "from ListPage lp inner join lp.listInstance li where li.objectId = :listId";
    static final String FIND_CONTENT = "select lp.content ";

    @Query(value = FIND_CONTENT + BY_LIST_ID + CLAUSE_AND_PAGE_NR)
    public String findContentByListIdAndPageNumber(@Param("listId") final Long listId, @Param("pageNumber") final Integer pageNumber);    
}

控制器:

@RestController
@RequestMapping("/download")
public class DownloadController {

    private final Logger logger = LoggerFactory.getLogger(DownloadController.class);

    @Resource
    ListRepository listRepository;

    @Resource
    PageRepository pageRepository;

    @RequestMapping(value = "/list/{id}/browse-txt", method = RequestMethod.GET, produces= MediaType.TEXT_PLAIN_VALUE)
    public ResponseEntity<StreamingResponseBody> getListAsSingleFileBrowseTxt(@PathVariable(value="id") Long listId,
                                                                            HttpServletResponse response,
                                                                            HttpServletRequest request) {


        ListInstance list = listRepository.findOne(listId);
        response.setHeader("Content-Disposition", "inline; filename=" + renameFileExtension(list.getFileName(),".TXT"));
        response.setHeader("content-type", "text/plain");


        return getResponse(auth.getCorporateKey(), list);
    }

    private ResponseEntity<StreamingResponseBody> getResponse (String corporateKey, ListInstance list) {

        Long listId = list.getObjectId();

        StreamingResponseBody responseBody = out -> {

            Integer pageCount = pageRepository.countByListId(listId);

            long bytesBuffered = 0;
            ByteArrayOutputStream buff = new ByteArrayOutputStream();
            String pageString;
            byte[] pageBytes;
            for (Integer i = 1; i <= pageCount; i++) {

                try {
                    pageString = pageRepository.findContentByListIdAndPageNumber(listId, i);

                    buff.write(pageBytes);
                    bytesBuffered += buff.size();
                    if (bytesBuffered > 1024) { //flush after 1KB
                        bytesBuffered = 0;
                        out.write(buff.toByteArray());
                        out.flush();
                        buff.reset();
                    }
                }
                catch (Exception e) {
                    logger.error("error writing lazy buffer: page " + i + " of list " + listId, e);
                }
            }
            out.write(buff.toByteArray());
            out.flush();
        };

        return new ResponseEntity(responseBody, HttpStatus.OK);
    }
}

当我呼叫控制器时,一切都按预期工作。我在浏览器中看到以1k块的形式出现的页面。这就是我想要的, - 大量内容的加载。

但是突然(并不总是在同一页面)它崩溃了: org.springframework.transaction.CannotCreateTransactionException:无法打开JPA EntityManager进行事务处理;嵌套异常是java.lang.IllegalStateException:EntityManager已关闭

我理解,这是因为JPA存储库在不同的线程中运行,其中创建存储库的线程已经完成(我在单个线程中尝试了这个读取循环并且这样做了。)

问题是: 如何防止JPA连接(无论我应该怎么称呼)在不同的线程中消失?

有人请再给我一个提示。

问候

S上。

2 个答案:

答案 0 :(得分:0)

例外情况显然是EntityManager is closed

您可以做以下事情

  1. getResponse (...)方法移动到某个服务类,并使用@transactional注释该方法。从您的控制器调用此方法。从PageRepository界面中删除@Transactional。它真的没关系,但建议。要了解更多信息,请查看link

  2. 重要pageString = pageRepository.findContentByListIdAndPageNumber(listId, i);您在这里为每个pageNumber点击数据库,因此会发生很多交易。修改代码只是为了通过传递in来执行一次list of page numbers查询。这样,DB只会有一笔交易。

  3. 只要尝试这些更改,请告诉我们是否有效。

答案 1 :(得分:0)

这就是我能解决的问题:

将此功能添加到 JPA存储库

@Query(value = FIND_CONTENT + BY_LIST_ID + CLAUSE_ORDER_BY_PAGE_NR)
@QueryHints(value = @QueryHint(name = HINT_FETCH_SIZE, value = "0"))
public Stream<String> streamAllPageContentByListID(@Param("listId") final Long listId);

这将是控制器

@RestController
@RequestMapping("/download")
public class DownloadController {

    private final Logger logger = LoggerFactory.getLogger(DownloadController.class);

    @Resource
    ListRepository listRepository;

    @Resource
    PageRepository pageRepository;

    @Transactional(readOnly = true)
    @RequestMapping(value = "/list/{id}/browse-txt", method = RequestMethod.GET, produces= MediaType.TEXT_PLAIN_VALUE)
    public ResponseEntity<StreamingResponseBody> getListAsSingleFileBrowseTxt(@PathVariable(value="id") Long listId,
                                                                            HttpServletResponse response,
                                                                            HttpServletRequest request) {


        ListInstance list = listRepository.findOne(listId);
        response.setHeader("Content-Disposition", "inline; filename=" + renameFileExtension(list.getFileName(),".TXT"));
        response.setHeader("content-type", "text/plain");


        return getResponse(response, auth.getCorporateKey(), list);
    }

    protected ResponseEntity getResponse (HttpServletResponse response, String corporateKey, ListInstance list) {

        Long listId = list.getObjectId();

        try {
            try (PrintWriter out = response.getWriter()) {                

                try (Stream<String> pageStream = pageRepository.streamAllPageContentByListID(listId)) {

                    pageStream.forEach(page -> {
                        out.write(page);
                    });
                    out.flush();
                }
            }
        } catch (Exception e) {
            logger.error("error writing lazy buffer of list " + listId, e);
            return new ResponseEntity(HttpStatus.BAD_REQUEST);
        }

        return new ResponseEntity(HttpStatus.OK);
    }
}

JPA存储库永远不会在不同的线程中工作。最后的提示来自这一方http://knes1.github.io/blog/2015/2015-10-19-streaming-mysql-results-using-java8-streams-and-spring-data.html

到目前为止,解决方案非常完美。懒惰负载的速度令人惊叹。