简体   繁体   中英

Hibernate - ConstraintViolation when persisting entity

I have an entity that contains a relationship to another entity in a manner I've never had to encounter before, and I'm getting an exception: "org.hibernate.exception.ConstraintViolationException: could not execute statement".

The parent entity is called "Post". A post can contain several Keyword entities. Keyword entities are unique by value, that is, if two posts contain the same keyword, both posts reference the same keyword entity.

My thought process was that there are many posts, each referencing many keywords, and any one keyword can be referenced by multiple posts, so it should be an @ManyToMany relationship. Obviously, it's not working. Inspecting the database shows that it is successfully persisting a few posts before it starts failing. As long as all the keywords are unique, it seems to be fine, but I'm thinking that it is dying whenever it's trying to persist a post with a keyword that is already being referenced by another post. Not sure how to fix this.

Here is what the classes look like (short version):

Post:

@Entity
public class Post implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "post_id_seq")
    @SequenceGenerator(name = "post_id_seq", sequenceName = "post_id_seq", allocationSize = 1)
    private Long id;

    @ElementCollection(fetch = FetchType.EAGER)
    @ManyToMany(cascade = CascadeType.ALL)
    private Set<Keyword> keywords = new HashSet<>();
}

Keyword:

@Entity
public class Keyword implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "keyword_id_seq")
    @SequenceGenerator(name = "keyword_id_seq", sequenceName = "keyword_id_seq", allocationSize = 1)
    private Long id;

    @Column(name = "KEYWORD_VALUE")
    private String value;
    private int count = 1;
}

UPDATE:

Here is the code I use in my service class to add a keyword to a post. Basically I have a Post object already that has Keywords filled in (request comes in via AJAX from a web front end and Spring unmarshals it automatically to a Post object). I have to loop through each keyword and see if an entity with the same value already exists in persistence. If so, increment the count for that keyword, merge it, then add that entity to the set that will end up replacing the Set that came in the request. If it doesn't already exist, I just use the Keyword that came in the request. Previously, I wasn't saving/merging the Keywords independently before adding them to the Post and persisting the post, but I started getting errors stating:

org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: com.saic.jswe.clients.swtc.domain.social.Keyword

Anyway, here is my service code:

public void addPost(Post post){
Set<Keyword> keywords = new HashSet<>();
        for (Keyword keyword : post.getKeywords()) {
            Keyword persistedKeyword = keywordDao.findByValue(keyword.getValue());
            if (persistedKeyword != null) {
                persistedKeyword.setCount(persistedKeyword.getCount() + 1);
                keywordDao.merge(persistedKeyword);
                keywords.add(persistedKeyword);

            } else {
                keywordDao.persist(keyword);
                keywords.add(keyword);
            }
        }
        post.setKeywords(keywords);
postDao.persist(post);
}

Also, during my testing when I'm getting this error, it's just a single thread attempting to add test Post objects one at a time.

Checking the logs, here is the actual constraint violation:

rg.postgresql.util.PSQLException: ERROR: insert or update on table "keyword" violates foreign key constraint "fk_3tcnkw7v196mudsgmy3nriibl" Detail: Key (id)=(1) is not present in table "post".

Hmmm... per the above code, it should only be adding a reference to a Keyword object with an ID if it did in fact find it in persistence. The keyword objects coming in with the Post object via the request should all have null IDs as they're not yet persisted.

I found where the issue came into play. A join table was being created called "post_keywords". It had 2 columns, one called "post" and one called "keyword". Each row represented the ID of a post and the ID of a keyword contained in that post. If there were multiple keywords in a post, there could be duplicate entries in the post column. However, as soon as a different post entity attempted to reference a keyword that was already used, it would complain about that ID already being present. Here's the visual:

post | keyword
-----+--------
 1   |   1
 1   |   2
 1   |   4
 2   |   3
 2   |   4   <--- this would be a problem since keyword 4 is already related to post 1

So my knowledge/understanding of JPA is pretty weak, but I've only ever needed real basic relationships. Given that I understood where the problem was happening, I decided to quit playing and experimenting and start reading.

For a minute, I thought I found a solution just using a OneToMany relationship, because I didn't necessarily care or need the keyword entity to directly know which posts reference it. This was incorrect, however. I could get that code to execute without error, but I ended up with each keyword only being owned by one entity. As each post tried to reference that keyword, it would just override the previous ownership of the keyword. Anyway, I really did need a ManyToMany relationship.

I ended up finding examples ( http://en.wikibooks.org/wiki/Java_Persistence/ManyToMany ) showing tables where the multiple child entities reference the same parent entity, so I just implemented the same JPA attributes in my code and viola, it worked. Here is what the code looks like now:

@Entity
public class Post implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "post_id_seq")
    @SequenceGenerator(name = "post_id_seq", sequenceName = "post_id_seq", allocationSize = 1)
    @Column(name="POST_ID")
    private Long id;

    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(
      name="POST_KEYWORD",
      joinColumns={@JoinColumn(name="POST_ID", referencedColumnName="POST_ID")},
      inverseJoinColumns={@JoinColumn(name="KEYWORD_ID", referencedColumnName="ID")})
    private Set<Keyword> keywords = new HashSet<>();
}

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