jpaeclipselinkjpqlconverterspayara

EclipseLink JPA converter subclasses doesn't work


I use payara5 (with EclipseLink). It looks like I can't use subclasses with a JPA converter. With wildfly (and Hibernate), it works fine.

The problem comes from this query :

@Override
public List<Employee> findByStatus(Employee.Status status) {
    return em.createNamedQuery("Employee.findByStatus", Employee.class)
        .setParameter("status", status)
        .getResultList();
}

It looks like, if the converter is a subclass, EclipseLink is not able to convert the parameter "status" into a string. Without the subclass, it works just fine. Is it a bug in EclipseLink ?

persistence.xml :

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
    xmlns="http://xmlns.jcp.org/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
        http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd"
>
    <persistence-unit name="primary" transaction-type="JTA">
        <!--jta-data-source>java:/TestDS</jta-data-source-->
        <jta-data-source>jdbc/TestDS</jta-data-source>
        <class>fjp.converter.entity.Employee</class>
        <class>fjp.converter.entity.converter.StatusConverter</class>
        <class>fjp.converter.entity.converter.StatusConverterSubClass</class>
        <exclude-unlisted-classes>true</exclude-unlisted-classes>
        <shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>
        <properties>
            <property name="javax.persistence.schema-generation.database.action" value="drop-and-create" />
            <property name="eclipselink.logging.level.sql" value="FINE"/>
            <property name="eclipselink.logging.parameters" value="true"/>
             <property name="hibernate.show_sql" value="true"/>
        </properties>
    </persistence-unit>
</persistence>

Entity :

package fjp.converter.entity;

import java.io.Serializable;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.persistence.Convert;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.NamedQuery;

@NamedQuery(name="Employee.findByStatus", query="select e from Employee e where e.status=:status")
@Entity
public class Employee implements Serializable{

    private static final long serialVersionUID = 1L;

    public enum Status implements HasCode {
        SENIOR("SENIOR"),
        JUNIOR("JUNIOR");
        private String code;
        private Status(String s) {
            this.code = s;
        }
        @Override
        public String getCode() {
            return this.code;
        }
        private static Map<String, Status> map = Stream.of(values()).collect(Collectors.toMap(Status::getCode, Function.identity()));

        public static Status fromString(String code) {
            return map.get(code);
        }
    }

    @Id
    private long id;
//  @Convert(converter = fjp.converter.entity.converter.StatusConverter.class)
    @Convert(converter = fjp.converter.entity.converter.StatusConverterSubClass.class)
    private Status status;

    public long getId() {
        return id;
    }
    public void setId(long id) {
        this.id = id;
    }
    public Status getStatus() {
        return this.status;
    }
    public void setStatus(Status s) {
        this.status = s;
    }

    @Override
    public String toString() {
        return String.format("id=%d, status=%s", id, status == null ? null : status.getCode());
    }
    @Override
    public boolean equals(Object o) {
        if(o == this) return true;
        if(!(o instanceof Employee)) return false;
        Employee e = (Employee) o;
        return e.getId() == getId();
    }

    @Override
    public int hashCode() {
        return Long.hashCode(getId());
    }
}

Interface HasCode :

package fjp.converter.entity;

public interface HasCode {
    String getCode();
}

StatusConverter :

package fjp.converter.entity.converter;

import javax.persistence.Converter;
import javax.persistence.AttributeConverter;

import fjp.converter.entity.Employee.Status;

@Converter
public class StatusConverter implements AttributeConverter<Status, String> {
    @Override
    public String convertToDatabaseColumn(Status e) {
        return e == null ? null : e.getCode();
    }
    @Override
    public Status convertToEntityAttribute(String s) {
        if(s == null) return null;
        switch(s) {
            case "SENIOR": return Status.SENIOR;
            case "JUNIOR": return Status.JUNIOR;
            default: return null;
        }
    }
}

StatusConverterSubClass :

package fjp.converter.entity.converter;

import javax.persistence.Converter;

import fjp.converter.entity.Employee.Status;

@Converter
public class StatusConverterSubClass extends EnumCodeConverter<Status> {
    public StatusConverterSubClass() {
        super(Status::fromString);
    }
}

Converter base class :

package fjp.converter.entity.converter;

import java.util.function.Function;

import javax.persistence.AttributeConverter;

import fjp.converter.entity.HasCode;

