Spring Data JpaRepository 在使用方法 getById() 时抛出 LazyInitializationException 但在使用 findById() 时不会
Spring Data JpaRepository throws LazyInitializationException when using method getById() but not when using findById()
在我的 Spring 启动(版本 2.5.4)中,我有一项服务通过 ExecutorService
(在新线程中)执行其方法之一。在这个新线程中,我访问了我的一些存储库 (JpaRepository
s)。我在 JpaRepository
的 getById()
和 findById()
中看到了不同的行为,我进行了搜索但没有找到。
这是我的实体:
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class LimoSet {
@Id
@Column(name = "_id")
private String id;
@Column(name = "_status")
private String status;
@OneToMany(mappedBy = "set", fetch = FetchType.EAGER)
private Set<Limo> limos = new LinkedHashSet<>();
@Column(name = "_statistics")
private String statistics;
}
这是存储库:
@Repository
public interface LimoSetRepository extends JpaRepository<LimoSet, String> {
}
这是服务:
@Service
@Transactional
public class GeneratorService {
private final static Logger logger = LoggerFactory.getLogger(GeneratorService.class);
private final LimoSetRepository setRepository;
private final ExecutorService executor = Executors.newFixedThreadPool(3);;
public void generate(Options opts) {
.
.
.
Callable<String> task = () -> {
try {
this.runGenerate(opts);
} catch (JsonProcessingException e) {
e.printStackTrace();
} catch (RuntimeException e) {
e.printStackTrace();
var set = setRepository.getById(opts.getName());
set.setStatus(e.getMessage());
setRepository.save(set);
}
return "ok";
};
executor.submit(task);
}
void runGenerate(Options opts) throws JsonProcessingException {
.
.
.
var set = setRepository.findById(opts.getName()).get(); //this is ok
var set = setRepository.getById(opts.getName()); //this throws LazyInitializationException
set.setStatus("GENERATED"); //the Exception is reported in this line
setRepository.save(set);
}
}
为什么 findById()
有效而 getById()
无效?
有几个因素会导致 LazyInitializationException。
引物:
熟悉findById
和getById
的不同语义
findById
获取整个对象
getById
获取对象代理
参见:How do find and getReference EntityManager methods work when using JPA and Hibernate
When loading a Proxy, you need to be aware that a LazyInitializationException
can be thrown if you try to access the Proxy reference after the EntityManager is closed.
其次,您需要了解为什么 runGenerate
运行 没有活动事务,即使 class 被标记为 @Transactional。
(注意:没有活动事务是关闭EntityManager的根本原因)
要确保交易未激活,请参阅 Detecting If a Spring Transaction Is Active:
assertTrue(TransactionSynchronizationManager.isActualTransactionActive());
为什么不传播来自测试的父事务
runGenerate
是 运行 通过单独线程上的执行程序
- 活动事务保持为线程本地,因此事务不会跨线程传播。
为什么@Transactional
对runGenerate
没有效果?
Spring(默认情况下)使用 CGLIB 代理来启用方面。
CGLIB 代理有 2 个要求:
- 代理方法必须是public
- 您必须通过代理调用该方法(而不是通过实际对象)
None 满足这些条件。
为什么交易在测试本身是活跃的
Spring提供了一个特殊的TestExecutionListener
叫做TransactionalTestExecutionListener
,它检查测试方法是否被标记为@Transactional,如果需要则启动事务
如何修复
- 提取
runGenerate
到一个服务,标记为public和@Transactional
- 将新服务自动连接到您的测试中
- 更一般地说,请注意您的任务的哪些部分在事务上下文中运行(您的任务的 catch 块不是事务性的)
- 也更一般地,了解脏检查。一旦您在事务上下文中有了 运行 的方法,
save
调用将是多余的。
在我的 Spring 启动(版本 2.5.4)中,我有一项服务通过 ExecutorService
(在新线程中)执行其方法之一。在这个新线程中,我访问了我的一些存储库 (JpaRepository
s)。我在 JpaRepository
的 getById()
和 findById()
中看到了不同的行为,我进行了搜索但没有找到。
这是我的实体:
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class LimoSet {
@Id
@Column(name = "_id")
private String id;
@Column(name = "_status")
private String status;
@OneToMany(mappedBy = "set", fetch = FetchType.EAGER)
private Set<Limo> limos = new LinkedHashSet<>();
@Column(name = "_statistics")
private String statistics;
}
这是存储库:
@Repository
public interface LimoSetRepository extends JpaRepository<LimoSet, String> {
}
这是服务:
@Service
@Transactional
public class GeneratorService {
private final static Logger logger = LoggerFactory.getLogger(GeneratorService.class);
private final LimoSetRepository setRepository;
private final ExecutorService executor = Executors.newFixedThreadPool(3);;
public void generate(Options opts) {
.
.
.
Callable<String> task = () -> {
try {
this.runGenerate(opts);
} catch (JsonProcessingException e) {
e.printStackTrace();
} catch (RuntimeException e) {
e.printStackTrace();
var set = setRepository.getById(opts.getName());
set.setStatus(e.getMessage());
setRepository.save(set);
}
return "ok";
};
executor.submit(task);
}
void runGenerate(Options opts) throws JsonProcessingException {
.
.
.
var set = setRepository.findById(opts.getName()).get(); //this is ok
var set = setRepository.getById(opts.getName()); //this throws LazyInitializationException
set.setStatus("GENERATED"); //the Exception is reported in this line
setRepository.save(set);
}
}
为什么 findById()
有效而 getById()
无效?
有几个因素会导致 LazyInitializationException。
引物:
熟悉findById
和getById
findById
获取整个对象getById
获取对象代理
参见:How do find and getReference EntityManager methods work when using JPA and Hibernate
When loading a Proxy, you need to be aware that a
LazyInitializationException
can be thrown if you try to access the Proxy reference after the EntityManager is closed.
其次,您需要了解为什么 runGenerate
运行 没有活动事务,即使 class 被标记为 @Transactional。
(注意:没有活动事务是关闭EntityManager的根本原因)
要确保交易未激活,请参阅 Detecting If a Spring Transaction Is Active:
assertTrue(TransactionSynchronizationManager.isActualTransactionActive());
为什么不传播来自测试的父事务
runGenerate
是 运行 通过单独线程上的执行程序- 活动事务保持为线程本地,因此事务不会跨线程传播。
为什么@Transactional
对runGenerate
没有效果?
Spring(默认情况下)使用 CGLIB 代理来启用方面。 CGLIB 代理有 2 个要求:
- 代理方法必须是public
- 您必须通过代理调用该方法(而不是通过实际对象)
None 满足这些条件。
为什么交易在测试本身是活跃的
Spring提供了一个特殊的TestExecutionListener
叫做TransactionalTestExecutionListener
,它检查测试方法是否被标记为@Transactional,如果需要则启动事务
如何修复
- 提取
runGenerate
到一个服务,标记为public和@Transactional - 将新服务自动连接到您的测试中
- 更一般地说,请注意您的任务的哪些部分在事务上下文中运行(您的任务的 catch 块不是事务性的)
- 也更一般地,了解脏检查。一旦您在事务上下文中有了 运行 的方法,
save
调用将是多余的。