【问题标题】:JPA merge results in duplicate entry of foreign entityJPA 合并导致外国实体重复输入
【发布时间】:2012-10-05 20:27:06
【问题描述】:

问题:

有没有人知道如何在没有EntityManager 尝试重新插入外部实体的情况下合并?

场景:

只是为了设置一个与我的情况非常匹配的场景:我有两个实体

@Entity
@Table(name = "login", catalog = "friends", uniqueConstraints =
@UniqueConstraint(columnNames = "username"))
public class Login implements java.io.Serializable{

   private static final long serialVersionUID = 1L;
   @Id
   @GeneratedValue(strategy = IDENTITY)
   @Column(name = "id", unique = true, nullable = false)
   private Integer id;
   @Column(name = "username", unique = true, nullable = false, length = 50)
   private String username;
   @Column(name = "password", nullable = false, length = 250)
   private String password;
}

@Entity
@Table(name = "friendshiptype", catalog = "friends")
public class FriendshipType implements java.io.Serializable{

   private static final long serialVersionUID = 1L;
   @Id
   @GeneratedValue(strategy = IDENTITY)
   @Column(name = "id", unique = true, nullable = false)
   private Integer id;
   @OneToOne(fetch = FetchType.LAZY)
   @JoinColumn(name = "username")
   private Login login;
      @Column(name = "type", unique = true, length = 32)
   private String type;
   ...//other fields go here
}

Login 实体和FriendshipType 实体都分别持久化到数据库中。然后,稍后,我需要将Login 行与FriendshipType 行合并。当我调用entityManager.merge(friendship) 时,它会尝试插入一个新的Login,这当然会导致以下错误

Internal Exception: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry 'myUserName1350319637687' for key 'username'
Error Code: 1062
Call: INSERT INTO friends.login (password, username) VALUES (?, ?)

再次,我的问题是如何在不让 enityManager 尝试重新插入外来对象的情况下合并两个对象?

【问题讨论】:

  • 这可能与您的问题无关,但不应该是@JoinColumn(name = "id")。我见过的所有例子都是通过 id 属性加入的。
  • @Guido Simone 我尝试了你的建议,但没有改变。
  • 好的 - 感谢您告诉我。希望你能得到更好的答案。

标签: java jpa merge


【解决方案1】:

这是我解决问题的方法。我终于明白了合并没有解决的原因是因为 login.id 是由 JPA 自动生成的。因此,由于我真的不需要自动生成的 id 字段,因此我将其从架构中删除并使用 username 作为 @id 字段:

@Entity
@Table(name = "login", catalog = "friends", uniqueConstraints =
@UniqueConstraint(columnNames = "username"))
public class Login implements java.io.Serializable{

   private static final long serialVersionUID = 1L;
   @Id
   @Column(name = "username", unique = true, nullable = false, length = 50)
   private String username;
   @Column(name = "password", nullable = false, length = 250)
   private String password;
}

我想到的另一个解决方案,我没有实施,但如果他们需要一个自动生成的 id 字段,可能会对其他人有所帮助。

不要为合并创建Login 的实例,而是从数据库中获取实例。我的意思是,而不是

Login login = new Login(); login.setUsername(username); login.setPassword(password);

宁愿

Login login = loginDao.getByUsername(username);

这样,不会生成新的 id 字段,使实体看起来不同。

感谢大家的帮助并为大家投票,尤其是@mijer 的耐心。

【讨论】:

  • 我很高兴您找到了 kasavbere 解决方案,但从长远来看,我认为删除自动生成的 id 以使关系正常工作并不是一个好的设计权衡。花点时间让使用@ManyToOne 关系的想法深入您的脑海(这些阻抗不匹配问题比您想象的更常见,并且在您下次遇到其中一个时放弃 PK 可能不是解决方案)。跨度>
  • 我使用username 作为主键。正如我在替代解决方案中提到的那样,如果需要,我可以很好地保留自动生成的字段。我认为@ManyToOne 可能会产生误导,因为关系实际上是@OneToOne。尽管如此,任何一个都没有解决问题,我仍然在我的解决方案中使用@ManyToOne——我还没有改回来。
  • 很奇怪,@ManyToOne 注释应该可以解决问题,除非您使用 FriendshipType 保留未附加的新 Login 对象(您的第二个建议可以解决)。