public class EnumCodeConverter<T extends HasCode> implements AttributeConverter<T, String> {
    private final Function<String, ? extends T> fromString;
    
    protected EnumCodeConverter(Function<String, ? extends T> fromString) {
        this.fromString = fromString;
    }

    @Override
    public String convertToDatabaseColumn(T attribute) {
        return attribute == null ? null : attribute.getCode();
    }

    @Override
    public T convertToEntityAttribute(String code) {
        if(code == null) return null;
        T r = this.fromString.apply(code);
        if(r == null) {
            throw new IllegalArgumentException(String.format("unknow code: '%s', '%s'", code, this.getClass()));
        }
        return r;
    }
}

dao :

package fjp.converter.dao;

import java.util.List;

import fjp.converter.entity.Employee;

public interface EmployeeDAO {
    public List<Employee> findByStatus(Employee.Status status);
}

daoimpl :

package fjp.converter.dao;

import java.util.List;

import javax.ejb.Stateless;
import javax.ejb.Local;
import javax.persistence.PersistenceContext;
import javax.persistence.EntityManager;

import fjp.converter.entity.Employee;

@Local(EmployeeDAO.class)
@Stateless
public class EmployeeDAOImpl implements EmployeeDAO {
    @PersistenceContext
    private EntityManager em;
    
    @Override
    public List<Employee> findByStatus(Employee.Status status) {
        return em.createNamedQuery("Employee.findByStatus", Employee.class)
            .setParameter("status", status)
            .getResultList();
    }
}

Test servlet :

package fjp.converter.servlet;

import javax.inject.Inject;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import fjp.converter.dao.EmployeeDAO;
import fjp.converter.entity.Employee.Status;

@WebServlet("/test")
public class Test extends HttpServlet {
    private static final long serialVersionUID = 1L;
    @Inject
    private EmployeeDAO dao;

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) {
        Status status = Status.SENIOR;
        var list = dao.findByStatus(status);
        System.out.println("FJP: " + list.size());
    }
}

And payara logs :

[2021-11-11T11:42:56.565+0100] [Payara 5.2021.3] [CONFIG] [] [org.eclipse.persistence.default] [tid: _ThreadID=185 _ThreadName=admin-thread-pool::admin-listener(11)] [timeMillis: 1636627376565] [levelValue: 700] [[
  The default table generator could not locate or convert a java type (class fjp.converter.entity.Employee$Status) into a database type for database field (EMPLOYEE.STATUS). The generator uses "java.lang.String" as default java type for the field.]]

[2021-11-11T11:43:21.771+0100] [Payara 5.2021.3] [AVERTISSEMENT] [AS-EJB-00056] [javax.enterprise.ejb.container] [tid: _ThreadID=76 _ThreadName=http-thread-pool::http-listener-1(5)] [timeMillis: 1636627401771] [levelValue: 900] [[
  A system exception occurred during an invocation on EJB EmployeeDAOImpl, method: public java.util.List fjp.converter.dao.EmployeeDAOImpl.findByStatus(fjp.converter.entity.Employee$Status)]]

