0

QUESTIONS:

Does anyone know how to merge without having EntityManager trying to re-insert the foreign entity?

SCENARIO:

Just to set up a scenario that closely matches my case: I have two entities

@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
}

Both the Login entity and the FriendshipType entity are persisted to the database separately. Then, later, I need to merge a Login row with a FriendshipType row. When I call entityManager.merge(friendship), it tries to insert a new Login which of course results in the following error

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 (?, ?)

My question, again, is how do I merge two objects without having enityManager trying to reinsert the foreign object?

kasavbere
  • 5,873
  • 14
  • 49
  • 72

4 Answers4

5

Here is how I solve the problem. I finally figure the reason the merge is not resolving is because the login.id is auto generated by JPA. So since I really don't need an auto-generated id field, I remove it from the schema and use username as the @id field:

@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;
}

Another solution that occurred to me, which I didn't implement but may help someone else, should they need to have an auto-generated id field.

Instead of creating an instance of Login for the merger, get the instance from the database. What I mean is, instead of

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

Do rather

Login login = loginDao.getByUsername(username);

That way, a new id field is not generated making the entity seem different.

Thanks and up-votes to everyone for helping, especially to @mijer for being so patient.

kasavbere
  • 5,873
  • 14
  • 49
  • 72
  • I'm glad that you have found a solution kasavbere, but in the long term I don't think that removing the autogenerated id to make a relationship work was a good design trade-off. Take sometime to let the idea of using a `@ManyToOne` relationship sink into your head (those impedance mismatch problems are more common than you think, and dropping the PK may not be a solution the next time you hit one of those). – Anthony Accioly Oct 15 '12 at 22:31
  • I am using the `username` as the primary key. As I mention in the alternative solution, I could very well keep the auto-generated field if I needed it. I thought `@ManyToOne` might be misleading since the realationship is really `@OneToOne`. Still, either one didn't solve the problem, and I am still using `@ManyToOne` in my solution -- I haven't changed it back yet. – kasavbere Oct 15 '12 at 23:11
  • Weird, the `@ManyToOne` annotation should solve the problem unless you are persisting unattached new `Login` objects with your `FriendshipType` (which your second suggestion would solve). – Anthony Accioly Oct 16 '12 at 01:10
2

You can make your @JoinColumn non updatable:

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

Or try to refresh / fetch your Login entity again before merging the FriendshipType:

// 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);

But, as other suggested I belive that FriendshipType would be better represented by a @ManyToOne relationship or maybe by a Embeddable or ElementCollection


Update

Yet another option is to change the owning side:

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 
}

This will affect your data model (login table will have a friendshiptype_id column instead of the other way around), but will prevent the errors that you are getting, since relationships are always maintained by the owning side.

Anthony Accioly
  • 21,918
  • 9
  • 70
  • 118
1

Have you tried cascade=MERGE? I.e.

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

UPDATE

Another possible option is to use @ManyToOne (it's save as the association is unique)

@ManyToOne(fetch = FetchType.LAZY, cascade=CascadeType.MERGE)
@JoinColumn(name = "username")
private Login login;
szhem
  • 4,672
  • 2
  • 18
  • 30
  • 1
    Is it correct, that friendshiptype.login field contains username and not the login id? – szhem Oct 15 '12 at 21:10
  • That was true. But I have changed it to `@JoinColumn(name = "login_id")`, which is reflected in the database schema as `CONSTRAINT FK_friendshiptype_login_id FOREIGN KEY (login_id) REFERENCES login (id)`. But I still get the error. – kasavbere Oct 15 '12 at 21:18
  • 1
    Could you try `@ManyToOne` instead of `@OneToOne`? – szhem Oct 15 '12 at 21:24
  • I just tried it with different permutations of `cascade={CascadeType.MERGE,CascadeType.PERSIST}` -- the whole power set. No change. – kasavbere Oct 15 '12 at 21:50
0

You can do it with your original @Id setup. i.e.

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

You can, but you don't need to change to:

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

The trick is you must start by loading from the DB, via em.find(...) or em.createQuery(...). Then the id is guaranteed to be populated with the right value from the DB.

Then you can detach the entity by ending a transaction (for a transaction-scoped entity manager in a session bean), or by calling em.detach(ent) or em.clear(), or by serialising the entity and passing it over the network.

Then you can update the entity, all the while, keeping the original id value.

Then you can call em.merge(ent) and you will still have the correct id. However, I believe the entity must already pre-exist in the persistent context of the entity manager at this instant, otherwise it will think that you have a new entity (with manually populated id), and try to INSERT on transaction flush/commit.

So the second trick is to ensure the entity is loaded at the point of the merge (via em.find(...) or em.query(...) again, if you have a new persistent context and not the original).

:-)

Glen Best
  • 22,769
  • 3
  • 58
  • 74