简体   繁体   中英

JPA many-to-many relationship, keeping a list of ids

I have 2 POJO classes in Java: Phrase and Tag, in a many-to-many relationship:

  1. Phrase.java

     @Entity @EntityListeners(value={PhraseListener.class}) public class Phrase { @Id @GeneratedValue @Column(name="id") private Long phraseId; @Column(nullable=false) private String text; @ManyToMany(cascade=CascadeType.ALL) @JoinTable(name="phrase_has_tag", joinColumns={@JoinColumn(name="phrase_id",referencedColumnName="id")}, inverseJoinColumns={@JoinColumn(name="tag_uname",referencedColumnName="uname")}) private Collection<Tag> tagObjects; @Transient private Set<String> tags; public Phrase() { tagObjects = new ArrayList<Tag>(); tags = new HashSet<String>(); } // getters and setters // … public void addTagObject(Tag t) { if (!getTagObjects().contains(t)) { getTagObjects().add(t); } if (!t.getPhrases().contains(this)) { t.getPhrases().add(this); } } public void addTag(String tagName) { if (!getTags().contains(tagName)) { getTags().add(tagName); } } 
  2. Tag.java

     @Entity public class Tag { @Id @Column(name="uname") private String uniqueName; private String description; @ManyToMany(mappedBy="tagObjects") private Collection<Phrase> phrases; public Tag() { phrases = new ArrayList<Phrase>(); } // getters and setters // … 

The primary key for the tag entity is its name. I want to keep in Phrase.java a Set of tag names "synchronized" with the tagObjects field of the many-to-many relationship, and viceversa. For doing this, I add a listener to Phrase.java :

public class PhraseListener {
  @PostLoad
  public void postLoad(Phrase p) {
    System.out.println("In post load");
    for (Tag tag : p.getTagObjects()) {
      p.addTag(tag.getUniqueName());
    }        
  }

  @PrePersist
  public void prePersist(Phrase p) {
    System.out.println("In pre persist");
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("TestJPA");
    EntityManager em = emf.createEntityManager();
    for (String tagName : p.getTags()) {
      Tag t = em.find(Tag.class, tagName);
      if (t == null) t = new Tag(tagName);
      p.addTagObject(t);
    }
  }
}

which after loading, it creates the set of tag names from the tag objects and before persisting it reads the set of tag names, and fetch or create tag objects.

My problem is that if I try to create multiple phrases which share tags, JPA instead of only creating the relationship (insert into the join table) it also create tag objects which violate primary key constraint.

transaction.begin();  
Phrase p = new Phrase("Never ask what sort of computer a guy drives. If he's a Mac user, he'll tell you. If not, why embarrass him?", "Tom Clancy");
p.addTag("apple");
p.addTag("macintosh");
em.persist(p);
transaction.commit();

transaction.begin();  
p = new Phrase("It's better to be a pirate than to join the Navy.", "Steve Jobs");
p.addTag("apple");
em.persist(p);
transaction.commit();

Exception in thread "main" javax.persistence.RollbackException: Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.5.0.v20130507-3faac2b): org.eclipse.persistence.exceptions.DatabaseException Internal Exception: java.sql.SQLException: [SQLITE_CONSTRAINT] Abort due to constraint violation (column uname is not unique) Error Code: 0 Call: INSERT INTO TAG (uname, DESCRIPTION) VALUES (?, ?) bind => [apple, null]

You haven't shown your addTag method in Phrase, but I assume that you have somewhere in there an expression new Tag() and it would look somehow similar to this:

public void addTag(String tagName) {
   Tag tag = new Tag();
   tag.setUniqueName(tagName);
   tag.getPhrases().add(this); 
   this.tagObjects.add(tag);
}

In this case the method addTag will create new object of type Tag everytime the method is called, which will result in different entries in the relational table, cause Hibernate persists the whole objects, not only particular fields of theirs, regardless if these fields are primary keys or not. After calling the method addTag two times, you will create two different objects and Hibernate could not know if those two objects relate to the same entry in the DB or not. This means that even though they have the same uniqueName , they could have a different description.

Imagine the following scenario:

transaction.begin();  
Phrase p = new Phrase("Never ask what sort of computer a guy drives. If he's a Mac user, he'll tell you. If not, why embarrass him?", "Tom Clancy");
Tag t = new Tag();
t.setUniqueName("apple");
t.setDescription("This is an example apple");
p.getTagObjects().add(t);
em.persist(p);
transaction.commit();

transaction.begin();  
p = new Phrase("It's better to be a pirate than to join the Navy.", "Steve Jobs");
t = new Tag();
t.setUniqueName("apple");
t.setDescription("Another description of the apple");
em.persist(p);
transaction.commit();

With this example the difference should be more obvious and should illustrate why it is impossible to Hibernate to know when are you referring to the same entry in the DB with two or more different objects.

As a solution I would suggest you to change the method addTag so that it has the following signature public void addTag(Tag tag) {... and keep track of the existing tags somewhere centralized or you can try out em.merge(p); instead of em.persist(p);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM