0

I have a problem when PUTing or PATCHing my Profile entity via Spring Data Rest. The error happens when I PUT or PATCH the same or new data for the "skills" or the "jobs" property.

This is what curl returns:

{"cause":{"cause":{"cause":null,"message":"Unique index or primary key violation: \"PRIMARY_KEY_6E ON PUBLIC.TRAINER_PROFILE_JOBS(TRAINER_PROFILE_ID, JOBS_ID) VALUES (77, 79, 2)\"; SQL statement:\ninsert into trainer_profile_jobs (trainer_profile_id, jobs_id) values (?, ?) [23505-197]"},"message":"could not execute statement"},"message":"could not execute statement; SQL [n/a]; constraint [\"PRIMARY_KEY_6E ON PUBLIC.TRAINER_PROFILE_JOBS(TRAINER_PROFILE_ID, JOBS_ID) VALUES (77, 79, 2)\"; SQL statement:\ninsert into trainer_profile_jobs (trainer_profile_id, jobs_id) values (?, ?) [23505-197]]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement"}

The application produces the following output:

org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint ["PRIMARY_KEY_6E ON PUBLIC.TRAINER_PROFILE_JOBS(TRAINER_PROFILE_ID, JOBS_ID) VALUES (77, 79, 2)"; SQL statement:
insert into trainer_profile_jobs (trainer_profile_id, jobs_id) values (?, ?) ...
Caused by: org.h2.jdbc.JdbcSQLException: Unique index or primary key violation: "PRIMARY_KEY_6E ON PUBLIC.TRAINER_PROFILE_JOBS(TRAINER_PROFILE_ID, JOBS_ID) VALUES (77, 79, 2)"; SQL statement:
insert into trainer_profile_jobs (trainer_profile_id, jobs_id) values (?, ?) [23505-197]
    at org.h2.message.DbException.getJdbcSQLException(DbException.java:357) ~[h2-1.4.197.jar:1.4.197]
    at org.h2.message.DbException.get(DbException.java:179) ~[h2-1.4.197.jar:1.4.197]
    at org.h2.message.DbException.get(DbException.java:155) ~[h2-1.4.197.jar:1.4.197]
    at org.h2.index.BaseIndex.getDuplicateKeyException(BaseIndex.java:101) ~[h2-1.4.197.jar:1.4.197]
    at org.h2.mvstore.db.MVSecondaryIndex.requireUnique(MVSecondaryIndex.java:236) ~[h2-1.4.197.jar:1.4.197]
    at org.h2.mvstore.db.MVSecondaryIndex.add(MVSecondaryIndex.java:202) ~[h2-1.4.197.jar:1.4.197]
    at org.h2.mvstore.db.MVTable.addRow(MVTable.java:732) ~[h2-1.4.197.jar:1.4.197]
    at org.h2.command.dml.Insert.insertRows(Insert.java:182) ~[h2-1.4.197.jar:1.4.197]
    at org.h2.command.dml.Insert.update(Insert.java:134) ~[h2-1.4.197.jar:1.4.197]
    at org.h2.command.CommandContainer.update(CommandContainer.java:102) ~[h2-1.4.197.jar:1.4.197]
    at org.h2.command.Command.executeUpdate(Command.java:261) ~[h2-1.4.197.jar:1.4.197]
    at org.h2.jdbc.JdbcPreparedStatement.executeUpdateInternal(JdbcPreparedStatement.java:199) ~[h2-1.4.197.jar:1.4.197]
    at org.h2.jdbc.JdbcPreparedStatement.executeUpdate(JdbcPreparedStatement.java:153) ~[h2-1.4.197.jar:1.4.197]
    at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeUpdate(ProxyPreparedStatement.java:61) ~[HikariCP-2.7.9.jar:na]
    at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeUpdate(HikariProxyPreparedStatement.java) ~[HikariCP-2.7.9.jar:na]
    at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:175) ~[hibernate-core-5.2.17.Final.jar:5.2.17.Final]
    ... 149 common frames omitted

There's only one way currently to make it work: 1. PUT or PATCH with an empty array for "jobs" and "skills" 2. Then PUT or PATCH again with the new data

This is what the entity looks like before I try to PUT or PATCH data:

curl -s -XGET -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" http://localhost:2222/trainerProfiles/66
{
  ...
  "jobs" : [ {
    "period" : {
      "start" : "2015-01-02",
      "end" : "2017-01-25"
    },
    "company" : "Lorem ipsum dolor sit amet",
    "sector" : "Lorem ipsum dolor sit amet",
    "position" : "Lorem ipsum dolor sit amet",
    "description" : "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat."
  } ],
  "skills" : [ {
    "topic" : "java",
    "level" : "EXPERT"
  }, {
    "topic" : "spring",
    "level" : "EXPERT"
  } ],
  "_links" : {
    ...
  }
}%

As you can see, the skills and jobs properties are embedded into the profile entity since I did not create an exported JPA repository for them. This is intended.

To give you the full picture of my entities, I need to introduce some base classes that are inherited from:

public interface BaseEntity {
    public Long getId();    
    public void setId(Long id); 
}

@Data
@MappedSuperclass
public class AbstractBaseEntity implements BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
}

@Data
@EqualsAndHashCode(callSuper=true)
@MappedSuperclass
public abstract class AbstractOwnedEntity extends AbstractBaseEntity implements OwnedEntity {
    @NotNull
    @ManyToOne
    private Company owner;  
}

The is the entity exposed via Spring Data Rest which give me a headache:

@NoArgsConstructor
@Data
@EqualsAndHashCode(callSuper = true)
@Entity
public class Profile extends AbstractOwnedEntity {
    ...

    @NotNull
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    private Set<JobEntry> jobs = new HashSet<JobEntry>();

    @NotNull
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    private Set<SkillEntry> skills = new HashSet<SkillEntry>();

    public Profile(...) {
        ...
    }

    public void addJob(JobEntry job) {
        this.jobs.add(job);
    }

    public void addSkill(SkillEntry skill) {
        this.skills.add(skill);
    }
    ...
}

The JPE Repository is a very basic CrudRepository with some SPEL in the form of @PreAuthorize, @PostFilter, etc to implement access control (omitted here)

@RepositoryRestResource
public interface TrainerProfileRepository extends CrudRepository<TrainerProfile, Long> {
    ...
}
user3235738
  • 335
  • 4
  • 22
  • You repository refers to an Entity named `TrainerProfile` but you have posted code for entity named `Profile`??? – Alan Hay Oct 30 '18 at 09:46
  • Sorry my bad. I played with the names while I was putting this post together. They are the same entities. – user3235738 Nov 04 '18 at 09:10

1 Answers1

0

If I recall correctly, I think you need to expose the IDs for the entities which have are not exposed via Rest Repositories - in your case, Skills and Jobs: otherwise, how can the framework know if your PUT/PATCH requests are referring to an existing entity or not.

You can expose the Ids for these 2 classes in the configuration as outlined here:

How to expose the resourceId with Spring Data Rest

Alan Hay
  • 22,665
  • 4
  • 56
  • 110