Hibernate: MappingException on @OneToOne relationship with two @JoinColumn's with additional PK read-only mapping for one of its columns


#1

Beware of the following DB design:

DB design

NOTE: Never mind what the creator of this “design” had in mind, but I cannot change it.

It’s about the relationship between MobileTans and Authenticators

The respective mappings of the MobileTans table:

@Entity
@Table(name="MobileTans")
public class MobileTan implements Serializable
{
    private static final long serialVersionUID = 1L;

    @Id
    @Column(name="ID", insertable=false, updatable=false)
    private Integer id;    // dupe read-only field, ID already in relationship below (writable)

    @Basic(optional=false)
    @Column(name="MOBILE_PHONE_NBR")
    private String mobilePhoneNbr;

    @Basic
    @Column(name="ACTIVATION_CODE")
    private String activationCode;

    @Basic
    @Column(name="ACTIVATION_ERROR")
    private Boolean activationError = Boolean.FALSE;

    @Basic
    @Column(name="STATUS")
    private Integer status;

    @OneToOne(optional=false, fetch=FetchType.EAGER)
    @JoinColumn(name="ID", referencedColumnName="ID")
    @JoinColumn(name="AUTH_NAME", referencedColumnName="AUTHENTICATOR")
    private Authenticator authenticator;

    ...
}

The clue here is the duplicate read-only mapping (insertable=false, updatable=false) for the @Id column.

The ID column is defined to be the PK, so there’s no use in setting the @Id onto the relationship. I must create an extra column mapping. I put the read-only on the extra column, because Hibernate (or JPA) won’t let me specify read-only only on parts of a relationship (I think this is forbidden by the JPA spec).

When launching the server, I get a mapping exception from Hibernate:

00:37:53,979 INFO  [org.hibernate.Version] (ServerService Thread Pool -- 72) HHH000412: Hibernate Core {5.3.6.Final}
00:37:53,980 INFO  [org.hibernate.cfg.Environment] (ServerService Thread Pool -- 72) HHH000206: hibernate.properties not found
00:37:54,088 INFO  [org.hibernate.annotations.common.Version] (ServerService Thread Pool -- 72) HCANN000001: Hibernate Commons Annotations {5.0.4.Final}
00:37:54,382 INFO  [org.hibernate.validator.internal.util.Version] (MSC service thread 1-2) HV000001: Hibernate Validator 6.0.13.Final
00:37:54,621 INFO  [org.jboss.weld.Version] (MSC service thread 1-3) WELD-000900: 3.0.5 (Final)
00:37:54,674 INFO  [org.jboss.as.clustering.infinispan] (ServerService Thread Pool -- 72) WFLYCLINF0002: Started client-mappings cache from ejb container
00:37:54,777 INFO  [org.jboss.as.jpa] (ServerService Thread Pool -- 72) WFLYJPA0010: Starting Persistence Unit (phase 2 of 2) Service 'mappingbug.war#BBStatsPU'
00:37:54,806 INFO  [org.hibernate.dialect.Dialect] (ServerService Thread Pool -- 72) HHH000400: Using dialect: org.hibernate.dialect.MySQL57Dialect
00:37:54,859 INFO  [org.hibernate.envers.boot.internal.EnversServiceImpl] (ServerService Thread Pool -- 72) Envers integration enabled? : true
00:37:55,007 ERROR [org.jboss.msc.service.fail] (ServerService Thread Pool -- 72) MSC000001: Failed to start service jboss.persistenceunit."mappingbug.war#BBStatsPU": org.jboss.msc.service.StartException in service jboss.persistenceunit."mappingbug.war#BBStatsPU": javax.persistence.PersistenceException: [PersistenceUnit: BBStatsPU] Unable to build Hibernate SessionFactory
    at org.jboss.as.jpa.service.PersistenceUnitServiceImpl$1$1.run(PersistenceUnitServiceImpl.java:195)
    at org.jboss.as.jpa.service.PersistenceUnitServiceImpl$1$1.run(PersistenceUnitServiceImpl.java:125)
    at org.wildfly.security.manager.WildFlySecurityManager.doChecked(WildFlySecurityManager.java:650)
    at org.jboss.as.jpa.service.PersistenceUnitServiceImpl$1.run(PersistenceUnitServiceImpl.java:209)
    at org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
    at org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1985)
    at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1487)
    at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1378)
    at java.lang.Thread.run(Thread.java:748)
    at org.jboss.threads.JBossThread.run(JBossThread.java:485)
Caused by: javax.persistence.PersistenceException: [PersistenceUnit: BBStatsPU] Unable to build Hibernate SessionFactory
    at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.persistenceException(EntityManagerFactoryBuilderImpl.java:1016)
    at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:942)
    at org.jboss.as.jpa.hibernate5.TwoPhaseBootstrapImpl.build(TwoPhaseBootstrapImpl.java:44)
    at org.jboss.as.jpa.service.PersistenceUnitServiceImpl$1$1.run(PersistenceUnitServiceImpl.java:167)
    ... 9 more
