Spring Data Jpa 关联对象序列化时出现no Session的解决方案
2022/4/17 6:21:06
本文主要是介绍Spring Data Jpa 关联对象序列化时出现no Session的解决方案,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
Spring Data Jpa 关联对象序列化时出现no Session的解决方案
在使用Spring Data Jpa时, 经常会编写类似下面的代码:
@Entity public class User { @Id @Column(name = "user_id") private Long id; @JoinTable @ManyToMany private Set<Role> roles;
@Entity public class Role { @Id @Column(name = "role_id") private Long id; }
然后进行如下类似下面的调用时发生异常:
@SpringBootTest(classes = JpaDemoApplication.class) class JpaDemoApplicationTests { @Autowired UserRepository userRepository; @Test void contextLoads() {} @Test void test() { userRepository.findById(1L).orElseThrow().getRoles().forEach(System.out::println); } }
failed to lazily initialize a collection of role: com.wymc.demo.User.roles, could not initialize proxy - no Session org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.wymc.demo.User.roles, could not initialize proxy - no Session at app//org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:614) at app//org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:218) at app//org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:591) at app//org.hibernate.collection.internal.AbstractPersistentCollection.read(AbstractPersistentCollection.java:149) at app//org.hibernate.collection.internal.PersistentSet.iterator(PersistentSet.java:188) at java.base@17.0.2/java.lang.Iterable.forEach(Iterable.java:74) at app//com.wymc.demo.JpaDemoApplicationTests.test(JpaDemoApplicationTests.java:16)
出现上面的异常的原因也很简单, test
方法中, 调用userRepository.findById
之后, 事务就已经提交, 此时会话就已经关闭, 而懒加载需求会话连接, 因此再调用getRoles
并对它进行迭代的时候就会抛出异常.
这个时候也只需要在方法上加上@Transaction
即可.
@Test @Transactional void test() { userRepository.findById(1L).orElseThrow().getRoles().forEach(System.out::println); }
然而, 如果我们在controller
中返回实体类时, 就像下面这样
@RestController @RequestMapping("/user") public class UserController { private final UserRepository userRepository; public UserController(UserRepository userRepository) { this.userRepository = userRepository; } @GetMapping("/{id}") public User test(@PathVariable("id") Long id) { return userRepository.findById(id).orElseThrow(); } }
如果不做任何配置, 你能通过/user/{id}
查询到数据并且得到正确的结果.
但是你会发现, 启动时控制台将会有一个警告:
WARN 19016 --- [ main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
如果你按照他的提示在application.properties
文件中添加spring.jpa.open-in-view=false
配置的话, 就又会出现no session
异常了.
此时控制台会给出一个警告:
WARN 16292 --- [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: failed to lazily initialize a collection of role: com.wymc.demo.User.roles, could not initialize proxy - no Session; nested exception is com.fasterxml.jackson.databind.JsonMappingException: failed to lazily initialize a collection of role: com.wymc.demo.User.roles, could not initialize proxy - no Session (through reference chain: com.wymc.demo.User["roles"])]
并响应500异常:
{ "timestamp": "2022-04-16T13:25:30.229+00:00", "status": 500, "error": "Internal Server Error", "path": "/user/1" }
通过异常信息你可以发现, 在序列化时发生了异常.
目前网上主要的解决方法有:
- 配置
spring.jpa.open-in-view=true
, 允许在视图层中开启会话, 这将会在序列化时进行懒加载查询 - 配置
hibernate
的enable_lazy_load_no_trans为true
, 允许在事务外开启会话进行懒加载, 与1同理 - 使用FetchType.EAGER策略, 查询到数据后直接加载关联的数据, 序列化时数据已经被加载因此不会出现异常
- 使用JPQL的
Join Fetch
语法, 通过生成join
查询出关联的数据, 同样也是在序列化之前就已经加载好数据 - 使用
@JsonIgnore
亦或是@JSONField(serialize=false)
, 直接不进行序列化
然而, 除了方法4和5以外, 无一例外都存在一个严重的问题: N+1问题
例如:
再增加一个Menu
类, Role
与Menu
形成多对多关系.
@Entity public class Menu { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "menu_id") private Long id; }
@Entity public class Role { @Id @Column(name = "role_id") private Long id; @JoinTable( name = "role_menus", joinColumns = @JoinColumn(name = "role_id"), inverseJoinColumns = @JoinColumn(name = "menu_id")) @ManyToMany private Set<Menu> menus; }
增加一个分页查询接口
@GetMapping public Page<User> page(Pageable pageable) { return userRepository.findAll(pageable); }
然后在数据库随便插入几条数据后进行查询(保证查询的用户关联角色关联菜单都存在数据就行)
查看控制台日志:
Hibernate: select user0_.user_id as user_id1_3_ from user user0_ limit ? Hibernate: select roles0_.user_id as user_id1_4_0_, roles0_.role_id as role_id2_4_0_, role1_.role_id as role_id1_1_1_ from user_roles roles0_ inner join role role1_ on roles0_.role_id=role1_.role_id where roles0_.user_id=? Hibernate: select menus0_.role_id as role_id1_2_0_, menus0_.menu_id as menu_id2_2_0_, menu1_.menu_id as menu_id1_0_1_ from role_menus menus0_ inner join menu menu1_ on menus0_.menu_id=menu1_.menu_id where menus0_.role_id=? Hibernate: select menus0_.role_id as role_id1_2_0_, menus0_.menu_id as menu_id2_2_0_, menu1_.menu_id as menu_id1_0_1_ from role_menus menus0_ inner join menu menu1_ on menus0_.menu_id=menu1_.menu_id where menus0_.role_id=? Hibernate: select roles0_.user_id as user_id1_4_0_, roles0_.role_id as role_id2_4_0_, role1_.role_id as role_id1_1_1_ from user_roles roles0_ inner join role role1_ on roles0_.role_id=role1_.role_id where roles0_.user_id=?
此时, 我的数据库只有两个用户, 但是竟然查询了五次(根据数据库内容不同查询的次数可能也会不同), 这是因为在序列化时懒加载关联的角色和菜单进行了查询, 为了查询出所有关联的数据, 进行了额外的四次查询, 而实际应用过程中, 这个查询次数只会更多, 并且因为关联的层级递增而快速增长. 这正是n+1
问题, 进行一次查询, 要进行n次关联对象的查询. 这大大增加了查询次数, 并且增加了请求响应时间.
而用JOIN FETCH
来查询也是不切实际的, 并且会导致生成的查询过于复杂, 也没有实际的可操作性.
方法5确实可以解决序列化实体类的懒加载异常问题, 但为了在某些时候能够正常序列化关联的对象, 我们要引入额外的类, 他们具备和实体类大体一样的字段, 在service
层将实体类转换, 按需复制其关联对象的字段, 结合mapstruct
使用, 也是一种可行的方法, 但这样会引入大量额外的类, 他们基本与实体类一致, 却要额外定义, 并且还要再进行一次转换, 对我来说这实在是太麻烦了.
难道没有一种能够在序列化时, 如果在会话关闭, 并且数据还没有加载, 就直接返回空, 如果数据已经加载就能够正常进行序列化的方法吗?
答案是有的.
debug看一下查询出来的User
对象, 可以发现roles
字段的类型为org.hibernate.collection.internal.PersistentSet
而非java.util.HashSet
, 对其进行迭代时的关键源码如下
public class PersistentSet extends AbstractPersistentCollection implements java.util.Set { @Override public Iterator iterator() { read(); return new IteratorProxy( set.iterator() ); } }
public abstract class AbstractPersistentCollection implements Serializable, PersistentCollection { protected final void read() { initialize( false ); } protected final void initialize(final boolean writing) { if ( initialized ) { return; } withTemporarySessionIfNeeded( new LazyInitializationWork<Object>() { @Override public Object doWork() { session.initializeCollection( AbstractPersistentCollection.this, writing ); return null; } }); } private <T> T withTemporarySessionIfNeeded(LazyInitializationWork<T> lazyInitializationWork) { SharedSessionContractImplementor tempSession = null; if ( session == null ) { if ( allowLoadOutsideTransaction ) { tempSession = openTemporarySessionForLoading(); } else { throwLazyInitializationException( "could not initialize proxy - no Session" ); } } // ...其它代码 } }
jpa查询会使用PersistentCollection
接口的实现类赋值给关联集合, 读取其内容时会调用AbstractPersistentCollection#initialize()
, 如果未进行初始化(fetchType为lazy时), 则会使用数据库会话加载数据, 如果会话不存在则尝试打开临时会话, 如果不允许打开临时会话, 最后会抛出no Session
异常.
到这里, 其实方法就显而易见了, 我们只需要对PersistentCollection
类型的数据进行判断, 如果已经初始化则序列化, 否则不序列化.
如果使用jackson进行序列化, 代码如下:
public class PersistentCollectionSerializer extends StdSerializer<PersistentCollection> { protected PersistentCollectionSerializer(Class<PersistentCollection> t) { super(t); } @Override public void serialize(PersistentCollection value, JsonGenerator gen, SerializerProvider provider) throws IOException { if (value.wasInitialized()) { if (value instanceof Collection<?>) { provider.findValueSerializer(Collection.class).serialize(value, gen, provider); return; } else if (value instanceof Map<?,?>) { provider.findValueSerializer(Map.class).serialize(value, gen, provider); return; } } provider.defaultSerializeNull(gen); } }
并通过Jackson2ObjectMapperBuilder
添加该序列化器.
@Component public class ObjectMapperConfiguration { @Bean public Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder() { return new Jackson2ObjectMapperBuilder() .createXmlMapper(false) .serializerByType( PersistentCollection.class, new PersistentCollectionSerializer(PersistentCollection.class)); } }
此时, 查询user得到的roles的结果就为空了.
但是N+1
问题并没有解决, 这只是在序列化时不再序列化未初始化的PersistentCollection
.
要解决N+1
问题, 可以去网上找方案.
这里推荐使用EntityGraph
.
只需要在repository中使用即可:
public interface UserRepository extends JpaRepository<User, Long> { @EntityGraph(attributePaths = {"roles"}) @Override Optional<User> findById(Long id); }
这样通过findById
查询出来的结果将会携带上roles
字段.
也可以在实体类上使用
@NamedEntityGraph( name = "user.roles", attributeNodes = {@NamedAttributeNode("roles")}) public class User {}
然后在repository中使用, @EntityGraph
注解通过名称来查找对应的@NamedEntityGraph
public interface UserRepository extends JpaRepository<User, Long> { @EntityGraph("user.roles") @Override Optional<User> findById(Long id); }
这种方法定义简单, 并且兼容性好.
通过以上方法, 你就可以在一般查询中, 不再序列化关联的集合, 也就不会发生no Session
异常, 同时在有必要的情况下, 能够正常获取到关联的数据.
这篇关于Spring Data Jpa 关联对象序列化时出现no Session的解决方案的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-23Springboot应用的多环境打包入门
- 2024-11-23Springboot应用的生产发布入门教程
- 2024-11-23Python编程入门指南
- 2024-11-23Java创业入门:从零开始的编程之旅
- 2024-11-23Java创业入门:新手必读的Java编程与创业指南
- 2024-11-23Java对接阿里云智能语音服务入门详解
- 2024-11-23Java对接阿里云智能语音服务入门教程
- 2024-11-23JAVA对接阿里云智能语音服务入门教程
- 2024-11-23Java副业入门:初学者的简单教程
- 2024-11-23JAVA副业入门:初学者的实战指南