Hibernate 代理和 JSF select 一个菜单不工作

Hibernate proxies and JSF select one menu not working

配置:WildFly 8.2 服务器、JPA 2.1、Hibernate 4.3.7、JSF 2.2、Mojarra 2.2.8、PrimeFaces 5.1

我想调整我的 JavaEE Web 应用程序,但是当我将 @ManyToOne(fetch=FetchType.EAGER) 转换为 @ManyToOne(fetch=FetchType.LAZY) 时,出现了一些问题

我想在 xhtml facelet 中编辑员工类别

这是模型:

@Entity
public class Employee implements Serializable {
    @Id
    private Integer id;

    @ManyToOne(fetch=FetchType.LAZY)
    @JoinColumn(name = "id_category", referencedColumnName = "id")
    private EmployeeCategory category;

    //...
}

@Entity
@Cacheable(true)
@Slf4j
public class EmployeeCategory implements Serializable {
    @Id
    private Integer id;

    private String label;

    @Override
    public boolean equals(Object o) {
        if (o==null) {
            return false;
        }
        if (!testClasses(this,o)) {
            return false;
        }
        final EmployeeCategory that = (EmployeeCategory) o;
        return this==that || this.id==that.id;
    }

    private boolean testClasses(Object o1, Object o2) {
        return getClass(o1).equals(getClass(o2));
    }

    private Class getClass(Object o) {
        if(o instanceof HibernateProxy) {
            return HibernateProxyHelper.getClassWithoutInitializingProxy(o);
        } else {
            return o.getClass();
        }
    }

    //...
}

这是 Employee 的控制器,它是一个拥有员工并将一些方法(save()、remove()...)暴露给 facelet 视图的 JSF 托管 bean:

@Named
@ViewScoped
public class EmployeeView implements Serializable {

    private Employee value;

    public Employee getValue() {
        return value;
    } 

    public void save() {
        //...
    }

    //init(), save(), remove()....
}

EmployeeCategory 的 EJB service/DAO:

@Stateless
public class EmployeeCategoryService {

    @PersistenceContext(unitName="myAppPU")
    private EntityManager entityManager;

    public List<EmployeeCategory> findAll() {
        CriteriaQuery cq = entityManager.getCriteriaBuilder().createQuery();
        cq.select(cq.from(EmployeeCategory.class));
        Query q = entityManager.createQuery(cq) ;
        return q.getResultList();
    }

    //...
}

管理的 JSF 向视图提供所有 EmployeeCategory 的列表,并且具有转换器的作用:

@Named
@ViewScoped
@Slf4j
public class EmployeeCategoryItems implements Converter, Serializable{

    @Inject
    private EmployeeCategoryService service;

    private final String defaultSelection = "select one item";

    private List<EmployeeCategory> values;

    private Map<Integer,EmployeeCategory> index;

    @PostConstruct
    protected void init() {
        values = service.findAll();
        unproxyfied();
        index = values.stream().collect(Collectors.toMap(c->c.getId(), c->c));
    }

    private void unproxyfied() {
        values.stream().map(item -> {
            if (item instanceof HibernateProxy) {
                log.debug("unproxyfied() {} was HibernateProxy",item);
                return (EmployeeCategory) ((HibernateProxy)item).getHibernateLazyInitializer().getImplementation();
            } else {
                return item;
            }
        }).collect(Collectors.toList()) ;
    }

    public String getDefaultSelection() {
        return defaultSelection;
    }

    public List<EmployeeCategory> getValues() {
        return values;
    }

    @Override
    public Object getAsObject(FacesContext context, UIComponent component, String value) {
        if (value==null || value.equals(defaultSelection)) {
            return null;
        }
        Integer id = Integer.parseInt(value);
        EmployeeCategory category = index.get(id);
        log.debug("getAsObject({}) return {}", value, category);
        return entity;
    }

    @Override
    public String getAsString(FacesContext context, UIComponent component, Object value) {
        if (value == null) {
            return null;
        }
        if(value instanceof HibernateProxy) {
            Integer id = (Integer) ((HibernateProxy)value).getHibernateLazyInitializer().getIdentifier();
            log.debug("getAsString({}) return {} HibernateProxy", value, id);
            return id.toString();
        } else {
            Integer id = ((EmployeeCategory)value).getId();
            log.debug("getAsString({}) return {}", value, id);
            return id.toString();
        }
    }
}

以及 pf 是 primefaces 组件前缀的 xhtml 页面:

<h:form>

    <pf:messages/>

    <!-- ... -->

    <pf:selectOneMenu value="#{employeeView.value.category}"
            converter="#{employeeCategoryItems}">
        <f:selectItem itemValue="#{null}"
                itemLabel="#{employeeCategoryItems.defaultSelection}"
                noSelectionOption="true"/>
        <f:selectItems value="#{employeeCategoryItems.values}"
                var="item" itemValue="#{item}" itemLabel="#{item.label}"/>
    </pf:selectOneMenu>

    <!-- ... -->

    <pf:commandButton value="Save" action="#{employeeView.save()}" update="@form"/>
</h:form>

所以当我使用延迟加载而不是急切的方式通过 HibernateProxy 替换真实的 EmployeeCategory 对象时出现问题

首先一切顺利,在页面中selectOneMenu设置为"select one item",显示类别列表,我保存成功(数据库中设置id_category)。

但是当我重新加载页面时,selectOneMenu 再次显示 "select one item" 如果我选择相同的类别并再次保存,我会收到一条 JSF 消息:"category: Validation Error: Value is not valid" 但如果我选择不同的类别,我会成功保存。

我在这个问题上进行了很多调查,正如您在代码中看到的那样,我尝试在 employeeCategoryItems 中取消代理对象。我还更改了 EmployeeCategory 的 equals() 方法以了解 HibernateProxy,我还尝试将所有注释移动到 getter 而不是字段,并且还尝试在 EmployeeService.findById() 中取消代理 employee.category 这是在 EmployeeView 中调用(此处未显示)。所有这些解决方案都不起作用。

我找到的唯一解决方案是将 employeeCategoryItems 的范围设置为 @ApplicationScoped 并在启动时加载此 JSF 托管 bean,在这种情况下,当项目首先加载到 employeeCategoryItems 之前在系统工作之前。但我无法将此解决方案应用于所有@ManyToOne...

不知道HibernateProxy和JSF(或者PrimeFaces)之间到底出了什么问题,也不知道怎么才能很好的解决

我唯一的办法就是让@ManyToOne 处于 EAGER 模式

如果有人能帮助我,那就太好了

解决方案已经差不多了,事实上我忘记了 EmployeeCategoryItems.unproxyfied() 方法中的一段代码

private void unproxyfied() {
    values = values.stream().map(item -> {
        if (item instanceof HibernateProxy) {
            return (EmployeeCategory) ((HibernateProxy)item).getHibernateLazyInitializer().getImplementation();
        } else {
            return item;
        }
    }).collect(Collectors.toList()) ;
}

有了这个,解决方案就可以了