Cannot create a repository for an entity with a shared key [DATAJPA-649]
Mauro Molinari opened DATAJPA-649 and commented
I have an entity:
@Entity
public class EntityA {
@Id
private Long id;
// getters/setters omitted
}
and another one with a shared primary key:
@Entity
public class EntityB {
@Id
@OneToOne
private EntityA entityA;
// getter/setters omitted
}
I'm trying to define a repository for EntityB:
public interface EntityBRepository extends JpaRepository<EntityB, EntityA> {
}
Apart from the fact that in order for this to be correct, EntityA must be serializable, even if it is I get the following error at runtime when the application starts (note I'm using EclipseLink 2.5.2):
Caused by: java.lang.IllegalArgumentException: Expected id attribute type [class java.lang.Long] on the existing id attribute [SingularAttributeImpl[EntityTypeImpl@1077297176:EntityA [ javaType: class com.example.EntityA descriptor: RelationalDescriptor(com.example.EntityA --> [DatabaseTable(EntityA)]), mappings: 9],org.eclipse.persistence.mappings.OneToOneMapping[entityA]]] on the identifiable type [EntityTypeImpl@512803841:EntityB [ javaType: class com.example.EntityB descriptor: RelationalDescriptor(com.example.EntityB --> [DatabaseTable(entityB)]), mappings: 7]] but found attribute type [class com.example.EntityA].
at org.eclipse.persistence.internal.jpa.metamodel.IdentifiableTypeImpl.getId(IdentifiableTypeImpl.java:200) ~[org.eclipse.persistence.jpa-2.5.2.jar:?]
at org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformation$IdMetadata.<init>(JpaMetamodelEntityInformation.java:222) ~[spring-data-jpa-1.7.1.RELEASE.jar:?]
at org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformation.<init>(JpaMetamodelEntityInformation.java:79) ~[spring-data-jpa-1.7.1.RELEASE.jar:?]
at org.springframework.data.jpa.repository.support.JpaEntityInformationSupport.getMetadata(JpaEntityInformationSupport.java:65) ~[spring-data-jpa-1.7.1.RELEASE.jar:?]
at org.springframework.data.jpa.repository.support.JpaRepositoryFactory.getEntityInformation(JpaRepositoryFactory.java:145) ~[spring-data-jpa-1.7.1.RELEASE.jar:?]
at org.springframework.data.jpa.repository.support.JpaRepositoryFactory.getTargetRepository(JpaRepositoryFactory.java:89) ~[spring-data-jpa-1.7.1.RELEASE.jar:?]
at org.springframework.data.jpa.repository.support.JpaRepositoryFactory.getTargetRepository(JpaRepositoryFactory.java:69) ~[spring-data-jpa-1.7.1.RELEASE.jar:?]
at org.springframework.data.repository.core.support.RepositoryFactorySupport.getRepository(RepositoryFactorySupport.java:177) ~[spring-data-commons-1.9.1.RELEASE.jar:?]
at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.initAndReturn(RepositoryFactoryBeanSupport.java:239) ~[spring-data-commons-1.9.1.RELEASE.jar:?]
at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.afterPropertiesSet(RepositoryFactoryBeanSupport.java:225) ~[spring-data-commons-1.9.1.RELEASE.jar:?]
at org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean.afterPropertiesSet(JpaRepositoryFactoryBean.java:92) ~[spring-data-jpa-1.7.1.RELEASE.jar:?]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1627) ~[spring-beans-4.1.2.RELEASE.jar:4.1.2.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1564) ~[spring-beans-4.1.2.RELEASE.jar:4.1.2.RELEASE]
... 21 more
I tried to change the repository definition as such:
public interface EntityBRepository extends JpaRepository<EntityB, Long> {
}
and change the code that invokes findOne(ID) on that repository as such:
// instead of: entityBRepository.findOne(entityAInstance);
entityBRepository.findOne(entityAInstance.getId());
but the same error is produced when the repository gets initialized.
I then tried to change EntityB definition as such:
@Entity
public class EntityB {
@Id
private Long id;
@MapsId
@OneToOne
private EntityA entityA;
// getter/setters omitted
}
while keeping Long as the ID type in EntityBRepository definition, but still the same error is produced. So, I can't produce a repository for EntityB in any way :-(
Affects: 1.7.1 (Evans SR1)
13 votes, 15 watchers
William Gorder commented
This is annoying the exact use case of
@Entity
public class EntityB {
@Id
private Long id;
@MapsId
@OneToOne
private EntityA entityA;
// getter/setters omitted
}
Is listed right on the @OneToOne Javadoc and Spring data JPA does not support it?
https://docs.oracle.com/javaee/6/api/javax/persistence/OneToOne.html
Mauro Molinari commented
Another similar problem is when you have a compound primary key made of a basic attribute and a related entity, although using a proper id class:
@Entity
public class Foo {
@Id
private Long id;
}
@Entity
@IdClass(BarId.class)
public class Bar {
public static class BarId implements Serializable {
private Long foo;
private MyEnum type;
protected BarId() {}
public BarId(Long foo, MyEnum type) {
this.foo = foo; this.type = type;
}
// getters omitted
}
@Id
@ManyToOne
private Foo foo;
@Id
@Enumerated(EnumType.STRING)
private MyEnum type;
}
The following repository, which I would expect to be declared correctly, produces an error:
public interface BarRepository
extends JpaRepository<Bar, BarId> {
}
The error given on startup is:
java.lang.IllegalArgumentException: Expected id attribute type [class com.example.Bar$BarId] on the existing id attribute [SingularAttributeImpl[BasicTypeImpl@1836242833:MyEnum [ javaType: class com.example.Bar$MyEnum],org.eclipse.persistence.mappings.DirectToFieldMapping[type-->Bar.type]]] on the identifiable type [EntityTypeImpl@930128389:Bar [ javaType: class com.example.Bar descriptor: RelationalDescriptor(com.example.Bar --> [DatabaseTable(Bar)]), mappings: 9]] but found attribute type [class com.example.Bar$MyEnum].
at org.eclipse.persistence.internal.jpa.metamodel.IdentifiableTypeImpl.getId(IdentifiableTypeImpl.java:200) ~[IdentifiableTypeImpl.class:?]
at org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformation$IdMetadata.<init>(JpaMetamodelEntityInformation.java:223) ~[JpaMetamodelEntityInformation$IdMetadata.class:?]
at org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformation.<init>(JpaMetamodelEntityInformation.java:79) ~[JpaMetamodelEntityInformation.class:?]
at org.springframework.data.jpa.repository.support.JpaEntityInformationSupport.getEntityInformation(JpaEntityInformationSupport.java:67) ~[JpaEntityInformationSupport.class:?]
at org.springframework.data.jpa.repository.support.JpaRepositoryFactory.getEntityInformation(JpaRepositoryFactory.java:146) ~[JpaRepositoryFactory.class:?]
at org.springframework.data.jpa.repository.support.JpaRepositoryFactory.getTargetRepository(JpaRepositoryFactory.java:90) ~[JpaRepositoryFactory.class:?]
at org.springframework.data.jpa.repository.support.JpaRepositoryFactory.getTargetRepository(JpaRepositoryFactory.java:70) ~[JpaRepositoryFactory.class:?]
at org.springframework.data.repository.core.support.RepositoryFactorySupport.getRepository(RepositoryFactorySupport.java:171) ~[RepositoryFactorySupport.class:?]
at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.initAndReturn(RepositoryFactoryBeanSupport.java:239) ~[RepositoryFactoryBeanSupport.class:?]
at org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport.afterPropertiesSet(RepositoryFactoryBeanSupport.java:225) ~[RepositoryFactoryBeanSupport.class:?]
at org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean.afterPropertiesSet(JpaRepositoryFactoryBean.java:92) ~[JpaRepositoryFactoryBean.class:?]
So, it seems like only very-basic primary key types are supported to create JPA repositories, which is a strong limit :-( Is there any plan to improve this? (I tested this with Spring Data JPA 1.8.2 and 1.9.1)
Mario Ceste commented
I was able to work around the problem. Below is the mapping that worked using Eclipselink v2.5.0. I used a listener to set the identifier.
@Entity
class Bar {
@Id
@Column(name="id")
private String id;
@ManyToOne
@PrimaryKeyJoinColumn
private Foo foo;
@PrePersist
public void prePersist() {
this.id = foo.id;
}
}
Unfortunately Foo must be persisted before Bar for this to work
Mark Paluch commented
I took a look at the issue and ran into following results by using the code from the original issue description:
- Hibernate: Requires the using class to implement
Persistable<EntityA>, runs fine afterwards.Persistableis not implemented thensaveof the entity runs into aNullPointerExceptionat
java.lang.NullPointerException
at org.springframework.data.repository.core.support.AbstractEntityInformation.isNew(AbstractEntityInformation.java:54)
at org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformation.isNew(JpaMetamodelEntityInformation.java:223)
at org.springframework.data.jpa.repository.support.SimpleJpaRepository.save(SimpleJpaRepository.java:505)
- EclipseLink: same issue with
Persistablethen the exception from above. - OpenJPA: Without
Persistable
<openjpa-2.4.1-r422266:1730418 nonfatal user error> org.apache.openjpa.persistence.ArgumentException: Encountered new object in persistent field "org.springframework.data.jpa.repository. EntityB.entityA" during attach. However, this field does not allow cascade attach. Set the cascade attribute for this field to CascadeType.MERGE or CascadeType.ALL (JPA annotations) or "merge" or "all" (JPA orm.xml). You cannot attach a reference to a new object without cascading.
FailedObject: org.springframework.data.jpa.repository.EntityA@1de9d54
with Persistable it runs fine
Mark Paluch commented
Using a shared entity as @Id isn't a good idea, the better approach is sticking to the way the JPA spec describes.
@Entity
public class EntityB {
@Id
private Long id;
@MapsId
@OneToOne
private EntityA entityA;
}
This approach works with Hibernate and OpenJPA but fails with EclipseLink. The repository must declare … extends JpaRepository<EntityB, Long>. I filed a bug at the EclipseLink Bugzilla and created a test-case.
Bug report: https://bugs.eclipse.org/bugs/show_bug.cgi?id=497143 Test case: https://gist.github.com/mp911de/ba6b5150a486da42a65f8efecf86f70c
Mauro Molinari commented
I think I never received notifications for this bug, this is why I reply only now.
Why using a shared primary key should not be a good idea? It's listed in Pro JPA 2 as a legitimate use case. Indeed, the use of @MapsId is a "plus" when you also want to reference the Long primary key. Plain EclipseLink works perfectly fine in this scenario and I'm using it with now issue at all (apart from this problem with Spring Data).
It's not clear to me why should implementing Persistable necessary? Is this a sort of proposed workaround?
jonlondonwork commented
Did anyone find a workaround for this problem?
I have encountered the same issue with eclipselink 2.6.1 and spring-data-jpa 2.1.4.RELEASE
The issue still lingers on EclipseLink's bug tracker.