sprng boot jpa + redis LazyInitializationException
Asked Answered
C

2

7

I use spring boot 2.1.2 and redis as cache provider.

But now, I have a question.

  1. In sysUser entity
@Data
@Entity
@Table(name = "sys_user")
@ToString(exclude = "roles")
@EqualsAndHashCode(callSuper = true)
@Proxy(lazy = false)
public class SysUser extends BaseEntity implements UserDetails {

    // ...

    /**
     * 当前用户的权限
     */
    @ManyToMany(fetch = FetchType.EAGER)
    @JsonIgnoreProperties(value = "users")
    @JoinTable(name = "sys_user_role",
            joinColumns = {@JoinColumn(name = "user_id", nullable = false)},
            inverseJoinColumns = {@JoinColumn(name = "role_id", nullable = false)})
    private List<SysRole> roles;

    // ...
}

  1. In sysRole entity

@Data
@Entity
@Table(name = "sys_role")
@EqualsAndHashCode(callSuper = true)
@ToString(exclude = {"users", "permissions"})
@Proxy(lazy = false)
public class SysRole extends BaseEntity {

    // ...

    /**
     * 当前角色的菜单
     */
    @JsonIgnoreProperties(value = "roles")
    @ManyToMany(cascade = CascadeType.MERGE, fetch = FetchType.EAGER)
    @JoinTable(name = "sys_permission_role", joinColumns = @JoinColumn(name = "role_id"),
            inverseJoinColumns = @JoinColumn(name = "permission_id"))
    private List<SysPermission> permissions = new ArrayList<>();

    /**
     * 当前角色对应的用户
     * 双向映射造成数据重复查询死循环问题
     */
    @ManyToMany(mappedBy = "roles")
    private List<SysUser> users = new ArrayList<>();

}

  1. In SysPermission entitty
@Data
@Entity
@Table(name = "sys_permission")
@EqualsAndHashCode(callSuper = true)
@Proxy(lazy = false)
public class SysPermission extends BaseEntity {
    // ...

    /**
     * 菜单角色
     * 双向映射造成数据重复查询死循环问题
     */
    @ManyToMany(mappedBy = "permissions")
    private List<SysRole> roles = new ArrayList<>();
}

  1. In sysUser service impl
    @Override
    @Cacheable
    public SysUser loadUserByUsername(String username) {
        return sysUserRepository.findFirstByUsernameAndEnabledTrue(username).orElseThrow(() ->
                new UsernameNotFoundException("用户不存在")
        );
    }
  1. redis config
    @Bean
    @Override
    public CacheManager cacheManager() {
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofHours(12))
                .prefixKeysWith(applicationProperties.getName())
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer()))
                .disableCachingNullValues();
        return RedisCacheManager
                .builder(RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory))
                .cacheDefaults(redisCacheConfiguration)
                .transactionAware()
                .build();
    }
    @Bean(name = "redisTemplate")
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);
        redisTemplate.setKeySerializer(keySerializer());
        redisTemplate.setHashKeySerializer(keySerializer());
        redisTemplate.setValueSerializer(valueSerializer());
        redisTemplate.setHashValueSerializer(valueSerializer());
        return redisTemplate;
    }
    private RedisSerializer<String> keySerializer() {
        return new StringRedisSerializer();
    }
    private RedisSerializer<Object> valueSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }

Question

When I first called loadUserByUsername,it is ok.And in redis

redis

in json.cn

json.cn

But when I secound called loadUserByUsername,it is wrong,And get exception


org.springframework.data.redis.serializer.SerializationException: Could not read JSON: failed to lazily initialize a collection, could not initialize proxy - no Session (through reference chain: cn.echocow.xiaoming.model.entity.SysUser["roles"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: failed to lazily initialize a collection, could not initialize proxy - no Session (through reference chain: cn.echocow.xiaoming.model.entity.SysUser["roles"])

    at org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer.deserialize(GenericJackson2JsonRedisSerializer.java:132)
    at org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer.deserialize(GenericJackson2JsonRedisSerializer.java:110)
    at org.springframework.data.redis.serializer.DefaultRedisElementReader.read(DefaultRedisElementReader.java:48)
......
Caused by: com.fasterxml.jackson.databind.JsonMappingException: failed to lazily initialize a collection, could not initialize proxy - no Session (through reference chain: cn.echocow.xiaoming.model.entity.SysUser["roles"])
    at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:394)
    at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:353)
......
Caused by: org.hibernate.LazyInitializationException: failed to lazily initialize a collection, could not initialize proxy - no Session
    at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:597)
    at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:216)
    at org.hibernate.collection.internal.AbstractPersistentCollection.readSize(AbstractPersistentCollection.java:160)
    at org.hibernate.collection.internal.PersistentBag.size(PersistentBag.java:287)
    at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:302)
......

exception

Other

I try these methods

  1. @JsonIgnore , but it will set roles is null, I want to use this field.

  2. Config jackson registerModule Hibernate5Module, it will set roles is null.

  3. Use @Proxy(lazy = false), no changes.

  4. Use @ManyToMany(fetch = FetchType.EAGER), no changes

  5. config

