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