将JPA双向@OneToOne映射到非拥有端

时间:2017-05-29 06:11:36

标签: java hibernate jpa

  

*发布后编辑现已附加到帖子末尾。

     

在撰写此问题时,我遇到了一些文档,这些文档在应用时会产生不同的错误。我仍然觉得以下信息仍然与旁观者有关。有关修改,请参阅下面的预发布编辑

我想在 JPA 2.1

中实现以下双向关系

mapping diagram

我目前的尝试是这样的:

public class A implements Serializable {

    private long aId;
    private AMore aMore;
    private String foo;

    @Id
    @Column(name = "a_id", nullable = false)
    @GeneratedValue(/*...*/)
    public long getAId() { return aId; }

    @OneToOne(cascade = CascadeType.ALL) // this is owning side
    public AMore getAMore() { return aMore; }

    public void setAMore(AMore aMore) {
        this.aMore = aMore;
        this.aMore.setA(this);
    }

    // set aId
    // set/get foo;
}


public class AMore implements Serializable {

    private A a;
    private long version = 1L;
    private String bar;

    @Id
    @OneToOne(optional = false, mappedBy = "aMore") // this is non-owning-side
    @JoinColumn(name="a_id", referencedColumnName = "a_id")
    public A getA() { return a; }

    @Version
    @Column(name = "A_MORE_VER")
    public long getVersion() {
        return version; // not sure if relevant but thought i'd add this incase.
    }

    // set A, version
    // set/get bar

}

尝试创建EntityManagerFactory失败,出现此异常:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.class]: Invocation of init method failed; nested exception is java.lang.NullPointerException
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1628)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:555)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:483)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
    at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1081)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:856)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:542)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:737)
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:370)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:314)
    at org.springframework.boot.builder.SpringApplicationBuilder.run(SpringApplicationBuilder.java:134)
    at com.example.Application.main(Application.java:21)
Caused by: java.lang.NullPointerException: null
    at org.hibernate.cfg.annotations.TableBinder.bindFk(TableBinder.java:599)
    at org.hibernate.cfg.ToOneFkSecondPass.doSecondPass(ToOneFkSecondPass.java:101)
    at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.processEndOfQueue(InFlightMetadataCollectorImpl.java:1786)
    at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.processFkSecondPassesInOrder(InFlightMetadataCollectorImpl.java:1730)
    at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.processSecondPasses(InFlightMetadataCollectorImpl.java:1617)
    at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:278)
    at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.metadata(EntityManagerFactoryBuilderImpl.java:847)
    at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:874)
    at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:60)
    at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:353)
    at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:370)
    at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:359)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1687)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1624)
    ... 14 common frames omitted