spring:
  jpa:
    open-in-view: true
    properties
      hibernate:
        enable_lazy_load_no_trans: true

no changes...

  1. Use another json tools, such as gson and FastJson, but infinite loop for jpa when save cache.

Please help me, I had spent three days...But I do not resolve this question...

Thanks!

github address: XIAOMING

If do not have resolve method, maybe I must use Mybatis. But there is a lot of work.Please help me resolve this question...

Cosmogony answered 16/2, 2019 at 11:24 Comment(1)
Hi i have same issue github.com/ripper2hl/sepomex/tree/redis-cacheSizemore
N
4

1st. create 2 classes below The HibernateCollectionIdResolver.class will translate HibernateCollection class into JDK collection class, so the Jackson will write json from

{
    "paramA": [
    "org.hibernate.collection.internal.PersistentSet",
    []
  ]
}

to

{
    "paramA": [
    "java.util.HashSet",
    []
  ]
}

then the method typeFromId will get JDK JavaType from the class full name above, to deserialize your json to POJO.

class HibernateCollectionIdResolver extends TypeIdResolverBase {

    public HibernateCollectionIdResolver() {
    }

    @Override
    public String idFromValue(Object value) {
        //translate from HibernanteCollection class to JDK collection class
        if (value instanceof PersistentArrayHolder) {
            return Array.class.getName();
        } else if (value instanceof PersistentBag || value instanceof PersistentIdentifierBag || value instanceof PersistentList) {
            return List.class.getName();
        } else if (value instanceof PersistentSortedMap) {
            return TreeMap.class.getName();
        } else if (value instanceof PersistentSortedSet) {
            return TreeSet.class.getName();
        } else if (value instanceof PersistentMap) {
            return HashMap.class.getName();
        } else if (value instanceof PersistentSet) {
            return HashSet.class.getName();
        } else {
            //default is JDK collection
            return value.getClass().getName();
        }
    }

    @Override
    public String idFromValueAndType(Object value, Class<?> suggestedType) {
        return idFromValue(value);
    }

    //deserialize the json annotated JDK collection class name to JavaType
    @Override
    public JavaType typeFromId(DatabindContext ctx, String id) throws IOException {
        try {
            return ctx.getConfig().constructType(Class.forName(id));
        } catch (ClassNotFoundException e) {
            throw new UnsupportedOperationException(e);
        }
    }

    @Override
    public JsonTypeInfo.Id getMechanism() {
        return JsonTypeInfo.Id.CLASS;
    }
}

@JsonTypeInfo(
        use = JsonTypeInfo.Id.CLASS
)
@JsonTypeIdResolver(value = HibernateCollectionIdResolver.class)
public class HibernateCollectionMixIn {
}

2nd. register this MixIn class to you ObjectMapper

        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper mapper = new ObjectMapper();
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        mapper.registerModule(new Jdk8Module());
        mapper.registerModule(new JavaTimeModule());
        mapper.registerModule(new JodaModule());
        mapper.addMixIn(Collection.class, HibernateCollectionMixIn.class);
        jackson2JsonRedisSerializer.setObjectMapper(mapper);

last, register your jackson2JsonRedisSerializer to your RedisCacheConfiguration.

This would be helpful, I spent 2 days researching how to solve this problem. And I found the json type id could be rewrite... So just override jackson typeIdResolver~

EDIT: solve deserialization issue and add some comments

Neall answered 19/6, 2020 at 12:54 Comment(5)
While this code may solve the question, including an explanation of how and why this solves the problem would really help to improve the quality of your post, and probably result in more up-votes. Remember that you are answering the question for readers in the future, not just the person asking now. Please edit your answer to add explanations and give an indication of what limitations and assumptions apply.Fetiparous
@БогданОпир ok, and there's also some deserialize problems need to fix...Neall
@Neall This works for me so perfectly, can`t thank you moreLegitimatize
Thank you! this took 2 days to find the issue.Ellaelladine
@Neall if you can answer the same here #77070497, I will award you the bountyPestle
P
0

In your code you return valueSerializer like this

 private RedisSerializer<Object> valueSerializer() {
    return new GenericJackson2JsonRedisSerializer();
}

But you will have to return the GenericJackson2JsonRedisSerializer with Jackson Object mapper that has Hibernate5Module or Hibernate4Module registered as a module

public ObjectMapper getMapper() {
    ObjectMapper mapper = new ObjectMapper();
    mapper.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS);
    mapper.enable(SerializationFeature.FAIL_ON_EMPTY_BEANS);

    mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);

    // Registering Hibernate5Module to support lazy objects for hibernate 5
    // Use Hibernate4Module if using hibernate 4 
    mapper.registerModule(new Hibernate5Module());
    return mapper;
}


private RedisSerializer<Object> valueSerializer() {
    return new GenericJackson2JsonRedisSerializer(getMapper());
}
Pyrology answered 3/5, 2019 at 9:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.