I have been trying to get a RESTful interface going with Spring boot but have hit a problem.
When POST'ing the following JSON I get an org.hibernate.TransientObjectException exception (please see below) that indicates that the sub-object is not being saved.
On hand tracing it appears that the JSON is correctly rendered into domain objects but when the save() method (SimpleJpaRepository) is called it doesn't attempt to recurse down through any sub-objects. Is this intentional? If so, what is the correct approach to configuring a @RepositoryRestResource such that it will save sub-objects?
I have a minimal spring.boot application which I will happily provide if it would help with diagnosis.
I have been looking around for a working example that has a sub-object (relation) but I haven't found one yet. The spring.io example was great for getting me going but I have become a bit stuck expanding on it.
Code snippets follow below:
JSON being posted:
{
"name" : "Test Sample Group",
"description" : null,
"projectCode" : null,
"creator" : "user001",
"createdDate" : 1395130128971,
"lastModifiedDate" : 1395130128971,
"samples" : [ {
"userPreferredId" : "S00012223434",
"wtsiUID" : "99997853483845",
"synonyms" : [ "ABC12345", "Humgen-0011232233", "1200088132734888234" ]
}, {
"userPreferredId" : "S000634734588",
"wtsiUID" : "34583934085358",
"synonyms" : [ "4875345993599934", "Humgen-004537682", "ABC674534" ]
} ]
}
Which is posted using the following command:
curl -i -X POST -H "Content-Type:application/json" --data @postdata.txt http://localhost:8080/samplegroup/
The exception thrown is as follows:
org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: uk.ac.sanger.mig.samplegroup.domain.Sample
at** org.hibernate.engine.internal.ForeignKeys.getEntityIdentifierIfNotUnsaved(ForeignKeys.java:294)
at org.hibernate.type.EntityType.getIdentifier(EntityType.java:510)
at org.hibernate.type.ManyToOneType.nullSafeSet(ManyToOneType.java:165)
at org.hibernate.persister.collection.AbstractCollectionPersister.writeElement(AbstractCollectionPersister.java:899)
at org.hibernate.persister.collection.AbstractCollectionPersister.recreate(AbstractCollectionPersister.java:1308)
at org.hibernate.persister.collection.OneToManyPersister.recreate(OneToManyPersister.java:184)
at org.hibernate.action.internal.CollectionRecreateAction.execute(CollectionRecreateAction.java:67)
at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:453)
at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:345)
at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:350)
at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:56)
at org.hibernate.internal.SessionImpl.flush(SessionImpl.java:1218)
at org.hibernate.internal.SessionImpl.managedFlush(SessionImpl.java:421)
at org.hibernate.engine.transaction.internal.jdbc.JdbcTransaction.beforeTransactionCommit(JdbcTransaction.java:101)
at org.hibernate.engine.transaction.spi.AbstractTransactionImpl.commit(AbstractTransactionImpl.java:177)
at org.hibernate.jpa.internal.TransactionImpl.commit(TransactionImpl.java:77)
at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:515)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:757)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:726)
at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:478)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:272)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:95)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:136)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.data.jpa.repository.support.LockModeRepositoryPostProcessor$LockModePopulatingMethodIntercceptor.invoke(LockModeRepositoryPostProcessor.java:92)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:207)
at com.sun.proxy.$Proxy67.save(Unknown Source)
at org.springframework.data.rest.core.invoke.CrudRepositoryInvoker.invokeSave(CrudRepositoryInvoker.java:106)
at org.springframework.data.rest.webmvc.RepositoryEntityController.createAndReturn(RepositoryEntityController.java:339)
at org.springframework.data.rest.webmvc.RepositoryEntityController.postEntity(RepositoryEntityController.java:177)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at org.springframework.web.method.support.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:215)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:132)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:104)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandleMethod(RequestMappingHandlerAdapter.java:749)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:690)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:83)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:945)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:876)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:961)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:863)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:646)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:837)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:727)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:303)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:77)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:108)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:220)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:122)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:501)
at org.apache.catalina.valves.RemoteIpValve.invoke(RemoteIpValve.java:683)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:170)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:98)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:116)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:408)
at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1040)
at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:607)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1721)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1679)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:744)
The RESTful repository definition is as follows:
package uk.ac.sanger.mig.samplegroup.repository;
import java.util.List;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import uk.ac.sanger.mig.samplegroup.domain.SampleGroup;
@RepositoryRestResource(collectionResourceRel = "samplegroup", path = "samplegroup")
public interface SampleGroupRepository extends PagingAndSortingRepository<SampleGroup, Long> {
List<SampleGroup> findByName(@Param("name") String name);
List<SampleGroup> findByCreator(@Param("name") String name);
}
Entity Definitions are as follows:
package uk.ac.sanger.mig.samplegroup.domain;
import java.util.Date;
import java.util.Set;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;
import javax.persistence.SequenceGenerator;
@Entity
public class SampleGroup {
// Database primary key use only.
@Id
@GeneratedValue(strategy=GenerationType.SEQUENCE, generator="SAMPLE_GROUP_SEQ")
@SequenceGenerator(
name="SAMPLE_GROUP_SEQ",
sequenceName="SAMPLE_GROUP_SEQ",
allocationSize=1
)
private Long id;
@Column(unique=true, nullable=false)
private String name;
private String description;
private String projectCode;
private String creator;
private Date createdDate;
private Date lastModifiedDate;
@OneToMany
@JoinColumn(name="SAMPLE_FK")
private Set<Sample> samples;
public SampleGroup() {}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCreator() {
return creator;
}
public void setCreator(String creator) {
this.creator = creator;
}
public Date getCreatedDate() {
return createdDate;
}
public void setCreatedDate(Date createdDate) {
this.createdDate = createdDate;
}
public Date getLastModifiedDate() {
return lastModifiedDate;
}
public void setLastModifiedDate(Date lastModifiedDate) {
this.lastModifiedDate = lastModifiedDate;
}
public Set<Sample> getSamples() {
return samples;
}
public void setSamples(Set<Sample> samples) {
this.samples = samples;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getProjectCode() {
return projectCode;
}
public void setProjectCode(String projectCode) {
this.projectCode = projectCode;
}
@Override
public String toString() {
return "SampleGroup [id=" + id + ", name=" + name + ", description="
+ description + ", projectCode=" + projectCode + ", creator="
+ creator + ", createdDate=" + createdDate
+ ", lastModifiedDate=" + lastModifiedDate + ", samples="
+ samples + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result
+ ((projectCode == null) ? 0 : projectCode.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
SampleGroup other = (SampleGroup) obj;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
if (projectCode == null) {
if (other.projectCode != null)
return false;
} else if (!projectCode.equals(other.projectCode))
return false;
return true;
}
}
package uk.ac.sanger.mig.samplegroup.domain;
import java.util.Set;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.SequenceGenerator;
@Entity
public class Sample {
// Database primary key use only.
@Id
@GeneratedValue(strategy=GenerationType.SEQUENCE, generator="SAMPLE_SEQ")
@SequenceGenerator(
name="SAMPLE_SEQ",
sequenceName="SAMPLE_SEQ",
allocationSize=1
)
private Long id;
private String userPreferredId; // What the buisiness user prefers to call the sample.
@Column(unique=true, nullable=false)
private String wtsiUID; // must be unique within WTSI (primary key)
@ElementCollection
private Set<String> synonyms;
public Sample() {}
public String getUserPreferredId() {
return userPreferredId;
}
public void setUserPreferredId(String userPreferredId) {
this.userPreferredId = userPreferredId;
}
public String getWtsiUID() {
return wtsiUID;
}
public void setWtsiUID(String wtsiUID) {
this.wtsiUID = wtsiUID;
}
public Set<String> getSynonyms() {
return synonyms;
}
public void setSynonyms(Set<String> synonyms) {
this.synonyms = synonyms;
}
@Override
public String toString() {
return "Sample [id=" + id + ", userPreferredId=" + userPreferredId
+ ", wtsiUID=" + wtsiUID + ", synonyms=" + synonyms + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((wtsiUID == null) ? 0 : wtsiUID.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Sample other = (Sample) obj;
if (wtsiUID == null) {
if (other.wtsiUID != null)
return false;
} else if (!wtsiUID.equals(other.wtsiUID))
return false;
return true;
}
}