jack
jack

Reputation: 184

How to save child entity with shared primary key

The Problem

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?

What I've tried

Parent entity:

@Entity
public class Parent {

  @Id
  @Column(name = "code")
  String code;

}

Child entity:

@Entity
public class Child {

  @Id
  @Column(name = "code")
  String code;

  @MapsId
  @JoinColumn(name = "code")
  @OneToOne
  Parent parent;
}

Test:

@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

Answers (1)

Lesiak
Lesiak

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:

  • don't set value on your child @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
  • notice that you have 2 fields 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

Related Questions