spring-boothibernatespring-data-jpahibernate-envers

Hibernate - Unable to perform beforeTransactionCompletion callback: Cannot read the array length because "array" is null


I got the error below after upgrading Spring Boot from version 2.7.14 to 3.1.2.

Caused by: org.hibernate.HibernateException: Unable to perform beforeTransactionCompletion callback: Cannot read the array length because "array" is null
at org.hibernate.engine.spi.ActionQueue$BeforeTransactionCompletionProcessQueue.beforeTransactionCompletion(ActionQueue.java:1016)
at org.hibernate.engine.spi.ActionQueue.beforeTransactionCompletion(ActionQueue.java:548)
at org.hibernate.internal.SessionImpl.beforeTransactionCompletion(SessionImpl.java:1967)
at org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl.beforeTransactionCompletion(JdbcCoordinatorImpl.java:439)
at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.beforeCompletionCallback(JdbcResourceLocalTransactionCoordinatorImpl.java:169)
at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.commit(JdbcResourceLocalTransactionCoordinatorImpl.java:267)
at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:101)
at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:561)
... 143 common frames omitted
Caused by: java.lang.NullPointerException: Cannot read the array length because "array" is null
at java.base/java.util.Arrays.stream(Arrays.java:5428)
at io.hypersistence.utils.hibernate.type.util.ParameterTypeUtils.getAnnotations(ParameterTypeUtils.java:76)
at io.hypersistence.utils.hibernate.type.util.ParameterTypeUtils.getAnnotationOrNull(ParameterTypeUtils.java:53)
at io.hypersistence.utils.hibernate.type.util.ParameterTypeUtils.getColumnType(ParameterTypeUtils.java:90)
at io.hypersistence.utils.hibernate.type.json.internal.JsonJdbcTypeDescriptor.resolveJdbcTypeDescriptor(JsonJdbcTypeDescriptor.java:90)
at io.hypersistence.utils.hibernate.type.json.internal.JsonJdbcTypeDescriptor.sqlTypeDescriptor(JsonJdbcTypeDescriptor.java:72)
at io.hypersistence.utils.hibernate.type.json.internal.JsonJdbcTypeDescriptor$1.doBind(JsonJdbcTypeDescriptor.java:40)
at org.hibernate.type.descriptor.jdbc.BasicBinder.bind(BasicBinder.java:61)
        <dependency>
            <groupId>com.oracle.database.jdbc</groupId>
            <artifactId>ojdbc8</artifactId>
            <version>21.1.0.0</version>
            <scope>runtime</scope>
        </dependency>
 <dependency>
     <groupId>org.hibernate</groupId>
     <artifactId>hibernate-envers</artifactId>
     <version>6.2.6.Final</version>
  </dependency>
<dependency>
    <groupId>io.hypersistence</groupId>
    <artifactId>hypersistence-utils-hibernate-62</artifactId>
    <version>3.5.1</version>
</dependency>

After some debugging, I realised that the issue is related to Hibernate Envers and Hypersistence Utils mechanism to process @Type(JsonType.class) within CCS_CUSTOMER_DETAILS_AUDIT_LOG entity generated by Hibernate Envers.

Caused by: java.lang.NullPointerException: Cannot read the array length because "array" is null
at java.base/java.util.Arrays.stream(Arrays.java:5428)
at io.hypersistence.utils.hibernate.type.util.ParameterTypeUtils.getAnnotations(ParameterTypeUtils.java:76)

It works using H2, therefore the issue involves Oracle dialect somehow.

Have you face a similar issue migrating to Spring Boot 3? if so, how have you fixed it?

Database Tables:

CREATE TABLE CCS_CUSTOMER_DETAILS
(
    CUSTOMER_NUMBER                 VARCHAR2(9)  NOT NULL
        CONSTRAINT CCS_CUSTOMER_DETAILS_PK
            PRIMARY KEY,
    CUSTOMER_EGN                    VARCHAR2(10) NOT NULL
        CONSTRAINT CCS_CUSTOMER_DETAILS_EGN_UNIQUE
            UNIQUE,
    CUSTOMER_IS_AGREED_TO_USE_OF_DATA NUMBER(1, 0) NOT NULL,
    CUSTOMER_LOCALIZED_FULLNAME              VARCHAR2(4000) NOT NULL
        CONSTRAINT LOCALIZED_FULLNAME_IS_JSON
            CHECK (CUSTOMER_LOCALIZED_FULLNAME IS JSON),
    DRIVING_LICENSE_NUMBER          VARCHAR2(9),
    DRIVING_LICENSE_EXPIRATION_DATE DATE,
    CUSTOMER_STATUS              SMALLINT  NOT NULL,
    CREATED_ON                   TIMESTAMP NOT NULL,
    LAST_UPDATED_ON              TIMESTAMP NOT NULL
);

CREATE TABLE CCS_AUDIT_REVISIONS_INFO
(
    REVISION_NUMBER      NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY
        CONSTRAINT CCS_SUBSCRIPTION_SERVICES_PK
            PRIMARY KEY,
    REVISION_TIMESTAMP  NUMBER
);