【解决方案2】:

您可以使您的@JoinColumn 不可更新:

@JoinColumn(name = "login_id", updatable = false) // or
@JoinColumn(name = "username", referencedColumnName = "username", updatable= false) 

或者在合并FriendshipType之前尝试再次刷新/获取Login实体:

// either this
entityManager.refresh(friendship.getLogin());
// or this
final Login login = entityManager
          .getReference(Login.class, friendship.getLogin().getId());
friendship.setLogin(login);
// and then
entityManager.merge(friendship);

但是,正如其他人所建议的那样,我相信FriendshipType 可以更好地由@ManyToOne 关系或EmbeddableElementCollection 来表示


更新

另一种选择是更改拥有方:

public class Login implements java.io.Serializable {
    @OneToOne(fetch = FetchType.LAZY) 
    @JoinColumn(name = "friendshiptype_id")
    private FriendshipType friendshipType;
    // Other stuff 
}

public class FriendshipType implements java.io.Serializable {
    @OneToOne(fetch=FetchType.LAZY, mappedBy="friendshipType")
    private Login login;
    // Other stuff 
}

这会影响您的数据模型(登录表将有一个 friendshiptype_id 列,而不是相反),但会防止您遇到错误,因为关系始终由拥有方维护。

【讨论】:

    【解决方案3】:

    你试过cascade=MERGE吗? IE。

    @OneToOne(fetch = FetchType.LAZY, cascade=CascadeType.MERGE)
    @JoinColumn(name = "username")
    private Login login;
    

    更新

    另一种可能的选择是使用@ManyToOne(它的保存是因为关联是唯一的)

    @ManyToOne(fetch = FetchType.LAZY, cascade=CascadeType.MERGE)
    @JoinColumn(name = "username")
    private Login login;
    

    【讨论】:

    • 是否正确,friendshiptype.login 字段包含用户名而不是登录 ID?
    • 确实如此。但我已将其更改为@JoinColumn(name = "login_id"),在数据库架构中反映为CONSTRAINT FK_friendshiptype_login_id FOREIGN KEY (login_id) REFERENCES login (id)。但我仍然得到错误。
    • 你能用@ManyToOne代替@OneToOne吗?
    • 我刚刚尝试了 cascade={CascadeType.MERGE,CascadeType.PERSIST} 的不同排列——整个电源组。没有变化。
    【解决方案4】:

    您可以使用原始的@Id 设置来完成。即

     @Id
     @GeneratedValue(strategy = IDENTITY)
     @Column(name = "id", unique = true, nullable = false)
     private Integer id;
    

    可以,但无需更改为:

     @Id
     @Column(name = "username", unique = true, nullable = false, length = 50)
     private String username;
    

    诀窍是您必须首先通过 em.find(...) 或 em.createQuery(...) 从 DB 加载。然后保证用数据库中的正确值填充 id。

    然后,您可以通过结束事务(对于会话 bean 中的事务范围实体管理器),或通过调用 em.detach(ent) 或 em.clear(),或通过序列化实体并传递来分离实体它通过网络。

    然后您可以一直更新实体,同时保持原始 id 值。

    然后你可以调用 em.merge(ent) 并且你仍然会有正确的 id。但是,我相信该实体此时必须已经预先存在于实体管理器的持久上下文中,否则它会认为您有一个新实体(具有手动填充的 id),并尝试在事务刷新/提交时插入。

    所以第二个技巧是确保实体在合并点加载(通过 em.find(...) 或 em.query(...) 再次,如果你有一个新的持久上下文而不是原版)。

    :-)

    【讨论】:

      猜你喜欢
      • 2019-10-09
      • 2010-12-28
      • 1970-01-01
      • 2015-12-25
      • 1970-01-01
      • 2016-09-17
      • 2015-01-24
      • 2014-08-31
      • 2017-09-04
      相关资源
      最近更新 更多