Spring Data JPA:getOne返回代理对象导致Book关联Author失效问题
我来帮你搞定这个问题——你猜的完全没错,核心原因就是authorRepository.getOne()返回的是JPA的懒加载代理对象,这和Spring Data JPA的方法设计直接相关。
先理清楚问题出在哪:
你写的POST请求处理器里,
authorRepository.getOne(createBookVM.authorId)返回的并不是真实的Author实体实例,而是一个JPA代理。这个代理不会立刻从数据库拉取完整的Author数据,只有当你访问它的非ID属性时才会触发懒加载查询。
你日志里能正常打印author.id,是因为代理对象本身就持有ID值,不需要去数据库查;但当你把这个代理赋值给book.author后,因为Controller方法默认没有事务上下文,执行bookRepository.save(book)时JPA无法正确初始化这个代理,导致最终持久化的Book里author字段为空。而userRepository.findOneByLogin()返回的是真实的User实体,所以赋值完全正常。
下面给你几个可行的解决方案:
方案1:替换getOne为findById(首推)
findById会直接从数据库加载完整的Author实体,返回真实实例而非代理,赋值后就能正常持久化关联关系,还能明确处理Author不存在的场景:
@PostMapping("/book") fun createBook(@RequestBody createBookVM: CreateBookVM): ResponseEntity<Book> { // 替换getOne为findById,处理Author不存在的情况 val author = authorRepository.findById(createBookVM.authorId) .orElseThrow { RuntimeException("Author not found with id: ${createBookVM.authorId}") } val userLogin = SecurityUtils.getCurrentUserLogin().orElseThrow { RuntimeException("User not logged in") } val user = userRepository.findOneByLogin(userLogin).orElseThrow { RuntimeException("IMPOSSIBLE: user does not exist in DB") } val book= Book() book.author = author book.user = user log.debug("Author object with id : {}", author.id) val result = bookRepository.save(book) return ResponseEntity.created(URI("/api/books/" + result.id)) .headers(HeaderUtil.createEntityCreationAlert(applicationName, true, ENTITY_NAME, result.id.toString())) .body(result) }
方案2:手动初始化代理对象
如果你一定要用getOne,可以通过Hibernate的工具类手动触发代理初始化,确保实体数据被加载:
import org.hibernate.Hibernate @PostMapping("/book") fun createBook(@RequestBody createBookVM: CreateBookVM): ResponseEntity<Book> { val author = authorRepository.getOne(createBookVM.authorId) // 手动初始化代理,拉取真实的Author数据 Hibernate.initialize(author) val userLogin = SecurityUtils.getCurrentUserLogin().orElseThrow { RuntimeException("User not logged in") } val user = userRepository.findOneByLogin(userLogin).orElseThrow { RuntimeException("IMPOSSIBLE: user does not exist in DB") } val book= Book() book.author = author book.user = user log.debug("Author object with id : {}", author.id) val result = bookRepository.save(book) return ResponseEntity.created(URI("/api/books/" + result.id)) .headers(HeaderUtil.createEntityCreationAlert(applicationName, true, ENTITY_NAME, result.id.toString())) .body(result) }
方案3:给Controller方法添加事务注解
让整个请求处理方法处于事务上下文内,这样JPA在保存Book时会自动初始化代理对象:
import org.springframework.transaction.annotation.Transactional @PostMapping("/book") @Transactional // 添加事务注解 fun createBook(@RequestBody createBookVM: CreateBookVM): ResponseEntity<Book> { // 原有代码保持不变 val author = authorRepository.getOne(createBookVM.authorId) val userLogin = SecurityUtils.getCurrentUserLogin().orElseThrow { RuntimeException("User not logged in") } val user = userRepository.findOneByLogin(userLogin).orElseThrow { RuntimeException("IMPOSSIBLE: user does not exist in DB") } val book= Book() book.author = author book.user = user log.debug("Author object with id : {}", author.id) val result = bookRepository.save(book) return ResponseEntity.created(URI("/api/books/" + result.id)) .headers(HeaderUtil.createEntityCreationAlert(applicationName, true, ENTITY_NAME, result.id.toString())) .body(result) }
小提醒
优先选方案1,因为findById的语义更清晰,还能灵活处理Author不存在的异常;而getOne在ID不存在时会直接抛出EntityNotFoundException,不如findById的orElseThrow可控。
内容的提问来源于stack exchange,提问作者Eerik Sven Puudist