CREATE TABLE CCS_CUSTOMER_DETAILS_AUDIT_LOG
(
    REVISION_NUMBER_START           NUMBER       NOT NULL,
    REVISION_NUMBER_END             NUMBER,
    REVISION_TYPE                   SMALLINT,
    CUSTOMER_NUMBER                 VARCHAR2(9),
    CUSTOMER_IS_AGREED_TO_USE_OF_DATA NUMBER(1, 0),
    CUSTOMER_LOCALIZED_FULLNAME     VARCHAR2(4000),
    CUSTOMER_EGN                    VARCHAR2(10),
    DRIVING_LICENSE_NUMBER          VARCHAR2(9),
    DRIVING_LICENSE_EXPIRATION_DATE DATE,
    CUSTOMER_STATUS              SMALLINT,
    CREATED_ON                   TIMESTAMP,
    LAST_UPDATED_ON              TIMESTAMP,
    CONSTRAINT CCS_AUDIT_REVISIONS_INFO_START_FK
        FOREIGN KEY (REVISION_NUMBER_START) REFERENCES CCS_AUDIT_REVISIONS_INFO,
    CONSTRAINT CCS_AUDIT_REVISIONS_INFO_END_FK
        FOREIGN KEY (REVISION_NUMBER_END) REFERENCES CCS_AUDIT_REVISIONS_INFO,
    CONSTRAINT CCS_CUSTOMER_DETAILS_AUDIT_LOG_PK
        PRIMARY KEY (CUSTOMER_NUMBER, REVISION_NUMBER_START)
);

JPA Entities:

@Getter
@Setter
@Entity
@Audited
@Table(name = "CCS_CUSTOMER_DETAILS")
public class CustomerDetailsEntity {

    @Id
    @Column(name = "CUSTOMER_NUMBER", nullable = false)
    private String customerNumber;

    @Column(name = "CUSTOMER_EGN", unique = true, nullable = false)
    private String egn;

    @Column(name = "DRIVING_LICENSE_NUMBER", unique = true, nullable = false)
    private String drivingLicenseNumber;

    @Column(name = "DRIVING_LICENSE_EXPIRATION_DATE", nullable = false)
    private LocalDate drivingLicenseExpirationDate;

    @Column(name = "CUSTOMER_IS_AGREED_TO_USE_OF_DATA", nullable = false)
    private boolean agreedToUseOfData;

    @Type(JsonType.class)
    @Column(name = "CUSTOMER_LOCALIZED_FULLNAME", nullable = false)
    private Map<Alphabet, FullName> localizedFullName;

    @Enumerated
    @Column(name = "CUSTOMER_STATUS", nullable = false)
    private Status status;

    @CreationTimestamp
    @Column(name = "CREATED_ON", updatable = false)
    private LocalDateTime createdOn;

    @UpdateTimestamp
    @Column(name = "LAST_UPDATED_ON")
    private LocalDateTime lastUpdatedOn;
}

@Getter
@Setter
@Entity
@RevisionEntity
@Table(name = "CCS_AUDIT_REVISIONS_INFO")
public class RevisionInfoEntity {

    @Id
    @GeneratedValue(strategy = IDENTITY)
    @RevisionNumber
    @Column(name = "REVISION_NUMBER")
    private long id;

    @RevisionTimestamp
    @Column(name = "REVISION_TIMESTAMP")
    private long timestamp;

Solution

  • I believe, the issue is related to incompatibility between hipersistence-utils-hibernate-62 version 3.7.1 and hibernate-envers version 6.2.1.Final.

    I've fixed the issue, implementing a custom converter for private Map<Alphabet, FullName> localizedFullName; field:

    
    @Getter
    @Setter
    @Entity
    @Audited
    @Table(name = "CCS_CUSTOMER_DETAILS")
    public class CustomerDetailsEntity {
    
       // code omitted
    
       @Convert(converter = FullNameMapToJsonConverter.class)  
       @Column(name = "CUSTOMER_LOCALIZED_FULLNAME", nullable = false)  
       private Map<Alphabet, FullName> localizedFullName;
       
    }
    
    @Converter  
    class FullNameMapToJsonConverter implements AttributeConverter<Map<Alphabet, FullName>, String> {  
    
        private final ObjectMapper objectMapper = new ObjectMapper();
          
        @Override  
        public String convertToDatabaseColumn(Map<Alphabet, FullName> customerInfo) {
            if (customerInfo == null) return "{}";  
          
            try {  
                return objectMapper.writeValueAsString(customerInfo);  
            } catch (Exception ex) {
                new IllegalArgumentException("JSON writing error", ex); 
            }  
        }
          
        @Override  
        public Map<Alphabet, FullName> convertToEntityAttribute(String customerInfoJSON) {
            if (customerInfoJSON == null || customerInfoJSON.isBLank()) return Map.of();
              
            try {  
                return bjectMapper.readValue(customerInfoJSON, new TypeReference<Map<Alphabet, FullName>>() {  
                });  
            } catch (Exception ex) {  
                new IllegalArgumentException("JSON reading error", ex); 
            }  
        }  
    }
    

    NOTE I can use @Type(JsonType.class) after upgrading to Spring Boot 3.2.2 and replacing hypersistence-utils-hibernate-62 with hypersistence-utils-hibernate-63 version 3.7.1