深入研究代码,我可以看到对bindFK的内部调用要求referencedIdentityAMore)包含一个标识符(通过referencedIdentity.getIdentifier()获得,返回null ,导致错误:

// TableBinder.java ...
else {
    idColumns = referencedEntity.getIdentifier().getColumnIterator();
}
// ...

预后修改

所以我在阅读后编辑了代码并产生了一个不同的错误,所以我想:上述信息不会创建一个新问题,而是希望仍能与求职者相关。

在仔细阅读documentation on one to one with primary and foreign key relationship后,我将aMore类更改为:

public class AMore implements Serializable {

    private long aId;
    private A a;
    private long version = 1L;
    private String bar;

    @Id
    @Column(name = "a_id")
    private long getAId() { return aId; }

    // removed Id from here and placed on above long property.
    @OneToOne(optional = false, mappedBy = "aMore") // this is non-owning-side
    @PrimaryKeyJoinColumn(name="a_id", referencedColumnName = "a_id") //CHANGED FROM JoinColumn -> PrimaryKeyJoinColumn
    public A getA() { return a; }

    @Version
    @Column(name = "A_MORE_VER")
    public long getVersion() {
        return version; // not sure if relevant but thought i'd add this incase.
    }

    // set A
    // set/get bar, version

}

现在我的hibernate查询通过其foo字段获取A在查询中包含不正确的列名:

select           a0_.a_id as a_id1_1_
                 a0_.a_more_a_id as a_f2_1_, -- as well as being a non-existing field, this should not even be included in the query to fetch only the A Entity info. (the relationship is a `FetchType.LAZY`)
                 a0_.foo as foo3_1_
from             MYSCHEMA.A as a0_
where            a0_.foo = ? -- bound at runtime, e.g. 'spam'

我感觉这是由于aMore在A侧的映射方式。

任何指针?

后后编辑

同时应用@JBNizet和@ XtremeBaumer的建议,并且还将id从基元更改为包装类型[Long],我遇到了Id生成问题。

如果我创建两个实体的实例以便同时保留,则aId上的AMore未填充我的预期并给出以下错误:

Caused by: org.hibernate.id.IdentifierGenerationException: ids for this class must be manually assigned before calling save(): com.example.AMore

我认为因为AAMore的ID都为空,需要分配给A的生成ID,所以A需要持久化,抓取并将id分配给AMore

我的新映射如下所示:

public class A implements Serializable {

    private Long aId;
    private AMore aMore;
    private String foo;

    @Id
    @Column(name = "a_id", nullable = false)
    @GeneratedValue(/*...*/)
    public Long getAId() { return aId; }

    @OneToOne(cascade = CascadeType.ALL, mappedBy = "a")
    public CourseOffering getAMore() { return aMore; }

    public void setAMore(AMore aMore) {
        this.aMore = aMore;
        this.aMore.setA(this);
    }

    // set aId
    // set/get foo;
}


    public class AMore implements Serializable {

        private Long aId;
        private A a;
        private long version = 1L;
        private String bar;

        @Id
        @Column(name = "a_id")
        private Long getAId() { return aId; }

        @OneToOne(optional = false, fetch = FetchType.LAZY)
        @MapsId
        public CourseOffering getA() { return a; }

        // set A, aId
        // set/get bar

    }

2 个答案:

答案 0 :(得分:0)

我发现了这个link并尝试在你的课程中使用它:

public class AMore implements Serializable {
    @Id
    private long aId;
    @OneToOne(fetch = FetchType.LAZY)
    @MapsId
    private A a;
    private long version = 1L;
    private String bar;

    public A getA() {
        return a;
    }
}


public class A implements Serializable {
    @Id
    @GeneratedValue
    private long aId;

    @OneToOne(mappedBy = "a", cascade = CascafeType.ALL, optional = false)
    private AMore aMore;

    private String foo;

    public long getAId() {
        return aId;
    }

    public AMore getAMore() {
        return aMore;
    }

    public void setAMore(AMore aMore) {
        this.aMore = aMore;
        this.aMore.setA(this);
    }

}

也许这对你有用

答案 1 :(得分:0)

感谢@JBNizet和@XtremeBaumer的贡献。最终解决方案中需要注意的一些事项:

  1. app.UseJwtBearerAuthentication(new JwtBearerOptions() { AutomaticAuthenticate = true, AutomaticChallenge = true, TokenValidationParameters = new TokenValidationParameters() { ValidIssuer = Configuration["Tokens:Issuer"], ValidAudience = Configuration["Tokens:Audience"], ValidateIssuerSigningKey = true, IssuerSigningKey = new Certificate(certPath: Configuration["Tokens:Certificate"], isValid: false).SecurityKey, ValidateLifetime = true, ValidateIssuer = true, ValidateAudience = true, ClockSkew = TimeSpan.Zero }, }); 属性应该是两个实体的包装类(aId)而不是原始类(Long),以便JPA提供程序识别需要使用long

    生成ID
      

    @GenerationValue的{​​{1}}默认值是有效ID,因此当在一个会话中保留多个新实体时,它会查找具有0 Id和错误的实体。

  2. long注释放在0关系上方,以便通知JPA提供商@MapsId字段映射到关系实体的@OneToOne字段。

      

    请注意,这会自动应用于我们的案例,因为我们只有一个ID字段,而不是使用@Id@Id的复合ID。有关如何使用父实体的主键映射组合键的信息,请参阅@MapsId上的文档。

  3. @IdClass注释上的mappedBy参数应该存在于父级的引用端,因为子实体“拥有”对其父级的id引用。

  4. 确保toString和equals / hashcode不包含反向关系深度复制值,以避免无限循环场景。

  5. 最终解决方案是编辑@ XtremeBaumer的解决方案:

    @EmbeddedId