Kawu
Kawu

Reputation: 14003

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

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).

EDIT: Authenticator entity

@Entity
@Table(name="Authenticators")
@IdClass(AuthenticatorPk.class)
public class Authenticator implements Serializable
{
    private static final long serialVersionUID = 1L;

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

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

    @Basic(optional=false)
    @Column(name="PROCEDURE")
    private String procedure;

    @Temporal(TemporalType.DATE)
    @Basic
    @Column(name="VALID_FROM")
    private Date validFrom;

    @Temporal(TemporalType.DATE)
    @Basic
    @Column(name="VALID_TO")
    private Date validTo;

    public Authenticator()
    {
    }

    ...
}

PK class:

public class AuthenticatorPk implements Serializable
{
    private static final long serialVersionUID = 1L;

    private Integer id;

    private String authenticator = "";

    public AuthenticatorPk()
    {
    }

    ...
}

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.

Other chances are that this is a Hibernate bug...?

Note, that I am more interested in knowing whether this should work in Hibernate or not. I am not really interested in any non-JPA alternative mappings. I can get by this by putting the read-only stuff onto the relationship, then this works, but not the described mappings above, which should... you get it :-)

Upvotes: 4

Views: 682

Answers (1)

Nathan
Nathan

Reputation: 1659

I can almost get what you are looking for with this mapping here:

    @Entity
@Table(name = "mobile_tan")
public class MobileTan  {
    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(fetch = FetchType.LAZY)
    @MapsId
    @JoinColumn(name="AUTH_NAME", referencedColumnName="AUTHENTICATOR")
    private Authenticator authenticator;
}

However, this creates a table with the following definition (HSQL)

    create table mobile_tan (
    AUTH_NAME integer not null,
    ACTIVATION_CODE varchar(255),
    ACTIVATION_ERROR boolean,
    MOBILE_PHONE_NBR varchar(255) not null,
    STATUS integer,
    primary key (AUTH_NAME)
)

When I run the following code:

    @Test
public void createMobileTan() {
    MobileTan tan = createMobileTan();
    session.flush();
    System.err.println("Tan:  " + tan.getId());
    System.err.println("Authenticator:  " + tan.getAuthenticator().getId());
    System.err.println("AUTH COLUMN:  " + session.createSQLQuery("select AUTH_NAME from mobile_tan").addScalar("AUTH_NAME", IntegerType.INSTANCE).setMaxResults(1).uniqueResult());
    System.err.println("AUTH COLUMN:  " + session.createSQLQuery("select ID from mobile_tan").addScalar("ID", IntegerType.INSTANCE).setMaxResults(1).uniqueResult());
}

public MobileTan createMobileTan() {
    Authenticator auth = createAuthenticator();

    MobileTan tan = new MobileTan();

    tan.setAuthenticator(auth);
    tan.setMobilePhoneNbr("0123456");

    session.persist(tan);

    return tan;
}

private Authenticator createAuthenticator() {
    Authenticator auth = new Authenticator();

    session.persist(auth);

    return auth;
}

I get the following output:

Tan:  1
Authenticator:  1
AUTH COLUMN:  1


java.sql.SQLSyntaxErrorException: user lacks privilege or object not found: ID in statement [select ID from mobile_tan limit ?]
    at org.hsqldb.jdbc.JDBCUtil.sqlException(Unknown Source)
    at org.hsqldb.jdbc.JDBCUtil.sqlException(Unknown Source)
    at org.hsqldb.jdbc.JDBCPreparedStatement.<init>(Unknown Source)
    at org.hsqldb.jdbc.JDBCConnection.prepareStatement(Unknown Source)
    at org.hibernate.engine.jdbc.internal.StatementPreparerImpl$5.doPrepare(StatementPreparerImpl.java:146)
    at org.hibernate.engine.jdbc.internal.StatementPreparerImpl$StatementPreparationTemplate.prepareStatement(StatementPreparerImpl.java:172)

The table is created using a primary key that is also a foreign key. As you can see, it sets the object relationships correctly and I can access this using the getId() method in the MobileTan class. But, as you can also see in the table definition DDL there is only one column that is physically created in the database. Do you really need 2 physical columns? Then you can decide to use only one column and also use this single column as the foreign key. Then drop the extra column.

I created my example using the information included in this link here: set-primary-key-and-foreign-key-on-a-single-column

Upvotes: 3

Related Questions