【问题标题】:Spring Jpa Data Repository save (update) with LinkedEntity for ManyToMany relationshipSpring Jpa Data Repository 使用 LinkedEntity 保存(更新)多对多关系
【发布时间】:2018-07-05 13:27:46
【问题描述】:

有 2 个实体(比如说规则和标签)使用链接实体具有多对多关系 as per hibernate reference documentation
规则实体:

@Entity
@Table(name = "rule")
@JsonIdentityInfo(
    generator = ObjectIdGenerators.PropertyGenerator.class,
    property = "name")
public class Rule implements Serializable {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@NaturalId
@NotBlank
@Column(unique = true)
private String name;

@Lob
@Column(columnDefinition = "TEXT")
private String content;

@OneToMany(mappedBy = "rule", cascade = {CascadeType.PERSIST, 
    CascadeType.MERGE})
private List<RuleLabel> labels = new ArrayList<>();
...


标签实体:

@Entity
@Table(name = "label")
@JsonIdentityInfo(
    generator = ObjectIdGenerators.PropertyGenerator.class,
    property = "id")
public class Label implements Serializable {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@NotBlank
private String name;

@OneToMany(mappedBy = "label", cascade = {CascadeType.PERSIST, 
    CascadeType.MERGE})
private List<RuleLabel> rules = new ArrayList<>();
...


链接实体:

@Entity
public class RuleLabel implements Serializable {

@Id
@ManyToOne
private Rule rule;

@Id
@ManyToOne
private Label label;
...


存储库:

@Repository
public interface LabelRepository extends JpaRepository<Label, Long>
...
@Repository
public interface RuleRepository extends JpaRepository<Rule, Long>
...


通过 RuleRepository.save(Rule) 创建新实体可以正常工作,但是当我尝试更新现有实体时(相同的方法 RuleRepository.save(Rule),但实体要保存的包含 id 字段)它会导致 Hibernate: select... 查询的无限循环:

Hibernate: select rule0_.id as id1_7_1_, rule0_.is_active as is_activ2_7_1_, rule0_.content as content3_7_1_, rule0_.is_deleted as is_delet4_7_1_, rule0_.import_section as import_s5_7_1_, rule0_.name as name6_7_1_, rule0_.rule_configuration as rule_con7_7_1_, labels1_.rule_id as rule_id1_8_3_, labels1_.label_id as label_id2_8_3_, labels1_.rule_id as rule_id1_8_0_, labels1_.label_id as label_id2_8_0_ from rule rule0_ left outer join rule_label labels1_ on rule0_.id=labels1_.rule_id where rule0_.id=?

结果是 StackOverflowError

java.lang.StackOverflowError: null
at com.mysql.jdbc.ServerPreparedStatement.getInstance(ServerPreparedStatement.java:332)
...

(LabelRepository 的行为方式相同)
如何修复?
更新: 将获取策略更改为 Lazy 后

@Id
@ManyToOne(fetch = FetchType.LAZY)
private Rule rule;

@Id
@ManyToOne(fetch = FetchType.LAZY)
private Label label;

无限循环问题已经解决,但出现了新问题 - 相关实体没有被填充并且当 Hibernate 尝试将值插入链接表时

Hibernate: insert into rule_label (rule_id, label_id) values (?, ?)

我们得到

org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint [null]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement
...
Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Column 'rule_id' cannot be null

【问题讨论】:

