AntoineB
AntoineB

Reputation: 4694

Hibernate - Persist @OneToOne with a composite key

I have the following "audit" table that can host audit information about any other class of my schema:

CREATE TABLE `audit` (
  `table_name` varchar(45) NOT NULL,
  `item_id` int(11) NOT NULL,
  `version` int(11) NOT NULL,
  `updated_at` datetime NOT NULL,
  `updated_by` varchar(25) NOT NULL,
  `comment` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`table_name`,`item_id`,`version`)
)

Then, I have different JPA entities in my schema like so:

@Entity(name = "EntityA")
@Table(name = "entity_a")
public class EntityA {
    @Id
    @GeneratedValue
    private Long id;

    private Long version;

    // Other fields

    @OneToOne(mappedBy = "id.item", targetEntity = EntityAAudit.class, fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    private EntityAAudit audit;
}

At the same time, I have an asbtract class Audit that is a super-class for multiple entity-specific Audit classes:

@MappedSuperclass
@Table(name = "audit")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "table_name", discriminatorType = DiscriminatorType.STRING)
@DiscriminatorOptions(insert = true, force = true)
public abstract class AuditHistory {

    // Some audit fields like the date and the author of the modification

}

@Entity(name = "EntityAAudit")
@Table(name = "audit")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorValue("entity_a")
@DiscriminatorOptions(insert = true, force = true)
public class EntityAAudit extends Audit {

    @EmbeddedId
    @JsonUnwrapped
    private AuditedId id;

    @Embeddable
    public static class AuditedId implements Serializable {
        @OneToOne
        @JoinColumn(name = "item_id", nullable = false)
        private EntityA item;

        @Column(name = "version", nullable = false)
        private Long version;
    }

}

This mapping works when retrieving the entities and their audit information from the database, but not when inserting a new entity with the corresponding audit information:

EntityA entity = new EntityA();
entity.setVersion(/* ... */);
// Setting the other basic fields of the entity

EntityAAudit audit = new EntityAAudit();
// Setting the basic fields of the audit

entity.setAudit(audit);

entity = em.merge(entity);

I end up with the following exception:

org.hibernate.id.IdentifierGenerationException: null id generated for:class EntityAAudit

I have literally tried everything I could think of or find online, and in the end it always comes down to the same issue: Hibernate tries to insert my Audit object with empty values for the item_id and the version.

If I manually set my entity instance and the version as the id of the audit object like so:

EntityA entity = new EntityA();
entity.setVersion(/* ... */);
// Setting the other basic fields of the entity

EntityAAudit audit = new EntityAAudit();
// Setting the basic fields of the audit
audit.setId(new EntityAAudit.AuditedId());
audit.getId().setItem(entity);
audit.getId().setVersion(entity.getVersion());

entity.setAudit(audit);

entity = em.merge(entity);

Then I end up with the even more obscure error here:

Caused by: java.lang.NullPointerException
    at org.hibernate.type.descriptor.java.AbstractTypeDescriptor.extractHashCode(AbstractTypeDescriptor.java:65)
    at org.hibernate.type.AbstractStandardBasicType.getHashCode(AbstractStandardBasicType.java:185)
    at org.hibernate.type.AbstractStandardBasicType.getHashCode(AbstractStandardBasicType.java:189)
    at org.hibernate.type.EntityType.getHashCode(EntityType.java:348)

Note that I cannot change the structure of my database nor the version of Hibernate (5.1.0, I know some bugs are fixed in later versions that could solve my issue...).

Thanks a lot :)

Upvotes: 0

Views: 516

Answers (1)

Brian Vosburgh
Brian Vosburgh

Reputation: 3276

You might try a "derived identity" mapping:

@Entity(name = "EntityAAudit")
@Table(name = "audit")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorValue("entity_a")
@DiscriminatorOptions(insert = true, force = true)
public class EntityAAudit extends Audit {

    @EmbeddedId
    @JsonUnwrapped
    private AuditedId id;

    @OneToOne
    @JoinColumn(name = "item_id", nullable = false)
    @MapsId("entityAId") // maps entityAId attribute of embedded id
    private EntityA item;

    @Embeddable
    public static class AuditedId implements Serializable {
        private Long entityAId; // corresponds to PK type of EntityA

        @Column(name = "version", nullable = false)
        private Long version;
    }

}

Note the @MapsId annotation on EntityAAudit.item.

Also, you will need to explicitly set EntityAAudit.item and AuditedId.version. JPA does not magically determine and set any circular references for you.

Derived identities are discussed (with examples) in the JPA 2.2 spec in section 2.4.1.

Upvotes: 1

Related Questions