Caused by: org.hibernate.MappingException: Repeated column in mapping for entity: net.bbstats.entity.MobileTan column: ID (should be mapped with insert="false" update="false")
    at org.hibernate.mapping.PersistentClass.checkColumnDuplication(PersistentClass.java:862)
    at org.hibernate.mapping.PersistentClass.checkPropertyColumnDuplication(PersistentClass.java:880)
    at org.hibernate.mapping.PersistentClass.checkColumnDuplication(PersistentClass.java:902)
    at org.hibernate.mapping.PersistentClass.validate(PersistentClass.java:634)
    at org.hibernate.mapping.RootClass.validate(RootClass.java:267)
    at org.hibernate.boot.internal.MetadataImpl.validate(MetadataImpl.java:347)
    at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:466)
    at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:939)
    ... 11 more

00:37:55,011 ERROR [org.jboss.as.controller.management-operation] (Controller Boot Thread) WFLYCTL0013: Operation ("deploy") failed - address: ([("deployment" => "mappingbug.war")]) - failure description: {"WFLYCTL0080: Failed services" => {"jboss.persistenceunit.\"mappingbug.war#BBStatsPU\"" => "javax.persistence.PersistenceException: [PersistenceUnit: BBStatsPU] Unable to build Hibernate SessionFactory
    Caused by: javax.persistence.PersistenceException: [PersistenceUnit: BBStatsPU] Unable to build Hibernate SessionFactory
    Caused by: org.hibernate.MappingException: Repeated column in mapping for entity: net.bbstats.entity.MobileTan column: ID (should be mapped with insert=\"false\" update=\"false\")"}}
00:37:55,018 INFO  [org.jboss.as.server] (ServerService Thread Pool -- 42) WFLYSRV0010: Deployed "mappingbug.war" (runtime-name : "mappingbug.war")
00:37:55,019 INFO  [org.jboss.as.controller] (Controller Boot Thread) WFLYCTL0183: Service status report
WFLYCTL0186:   Services which failed to start:      service jboss.persistenceunit."mappingbug.war#BBStatsPU": javax.persistence.PersistenceException: [PersistenceUnit: BBStatsPU] Unable to build Hibernate SessionFactory
WFLYCTL0448: 19 additional services are down due to their dependencies being missing or failed
00:37:55,079 INFO  [org.jboss.as.server] (Controller Boot Thread) WFLYSRV0212: Resuming server
00:37:55,081 INFO  [org.jboss.as] (Controller Boot Thread) WFLYSRV0060: Http management interface listening on http://127.0.0.1:9990/management
00:37:55,081 INFO  [org.jboss.as] (Controller Boot Thread) WFLYSRV0051: Admin console listening on http://127.0.0.1:9990
00:37:55,082 ERROR [org.jboss.as] (Controller Boot Thread) WFLYSRV0026: WildFly Full 14.0.1.Final (WildFly Core 6.0.2.Final) started (with errors) in 14146ms - Started 449 of 645 services (23 services failed or missing dependencies, 328 services are lazy, passive or on-demand)

BTW, I am using Hibernate Core 5.3.6.Final on Wildfly 14.

QUESTION:

What’s wrong here? Why does Hibernate complain about this? My impression is that this is perfectly legal.

Help greatly appreciated!
Kawu


#2

You need to use @EmbeddedId, not @Id in your case. Check out this article for more details.

For the @OneToOne association, you need to use @MapsId. This article explains you how to do that.


#3

Hello,

thanks for the answer, however this is not the answer I had hoped for…

The mappings as we use them don’t seem to offend anything the JPA spec defines.

My main problem is that we are using a code generator to generate the mappings in the first place, so changing to @EmbeddedId would mean to change to that for ALL entities, which is not what can so easily be done. And I don’t know if they like mixing @EmbeddedId and @IdClass, which kind of compete with each other.

In the end, as it is not clear why these mappings shouldn’t work, isn’t this actually a BUG IN HIBERNATE?

Karsten


#4

The mappings as we use them don’t seem to offend anything the JPA spec defines.

Just because Hibernate supports composite identifers via multiple @Id attributes, it does not mean that’s part of JPA. More, your mapping does not reflect the database schema, hence it’s not correct.

And I don’t know if they like mixing @EmbeddedId and @IdClass , which kind of compete with each other.

Mixing @EmbeddedId and @IdClass would be a mistake, and the JPA specification says that you should use one or the other for composite identifiers.

In the end, as it is not clear why these mappings shouldn’t work, isn’t this actually a BUG IN HIBERNATE?

Nope, there’s no bug. You are not mapping the composite identifier according to the JPA spec.

My main problem is that we are using a code generator to generate the mappings in the first place, so changing to @EmbeddedId would mean to change to that for ALL entities

So, you either fix the generator to render a valid JPA mapping or you fix it manually after the generator renders the broken mappings.


#5

Thanks for the answer.

Hmmm okay, now you say that the mappings for the model at hand are not correct.

What would the correct mappings WITHOUT using @EmbeddedId have to look like then (and we still want the relationship to be writable…)?

I don’t really see it…

Karsten


#6

What would the correct mappings WITHOUT using @EmbeddedId have to look like then (and we still want the relationship to be writable…)?

There’s an example in the Hibernate User Guide that shows a Hibernate-specific use case with multiple @Id declarations/

So, you could try to define the Authenticator like this:

@Entity
@Table(name="Authenticator")
public class Authenticator implements Serializable {

    @Id
    @Column(name="ID")
    private Integer id;
    
    @Id
    @Column(name="AUTHENTICATOR")
    private String authenticator;

    ...
}

And then, the MobileTan would be mapped like this:

@Entity
@Table(name="MobileTans")
public class MobileTan implements Serializable {

    @Id 
    @OneToOne(optional=false)
    @JoinColumn(name="ID", referencedColumnName="ID")
    @JoinColumn(name="AUTH_NAME", referencedColumnName="AUTHENTICATOR")
    private Authenticator authenticator;
    
    @Basic(optional=false)
    @Column(name="MOBILE_PHONE_NBR")
    private String mobilePhoneNbr;

    @Basic
    @Column(name="ACTIVATION_CODE")
    private String activationCode;

    @Basic
    @Column(name="ACTIVATION_ERROR")
    private Boolean activationError = Boolean.FALSE;

    @Basic
    @Column(name="STATUS")
    private Integer status;

    ...
}

There’s no need to map the individual PK/FK columns in MobileTan since the @OneToOne association should take care of that.

However, if you really want to have those mappings, you could add them like this:

@Column(name="ID", insertable=false, updatable=false)
private Integer id;

@Column(name="AUTH_NAME", insertable=false, updatable=false)
private String authenticator;

Notice that, this time, you no longer add the @Id annotation to the individual PK/FK properties since the @OneToOne association already defines the composite identifier.

If that does not work, it means that this mapping can only be supported via @EmbeddedId or @IdClass, but it’s worth to try and see if it works.


#7

Thanks again.

The “great” design of the DB at hand defines ID to be the only PK column in MobileTans, which is also part of an FK. AUTH_NAME is only an FK, but no PK column. Thus, your mappings cannot be implemented with what we have in the DB (and which we cannot change).

This is why I had to introduce another redundant column just to carry the @Id annotation. Basically, what we are doing here are using the JPA 1.0-compatible mappings.

And this is where Hibernate chokes…

@Entity
@Table(name="MobileTans")
public class MobileTan implements Serializable
{
    private static final long serialVersionUID = 1L;

    @Id
    @Column(name="ID", insertable=false, updatable=false)
    private Integer id;    // dupe read-only field, ID already in relationship below (writable)

    ...

    @OneToOne(optional=false, fetch=FetchType.EAGER)
    @JoinColumn(name="ID", referencedColumnName="ID") // PK/FK column
    @JoinColumn(name="AUTH_NAME", referencedColumnName="AUTHENTICATOR") // FK only
    private Authenticator authenticator;

I’d really expect Hibernate to be able to handle this. :roll_eyes:

Karsten


#8

If the ID is the only PK column in MobileTrans and this is a one-to-one association, it means the ID column is unique, so you could use just that as a JPA @Id while mapping the AUTH_NAME as a @Basic property.

This is why I had to introduce another redundant column just to carry the @Id annotation. Basically, what we are doing here are using the JPA 1.0-compatible mappings.

JPA 1.0 is 12 years old. You should definitely give JPA 2.2 a try.

I’d really expect Hibernate to be able to handle this.

Both JPA and Hibernate are designed to support solid database schema mappings. You database schema design is atypical, and that’s the real problem, not Hibernate.

Nevertheless, you might achieve your goal by mapping the ID column as @Id and use @JoinFormula for the association. Check out this article for more details.


#9

We are using JPA 2.2, yes. It’s just that the mappings at hand could have been implemented with JPA 1.0…

So, in the end, you don’t consider the mappings I describe something to become more stable for future Hibernate releases? I have encountered this issue many times in the past. I had hoped that things like this are being fixed/improved over time.

:man_shrugging:

Thanks
Karsten


#10

So, in the end, you don’t consider the mappings I describe something to become more stable for future Hibernate releases?

Hibernate has been stable for the past 17 years, being used by millions of developers every day. As long as the database schema is properly designed, you won’t have any issue.

I have encountered this issue many times in the past. I had hoped that things like this are being fixed/improved over time.

The beauty of open-source software is that you can always step in and provide an improvement. If you encountered this issue so many times in the past, in the true spirit of open-source software development, we are looking forward to getting a Pull Request from you with a fix.