V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Allianzcortex
V2EX  ›  程序员

Spring @ManyToMany 求助:数据库和字段对应后似乎陷入循环引用

  •  
  •   Allianzcortex · 2019-03-05 22:16:20 +08:00 · 3412 次点击
    这是一个创建于 2090 天前的主题,其中的信息可能已经有所发展或是发生改变。

    Hi 打扰大家了...这个问题理论上不应该很难,毕竟官方和各种文档上都有很多的 demo,但自己在实践过程中还是遇到了一个很尴尬的问题:

    场景是一篇文章(Article)有多个标签(Tag)

    • 定义是按照官方给的标准来进行的:

    Article :

    public class Article {
        ...
    
        @ManyToMany(cascade = {CascadeType.ALL})
        @JoinTable(name = "article_tag",
                joinColumns = @JoinColumn(name = "article_id", referencedColumnName = "id"),
                inverseJoinColumns = @JoinColumn(name = "tag_id", referencedColumnName = "id"))
        
        private Set<Tag> tagList = new HashSet<>();
    }
    

    Tag :

    public class Tag {
        ...
    
        @ManyToMany(mappedBy = "tagList")
        private Set<Article> articles=new HashSet<Article>();
    }
    
    
    • 对应生成的 sql 表数据格式则是:

    article 表没有与 tag 有关的字段 tag 表也没有与 article 有关的字段 所有的关系存储在 article_comment 表中:

    +------------+---------+------+-----+---------+-------+
    | Field      | Type    | Null | Key | Default | Extra |
    +------------+---------+------+-----+---------+-------+
    | ID         | int(11) | NO   | PRI | NULL    |       |
    | article_id | int(11) | YES  |     | NULL    |       |
    | comment_id | int(11) | YES  |     | NULL    |       |
    +------------+---------+------+-----+---------+-------+
    
    • 之后在查询 Article 时出现了异常:

    比如用最简单的语句只是返回一个 Article 对象:

    return articleRepository.findBySlug(slug);

    在添加 @ManyToMany 字段前是没有问题的,但添加之后就无法查询,会显示以下报错:

    Hibernate: select taglist0_.article_id as article_1_2_0_, taglist0_.tag_id as tag_id2_2_0_, tag1_.id as id1_4_1_, tag1_.tag_name as tag_name2_4_1_ from article_tag taglist0_ inner join tag tag1_ on taglist0_.tag_id=tag1_.id where taglist0_.article_id=?
    Hibernate: select articles0_.tag_id as tag_id2_2_0_, articles0_.article_id as article_1_2_0_, article1_.id as id1_0_1_, article1_.body as body2_0_1_, article1_.created_at as created_3_0_1_, article1_.description as descript4_0_1_, article1_.favorited as favorite5_0_1_, article1_.favorites_count as favorite6_0_1_, article1_.slug as slug7_0_1_, article1_.title as title8_0_1_, article1_.updated_at as updated_9_0_1_, article1_.user_id as user_id10_0_1_ from article_tag articles0_ inner join article article1_ on articles0_.article_id=article1_.id where articles0_.tag_id=?
    Hibernate: select taglist0_.article_id as article_1_2_0_, taglist0_.tag_id as tag_id2_2_0_, tag1_.id as id1_4_1_, tag1_.tag_name as tag_name2_4_1_ from article_tag taglist0_ inner join tag tag1_ on taglist0_.tag_id=tag1_.id where taglist0_.article_id=?
    
    

    看起来就像是 Article 和 Tag 在互相查询,引用依赖,再之后因为永远在互相查询会引起崩溃,从而:

     HHH000100: Fail-safe cleanup (collections) : org.hibernate.engine.loading.internal.CollectionLoadContext@4c5c2744<rs=HikariProxyResultSet@353080062 wrapping Result set representing update count of 3>
     Thread starvation or clock leap detected
    

    查询就无法进行下去。

    • 试了下有两个方法可以临时解决这个问题:

    ① 对 Article 里的 private Set<Tag> tagList 字段添加 @JsonIgnore 注解,这样返回的结果就不会包含 tag,也就不会报错。

    ② 修改 getter 方法(之前无论是 lombok 的 @Getter 还是用最经典的 Java getter 方法都不可行),比如重新定义如下:

     public Set<Tag> getTagList() {
            return new HashSet<Tag>();
        }
    

    会返回 "tagList": [], 这个字段,所有都为空。

    但这都并不是最终想要的结果 quq

    最终目的还是要解决循环依赖,有这样一篇文章: http://springquay.blogspot.com/2016/01/new-approach-to-solve-json-recursive.html 和主题类似,但里面的方法似乎都仍然没有效果。


    所以目前就卡在这个地方,完全没有思路,在网上基本找到所有类似的问题都没有相应解决方法...汗...想请问下 V 站的大家有没有遇到过类似的问题或者有什么调试的思路可以分享下么~谢谢啦。

    15 条回复    2019-03-07 08:25:44 +08:00
    qzeng2017
        1
    qzeng2017  
       2019-03-05 23:02:40 +08:00   ❤️ 1
    在 Article 类的 tagList 上 @JsonIgnoreProperties(value = {"articles"})
    在 Tag 类的 articles 上 @JsonIgnoreProperties(value = {"tagList"})
    Allianzcortex
        2
    Allianzcortex  
    OP
       2019-03-05 23:09:09 +08:00
    @qzeng2017 谢谢谢谢,这个方法和 http://springquay.blogspot.com/2016/01/new-approach-to-solve-json-recursive.html 里提到的第三种方法是一样的,遗憾的是还是会报错....
    S9Yh4wIFsBG7jnE4
        3
    S9Yh4wIFsBG7jnE4  
       2019-03-05 23:33:57 +08:00   ❤️ 1
    我之前也用过 many to many 的注解 没遇到过这情况啊
    Allianzcortex
        4
    Allianzcortex  
    OP
       2019-03-05 23:41:31 +08:00
    @shayang888 是啊...看到很多教程也都是开箱即用完全黑箱,但不知道为什么我的就出现这个问题,但我并没有做任何特殊操作(哭泣 !
    zjp
        5
    zjp  
       2019-03-06 00:21:49 +08:00 via Android   ❤️ 1
    上星期正好在写这个
    @JsonIgnore 应该不能满足需要,可以用 @JsonManagedReference、 @JsonBackReference,Jackson 会处理好循环引用
    用 Lombok 还要重写 equals 和 hashcode 方法,这里也循环引用了。麻烦的是 Tag 不同但其他属性都相同的两个 Article 应不应该是 equals,Tag 同理
    Allianzcortex
        6
    Allianzcortex  
    OP
       2019-03-06 00:37:59 +08:00
    @zjp 谢谢,这个方法和上面提到的链接里第一种方法是一样的,尝试了但还是不好用,很崩溃
    Allianzcortex
        7
    Allianzcortex  
    OP
       2019-03-06 00:57:19 +08:00
    卧槽... @zjp 感谢在先,这句 [用 Lombok 还要重写 equals 和 hashcode 方法,这里也循环引用了] 一句惊醒梦中人,已经在小数据集上测试成功了,== 让我重新整理一下
    Allianzcortex
        8
    Allianzcortex  
    OP
       2019-03-06 02:34:07 +08:00
    @zjp 太感谢了,问题已经解决。我的做法是重写了 @toString() 和 @hashCode() 后不会出现查询的错误,但预期仍然不是我想要的,循环返回 Json, 之后添加 @JsonMangedReference 和 @JsonBackReference 来定义 owner 和 target side,参考: https://www.baeldung.com/jackson-bidirectional-relationships-and-infinite-recursion 就完全解决了。真的太厉害了,只可惜金币只能加一次!👍👍
    airfling
        9
    airfling  
       2019-03-06 08:33:13 +08:00   ❤️ 2
    @NamedEntityGraph 这个注解结合 @EntityGraph 注解使用,其次对于多对一和一对一,在一的那一方注解加上属性 @ManyToOne(fetch = FetchType.LAZY)
    wc951
        10
    wc951  
       2019-03-06 11:56:00 +08:00 via Android   ❤️ 2
    多对多关系配置延迟加载,lombok 可以用 @tostring.exclude 排除,@EqualsAndHashCode 也可以设置 exclude,转 json 可以多加一个 dto
    mgcnrx11
        11
    mgcnrx11  
       2019-03-06 11:57:07 +08:00   ❤️ 1
    所以一般在我们那实践里,都不会直接把 Entity 做序列化返回,而是建一个 DTO 类去做返回。这是其中一个用 DTO 的理由
    Allianzcortex
        12
    Allianzcortex  
    OP
       2019-03-06 17:37:41 +08:00 via iPhone
    @wc951 谢谢😂 一会试一下新配置
    Allianzcortex
        13
    Allianzcortex  
    OP
       2019-03-06 17:40:18 +08:00 via iPhone
    @mgcnrx11 这是两件事呀~ 我在 @requestbody 这里用的也是 dto,但返回整个类引起循环引用问题不在这儿🤣
    zjp
        14
    zjp  
       2019-03-06 19:13:47 +08:00 via Android   ❤️ 1
    @airfling 既然讨论了我就伸手一回……
    在 Spring data JPA 里 fetchType.Lazy 不起作用有可能什么原因?我没对 hibernate 做任何配置…
    airfling
        15
    airfling  
       2019-03-07 08:25:44 +08:00   ❤️ 1
    @zjp 这个分两种的,一种是你直接用 jpaq 的语法写的方法,这个是 hibernate 的 hsql 查询机制,还有个是用 @query 注解写的 sql,这个是 jpa 自己的机制,这两个的实现机制不一样。你说的这种情况我之前也遇到过,具体解决方法现在忘了,一般解决思路就是把 showsql 打开,查看第一次查询使用的 sql 是否和自己预想的一样。然后多余的 sql 是在查询的时候产生的还是返回数据给前台产生的,返回数据给前台产生的就是检查 tostring 和 equal 方法和 json 序列化检查进行排除。调用产生的就是你用了这个表,但是你没查询出来,使用了 hibernate 的默认机制进行了 n+1 查询
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2975 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 14:13 · PVG 22:13 · LAX 06:13 · JFK 09:13
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.