My objective is to download Issue data of Apache projects using the Jira REST api and store the fetched objects using Spring with JPA (Hibernate).
The Issue entity can be linked with Developer entities using several relations such as Creator, Reporter, Assignee etc. This means that when receiving the JSON from the REST remote endpoint, the parsing will produce different objects of type Developer that can relate to the same Developer entity (i.e. if the Developer is both the assignee and the reporter). Besides, several issues can refer to same developer (i.e. a Developer is assigned to work to multiple issues).
Given a list of Issue keys, what I would like to achieve is, for each issue:
- Download corresponding JSON from Jira REST remote endpoint;
- Parse JSON into a Issue entity (along with referred child objects such as Developers)
- Store entities in DB
I don’t care for overwriting pre-existent entities (i.e. Developers already mentioned in previous issues) since I’m assuming that data in remote Jira DB is consistent.
When trying to save in db this issue alone, I’m getting the exception mentioned in the title (full stacktrace below).
From the stacktrace it seems that the problem is caused by the Developer entity referring to user vkorehov
, which is both Creator and Reportes for this issue. So the problem seems to be that the parser (GSON) creates two different entities with same primary key that cannot both be stored in DB since they would break the unique constraint.
Can I tell Spring to merge all Developer object with same Id into the same entity? Isn’t it be the default behaviour of Spring? Should I resolve by myself all duplicates of Developer objects before storing the Issue entity in DB??? 🙁
Full stacktrace:
org.springframework.dao.DuplicateKeyException: A different object with the same identifier value was already associated with the session : [it.torkin.dataminer.entities.jira.Developer#vkorehov]
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:304)
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:244)
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:550)
at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61)
at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:335)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:160)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:165)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223)
at jdk.proxy2/jdk.proxy2.$Proxy132.save(Unknown Source)
at it.torkin.dataminer.control.dataset.ApachejitController.loadIssues(ApachejitController.java:78)
at it.torkin.dataminer.control.dataset.ApachejitController.loadDataset(ApachejitController.java:178)
at it.torkin.dataminer.DatasetControllerTest.testLoadDataset(DatasetControllerTest.java:31)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:76)
at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:84)
at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:252)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:93)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:40)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:530)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:758)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:453)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:211)
Caused by: org.hibernate.NonUniqueObjectException: A different object with the same identifier value was already associated with the session : [it.torkin.dataminer.entities.jira.Developer#vkorehov]
at org.hibernate.event.internal.AbstractSaveEventListener.entityKey(AbstractSaveEventListener.java:237)
at org.hibernate.event.internal.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:219)
at org.hibernate.event.internal.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:137)
at org.hibernate.event.internal.DefaultPersistEventListener.entityIsTransient(DefaultPersistEventListener.java:175)
at org.hibernate.event.internal.DefaultPersistEventListener.persist(DefaultPersistEventListener.java:93)
at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:77)
at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:138)
at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:799)
at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:747)
at org.hibernate.engine.spi.CascadingActions$7.cascade(CascadingActions.java:290)
at org.hibernate.engine.spi.CascadingActions$7.cascade(CascadingActions.java:280)
at org.hibernate.engine.internal.Cascade.cascadeToOne(Cascade.java:517)
at org.hibernate.engine.internal.Cascade.cascadeAssociation(Cascade.java:439)
at org.hibernate.engine.internal.Cascade.cascadeProperty(Cascade.java:224)
at org.hibernate.engine.internal.Cascade.cascadeComponent(Cascade.java:410)
at org.hibernate.engine.internal.Cascade.cascadeProperty(Cascade.java:245)
at org.hibernate.engine.internal.Cascade.cascadeComponent(Cascade.java:410)
at org.hibernate.engine.internal.Cascade.cascadeProperty(Cascade.java:245)
at org.hibernate.engine.internal.Cascade.cascade(Cascade.java:157)
at org.hibernate.event.internal.AbstractSaveEventListener.cascadeBeforeSave(AbstractSaveEventListener.java:487)
at org.hibernate.event.internal.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.java:303)
at org.hibernate.event.internal.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:224)
at org.hibernate.event.internal.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:137)
at org.hibernate.event.internal.DefaultPersistEventListener.entityIsTransient(DefaultPersistEventListener.java:175)
at org.hibernate.event.internal.DefaultPersistEventListener.persist(DefaultPersistEventListener.java:93)
at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:77)
at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:54)
at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:127)
at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:757)
at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:741)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:319)
at jdk.proxy2/jdk.proxy2.$Proxy126.persist(Unknown Source)
at org.springframework.data.jpa.repository.support.SimpleJpaRepository.save(SimpleJpaRepository.java:629)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:354)
at org.springframework.data.repository.core.support.RepositoryMethodInvoker$RepositoryFragmentMethodInvoker.lambda$new$0(RepositoryMethodInvoker.java:277)
at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:170)
at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:158)
at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:516)
at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:285)
at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:628)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:168)
at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:143)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:70)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:379)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:138)
... 42 more
My model looks like this (only relevant parts listed):
@Entity
public class Issue {
@Id
@GeneratedValue
private long id;
private String key; // "PROJ-123"
/**
* Issue details mapped to the attributes obtainable from the
* Jira REST API.
*/
@Embedded
private IssueDetails details;
}
@Embeddable
@Data
public class IssueDetails {
@SerializedName("id")
@Column(unique = true) private String jiraId;
@SerializedName("key")
private String self; // link to issue in Jira
@Embedded private IssueFields fields;
}
@Embeddable
@Data
public class IssueFields{
@Column(columnDefinition = "text")
private String description;
private int upTimestampd;
private String summary; // issue title
private Timestamp resolutionTimestamp;
private Developer assignee;
@ManyToOne(cascade = { CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.LAZY)
private Developer creator;
@ManyToOne(cascade = { CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.LAZY)
private Developer reporter;
@ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.LAZY)
private List<Component> components;
}
@Data
@Entity
public class Developer{
private String accountType;
private boolean active;
@Embedded
private AvatarUrls avatarUrls;
private String displayName;
@Id
private String key;
private String name;
private String self;
private String timeZone;
}
database properties:
spring.datasource.driverClassName=org.postgresql.Driver
spring.jpa.properties.hibernate.event.merge.entity_copy_observer=allow
spring.jpa.hibernate.ddl-auto=create
spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults=false
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
Any help would be greatly appreciated, thank you all.
Exception triggers when launching the following test:
public class DatasetControllerTest {
@Autowired private IDatasetController datasetController;
@Test
public void testLoadDataset() throws UnableToLoadDatasetException{
datasetController.loadDataset();
log.info(datasetController.getDataset().toString());
}
}
@Service
@Slf4j
public class ApachejitController implements IDatasetController{
@Autowired private ApachejitConfig apachejitConfig;
@Autowired private JiraConfig jiraConfig;
@Autowired private CommitDao commitDao;
@Autowired private IssueDao issueDao;
@Autowired private DatasetDao datasetDao;
private Dataset dataset;
private void loadIssues(ApachejitDao apachejitDao, JiraDao jiraDao) throws UnableToLoadIssuesException {
/**
* - get the path of the issues folder from the configuration
* - list all csvs in the folder
* - for each csv
* - load each issue record from csv
* - for each record
* - transform record into an Issue object
* - fetch issue details from Jira API
* - store IssueDetails reference in Issue
* - load Commit matching the hash commit in Issue
* - store Commit reference in Issue
* - save Issue in the database
*/
List<Resultset<IssueRecord>> issues;
Issue issue;
IssueRecord record;
int skipped;
try {
issues = apachejitDao.getAllIssues(apachejitConfig.getIssuesPath());
for (int i = 0; i < issues.size(); i++) {
try (Resultset<IssueRecord> projectIssues = issues.get(i)) {
while (projectIssues.hasNext()) {
record = projectIssues.next();
// skip commit if it is already in db, but only if
// we do not have to refresh the db
if (!issueDao.existsByKey(record.getIssue_key())
|| apachejitConfig.isRefresh()) {
try {
issue = new Issue();
issue.setKey(record.getIssue_key());
linkIssueDetails(issue, jiraDao);
linkCommit(issue, record.getCommit_id());
issueDao.save(issue); // <-- THROWS EXCEPTION!!
dataset.setNrIssues(dataset.getNrIssues() + 1);
} catch (UnableToLinkIssueDetailsException | CommitNotFoundException e) {
log.warn(String.format("Skipping issue %s: %s", record.getIssue_key(), e.getMessage()));
dataset.getSkippedIssuesKeys().add(record.getIssue_key());
}
}
}
}
}
skipped = dataset.getSkippedIssuesKeys().size();
if (skipped > 0) {
log.warn(String.format("Skipped %d issues", skipped));
}
} catch (UnableToGetIssuesException | IOException e) {
throw new UnableToLoadIssuesException(e);
}
}
private void linkIssueDetails(Issue issue, JiraDao jiraDao) throws UnableToLinkIssueDetailsException {
try {
IssueDetails details = jiraDao.queryIssueDetails(issue.getKey());
issue.setDetails(details);
} catch (UnableToGetIssueException e) {
throw new UnableToLinkIssueDetailsException(e);
}
}
private void linkCommit(Issue issue, String commitHash) throws CommitNotFoundException {
Commit commit = commitDao.findByHash(commitHash);
if (commit == null){
throw new CommitNotFoundException(issue, commitHash);
}
issue.setCommit(commit);
}
/*
* Loads apachejit dataset from the filesystem into the local db,
* fetching issue details from Jira API
*
* @return a @Code Dataset entity with some stats about the dataset
*/
@Override
public void loadDataset() throws UnableToLoadDatasetException {
ApachejitDao apachejitDao;
JiraDao jiraDao;
if(apachejitConfig.isSkipLoad()) return;
try {
if(dataset == null){
dataset = new Dataset();
dataset.setName("apachejit");
}
apachejitDao = new ApachejitDao();
jiraDao = new JiraDao(
jiraConfig.getHostname(),
jiraConfig.getApiVersion());
loadCommits(apachejitDao);
loadIssues(apachejitDao, jiraDao);
datasetDao.save(dataset);
} catch (UnableToLoadCommitsException | UnableToLoadIssuesException e) {
dataset = null;
throw new UnableToLoadDatasetException(e);
}
}
}