如何使用共享主键保存子实体

时间:2019-04-26 20:38:36

标签: hibernate spring-data-jpa one-to-one

问题

我有两个实体,ParentChild,我希望它们共享一个主键,但是我希望这种关系是单向的。只有孩子应该了解父母。 spring-data-jpa存储库有可能吗?

我尝试过的

  • 我已经尝试过直接使用EntityManager,它可以正常工作。我能够先保留Parent,然后再保留Child实体。但是,当我使用Spring的JpaRepository尝试相同操作时,出现以下错误:org.springframework.orm.jpa.JpaSystemException: attempted to assign id from null one-to-one property

  • 我尝试用自己的SERIAL ID和对父代的外键引用对Child实体进行建模。可以,但是我更喜欢使用共享主键。

父实体:

@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());

  }
}

1 个答案:

答案 0 :(得分:1)

以下代码可以正常工作:

public class Child {

    @Id
    @Column(name = "code", insertable = false, updatable = false)
    String code;

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

并测试

@Test
@Transactional
public void testSpringDataJpaRepository() {
    Parent pA = parentRepo.save(Parent.builder().code("A").build());
    Child child = Child.builder().parent(pA).build();
    childRepo.save(child);
}

说明: 查看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);
    }
}

比选中AbstractEntityInformation.isNew。 得出的结论是,仅当实体为null(对于数字类型为0)时,它才是新实体。 因此,您的childRepo.save(child);等效于entityManager.merge(child); 检查是否在第二个测试中调用合并,则收到的错误是否相同。

要解决此问题:

  • 不要在孩子@Id上设置值(可能您也想强迫lombok也不要为其生成设置器)。这将导致persist被代替,而不是merge
  • 请注意,您有2个字段codeparent映射到同一数据库列。为了使映射正确,我在insertable = false, updatable = false列上使用了强制@Id(更改parent字段将导致生成正确的sql)