[2021-11-11T11:43:21.772+0100] [Payara 5.2021.3] [AVERTISSEMENT] [] [javax.enterprise.ejb.container] [tid: _ThreadID=76 _ThreadName=http-thread-pool::http-listener-1(5)] [timeMillis: 1636627401772] [levelValue: 900] [[
  
javax.ejb.EJBException: Exception [EclipseLink-3002] (Eclipse Persistence Services - 2.7.7.payara-p3): org.eclipse.persistence.exceptions.ConversionException
Exception Description: The object [SENIOR], of class [class java.lang.String], from mapping [org.eclipse.persistence.mappings.DirectToFieldMapping[status-->EMPLOYEE.STATUS]] with descriptor [RelationalDescriptor(fjp.converter.entity.Employee --> [DatabaseTable(EMPLOYEE)])], could not be converted to [class fjp.converter.entity.Employee$Status].
    at com.sun.ejb.containers.EJBContainerTransactionManager.processSystemException(EJBContainerTransactionManager.java:723)
    at com.sun.ejb.containers.EJBContainerTransactionManager.completeNewTx(EJBContainerTransactionManager.java:652)
    at com.sun.ejb.containers.EJBContainerTransactionManager.postInvokeTx(EJBContainerTransactionManager.java:482)
    at com.sun.ejb.containers.BaseContainer.postInvokeTx(BaseContainer.java:4592)
    at com.sun.ejb.containers.BaseContainer.postInvoke(BaseContainer.java:2125)
    at com.sun.ejb.containers.BaseContainer.postInvoke(BaseContainer.java:2095)
    at com.sun.ejb.containers.EJBLocalObjectInvocationHandler.invoke(EJBLocalObjectInvocationHandler.java:220)
    at com.sun.ejb.containers.EJBLocalObjectInvocationHandlerDelegate.invoke(EJBLocalObjectInvocationHandlerDelegate.java:90)
    at com.sun.proxy.$Proxy392.findByStatus(Unknown Source)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.jboss.weld.util.reflection.Reflections.invokeAndUnwrap(Reflections.java:410)
    at org.jboss.weld.module.ejb.EnterpriseBeanProxyMethodHandler.invoke(EnterpriseBeanProxyMethodHandler.java:134)
    at org.jboss.weld.bean.proxy.EnterpriseTargetBeanInstance.invoke(EnterpriseTargetBeanInstance.java:56)
    at org.jboss.weld.module.ejb.InjectionPointPropagatingEnterpriseTargetBeanInstance.invoke(InjectionPointPropagatingEnterpriseTargetBeanInstance.java:68)
    at org.jboss.weld.bean.proxy.ProxyMethodHandler.invoke(ProxyMethodHandler.java:106)
    at fjp.converter.dao.EmployeeDAO$1921730137$Proxy$_$$_Weld$EnterpriseProxy$.findByStatus(Unknown Source)
    at fjp.converter.servlet.Test.doGet(Test.java:36)

logs with FINEST level

[2021-11-13T09:19:12.784+0100] [Payara 5.2021.3] [LE PLUS PRÉCIS] [] [org.eclipse.persistence.default] [tid: _ThreadID=173 _ThreadName=admin-thread-pool::admin-listener(6)] [timeMillis: 1636791552784] [levelValue: 300] [[
  Missing class details for [fjp/converter/entity/converter/StatusConverterSubClass].]]

[2021-11-13T09:19:12.784+0100] [Payara 5.2021.3] [LE PLUS PRÉCIS] [] [org.eclipse.persistence.default] [tid: _ThreadID=173 _ThreadName=admin-thread-pool::admin-listener(6)] [timeMillis: 1636791552784] [levelValue: 300] [[
  Using existing class bytes for [fjp/converter/entity/converter/StatusConverterSubClass].]]

[2021-11-13T09:19:12.785+0100] [Payara 5.2021.3] [LE PLUS PRÉCIS] [] [org.eclipse.persistence.default] [tid: _ThreadID=173 _ThreadName=admin-thread-pool::admin-listener(6)] [timeMillis: 1636791552785] [levelValue: 300] [[
  Missing class details for [fjp/converter/entity/converter/EnumCodeConverter].]]

[2021-11-13T09:19:12.785+0100] [Payara 5.2021.3] [LE PLUS PRÉCIS] [] [org.eclipse.persistence.default] [tid: _ThreadID=173 _ThreadName=admin-thread-pool::admin-listener(6)] [timeMillis: 1636791552785] [levelValue: 300] [[
  Using existing class bytes for [fjp/converter/entity/converter/EnumCodeConverter].]]

[2021-11-13T09:19:12.790+0100] [Payara 5.2021.3] [INFOS] [] [org.eclipse.persistence.session./file:/home/frederic/payara5/glassfish/domains/domain1/applications/converter-1.0/WEB-INF/classes/_primary] [tid: _ThreadID=173 _ThreadName=admin-thread-pool::admin-listener(6)] [timeMillis: 1636791552790] [levelValue: 800] [[
  EclipseLink, version: Eclipse Persistence Services - 2.7.7.payara-p3]]

[2021-11-13T09:19:12.809+0100] [Payara 5.2021.3] [CONFIG] [] [org.eclipse.persistence.default] [tid: _ThreadID=173 _ThreadName=admin-thread-pool::admin-listener(6)] [timeMillis: 1636791552809] [levelValue: 700] [[
  The default table generator could not locate or convert a java type (class fjp.converter.entity.Employee$Status) into a database type for database field (EMPLOYEE.STATUS). The generator uses "java.lang.String" as default java type for the field.]]

Solution

  • It's definitely a bug in EclipseLink.

    Fortunately, there is a workaround. The AttributeConverter interface must be added to the subclass. It's totally useless as the superclass already implements it.