  • 这两个实体的代码粘贴时间太长了吧?...
  • 对不起,我的错。添加代码sn-ps,谢谢。
  • 您是否有 toString 或打印出递归关系、RuleLabel 列表和标签的东西?
  • 当您询问导致特定异常的特定代码段时,请发布该代码,并发布异常堆栈跟踪。否则,我们只能猜测。
  • 只是一个猜测:如果你覆盖它,删除你的 toString() 方法。

标签: java hibernate spring-data-jpa


【解决方案1】:

好的,我一直使用EmbeddableId 来链接实体与 JPA。在使用级联为我完成工作方面,我没有尝试过您提到的休眠示例。这可能很有趣,但纯 JPA 和 Spring Data Repositories 之间存在一些差异。通过使用EmbeddableId,您可以为链接实体创建一个单独的弹簧存储库。然后你自己管理关系。如果您不想这样做,那么您应该使用ManyToMany 注释,但链接实体允许您创建链接实体属性,此处未显示。此代码将为您工作并让您到达 B 点,您可以从那里进行实验:

@Entity
public class Label {
    @Id @GeneratedValue private Long id;
    @OneToMany(mappedBy = "ruleLabelId.labelId")
    private List<RuleLabel> rules = new ArrayList<>();

@Entity
public class Rule {
    @Id @GeneratedValue private Long id;
    @OneToMany(mappedBy = "ruleLabelId.ruleId")
    private List<RuleLabel> labels = new ArrayList<>();

@Entity
public class RuleLabel {
    @EmbeddedId
    private RuleLabelId ruleLabelId;

@SuppressWarnings("serial")
@Embeddable
public class RuleLabelId implements Serializable {
    private Long ruleId;
    private Long labelId;

public interface RuleRepository extends JpaRepository<Rule, Long> {
    @Query("from Rule r left join fetch r.labels where r.id = :id")
    public Rule getWithLabels(@Param("id") Long id);
}

public interface RuleLabelRepository extends JpaRepository<RuleLabel, RuleLabelId> {}

并使用它:

Rule rule = new Rule();
Label label = new Label();

ruleRepo.save(rule);
labelRepo.save(label);

RuleLabel ruleLabel = new RuleLabel();
RuleLabelId ruleLabelId = new RuleLabelId();
ruleLabelId.setRuleId(rule.getId());
ruleLabelId.setLabelId(label.getId());
ruleLabel.setRuleLabelId(ruleLabelId);

ruleLabelRepo.save(ruleLabel);

rule = ruleRepo.getWithLabels(1L);
System.out.println(rule + Arrays.toString(rule.getLabels().toArray()));

【讨论】:

  • 奇怪的是,为了通过保存和更新保持一致的行为,我们需要进行如此剧烈的更改并手动管理链接实体。无论如何 - 非常感谢您,您的解决方案运行良好!
【解决方案2】:

是的,因为它是你告诉 hibernate 做的事情。

默认情况下,所有@ManyToOne@OneToOne 关联都已加载EAGER,因此当它查询Rule 时,它也会查询RuleLabel,然后里面又是Rule导致无限的select 查询。最好加载它们LAZY

你可以像这样@ManyToOne(fetch=FetchType.LAZY)进行字段延迟加载

这就是JPA 2.0 spec 关于默认值的说法:

OneToMany: LAZY
ManyToOne: EAGER
ManyToMany: LAZY
OneToOne: EAGER

一个很好的 read 延迟加载

【讨论】:

  • 默认情况下它们是 LAZY 加载的。您可以使用 @ManyToOne(fetch=FetchType.LAZY) 让它们延迟加载,但这不是多余的吗?
  • 谢谢,设置 @ManyToOne(fetch = FetchType.LAZY) 有助于避免无限循环,但它会导致另一个问题 - 相关实体没有被填充,我有'Caused by: com. mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException:列'rule_id'不能为空'
  • @K.Nicholas 在 Hibernate 中默认都是 LAZY 加载的,但是在 JPA ManyToOne 和 OneToOne 中是 EAGER 加载的
  • @DmitryTkachev 使用新异常和查询代码更新您的问题
  • @K.Nicholas 是的,你是对的,这也可能是一个问题,这就是为什么我让他输入更新查询代码以获得更好的主意。
猜你喜欢
  • 1970-01-01
  • 2015-05-10
  • 1970-01-01
  • 1970-01-01
  • 2019-10-01
  • 1970-01-01
  • 2018-08-21
  • 2021-08-08
  • 2018-04-14
相关资源
最近更新 更多