Reputation: 184
I have two entities, Parent
& Child
, I want them to share a primary key but I want the relationship to be uni-directional. Only the child should know about the parent. Is this possible with spring-data-jpa repositories?
I've tried using the EntityManager directly and it works. I am able to first persist the Parent
and then persist the Child
entity. However, when I attempt the same using Spring's JpaRepository
I get the following error: org.springframework.orm.jpa.JpaSystemException: attempted to assign id from null one-to-one property
.
I've tried modeling the Child
entity differently with its own SERIAL id and a foreign key reference to the parent. This works but I'd prefer to use a shared primary key.
@Entity
public class Parent {
@Id
@Column(name = "code")
String code;
}
@Entity
public class Child {
@Id
@Column(name = "code")
String code;
@MapsId
@JoinColumn(name = "code")
@OneToOne
Parent parent;
}
@RunWith(SpringRunner.class)
@DataJpaTest
@Slf4j
public class MapsByIdTest {
@Autowired
ChildRepository childRepo;
@Autowired
ParentRepo parentRepo;
@Autowired
EntityManager entityManager;
@Test // FAILS with org.springframework.orm.jpa.JpaSystemException
public void testSpringDataJpaRepository() {
Parent pA = parentRepo.save(Parent.builder().code("A").build());
Child child = Child.builder().parent(pA).code("A").build();
childRepo.save(child);
}
@Test // WORKS!
public void testEntityManager() {
Parent p = Parent.builder().code("A").build();
entityManager.persist(p);
Child child = Child.builder().code("A").parent(p).build();
entityManager.persist(child);
log.info(entityManager.find(Parent.class, "A").toString());
log.info(entityManager.find(Child.class, "A").toString());
}
}
Upvotes: 3
Views: 2090
Reputation: 25936
The following code will work fine:
public class Child {
@Id
@Column(name = "code", insertable = false, updatable = false)
String code;
@MapsId
@JoinColumn(name = "code")
@OneToOne
Parent parent;
}
and test
@Test
@Transactional
public void testSpringDataJpaRepository() {
Parent pA = parentRepo.save(Parent.builder().code("A").build());
Child child = Child.builder().parent(pA).build();
childRepo.save(child);
}
To explain:
Look at the implementation of SimpleJpaRepository.save
@Transactional
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
Than check AbstractEntityInformation.isNew
.
It concludes that the entity is new only if its it is null (or 0 for numerical types).
Therefore, your childRepo.save(child);
is equivalent to entityManager.merge(child);
Check that if you call merge in your second test,the error you receive is identical.
To solve the issue:
@Id
(probably you want to force lombok not to generate setter for it as well). This will cause persist
to be alled instead of merge
code
and parent
mapped to the same db column. To make the mapping correct, I used forced insertable = false, updatable = false
on the @Id
column (the change of parent
field will cause the correct sql to be generated)Upvotes: 3