You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Vaadin14 Grid使用DataProvider时全选功能异常

Vaadin 14 Grid多选状态异常问题

问题现象

在Vaadin 14(14.1.17版本)中为开启多选模式的Grid配置自定义DataProvider时,出现两类异常:

  • 点击表头全选按钮后,列表行未在UI上呈现选中高亮状态,但后端调用grid.getSelectedItems()可正常获取全量条目,即后端选中逻辑正常,仅UI状态未同步
  • 若先手动选中单行再点击全选按钮,grid.getSelectedItems()不会覆盖此前手动选中的条目,最终返回的选中项总数为Grid总条目数+1,选中状态未正确重置

相关实现代码

FormEntity实体类

@Entity
@EqualsAndHashCode
@Table(name = "forms", schema = "click", catalog = "")
public class FormEntity {

    private long id;
    private String userId;
    private String requestorType;
    private String confirmationPhone;
    private Timestamp submittedDate;
    private int status;
    private Timestamp approvedDate;
    private String comments;
    private String organization;
    private PatientEntity patientEntity;


    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", nullable = false)
    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    @Basic
    @Column(name = "user_id", nullable = true, length = 70)
    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    @Basic
    @Column(name = "requestor_type", nullable = false, length = 50)
    public String getRequestorType() {
        return requestorType;
    }

    public void setRequestorType(String requestorType) {
        this.requestorType = requestorType;
    }

    @Basic
    @Column(name = "confirmation_phone", nullable = true, length = 50)
    public String getConfirmationPhone() {
        return confirmationPhone;
    }

    public void setConfirmationPhone(String confirmationPhone) {
        this.confirmationPhone = confirmationPhone;
    }


    @Basic
    @Column(name = "submitted_date", nullable = false)
    public Timestamp getSubmittedDate() {
        return submittedDate;
    }

    public void setSubmittedDate(Timestamp submittedDate) {
        this.submittedDate = submittedDate;
    }

    @Basic
    @Column(name = "status", nullable = false, length = 11)
    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    @Basic
    @Column(name = "approved_date", nullable = false)
    public Timestamp getApprovedDate() {
        return approvedDate;
    }

    public void setApprovedDate(Timestamp approvedDate) {
        this.approvedDate = approvedDate;
    }

    @Basic
    @Column(name = "comments", nullable = true)
    public String getComments() {
        return comments;
    }

    public void setComments(String comments) {
        this.comments = comments;
    }

    @Basic
    @Column(name = "organization", nullable = true)
    public String getOrganization() {
        return organization;
    }

    public void setOrganization(String organization) {
        this.organization = organization;
    }

    @ManyToOne
    @JoinColumn(name = "patient_id", referencedColumnName = "id", nullable = false)
    public PatientEntity getPatientEntity() {
        return patientEntity;
    }

    public void setPatientEntity(PatientEntity patientEntity) {
        this.patientEntity = patientEntity;
    }
}

DataProvider实现

private ConfigurableFilterDataProvider<FormEntity, Void, Map<String, Object>> getFilteringDataProvider(OrganizationDto organizationDto) {
    DataProvider<FormEntity, Map<String, Object>> dataProvider =
            DataProvider.fromFilteringCallbacks(
                    query -> {
                        Map.Entry<String, String> sortOrder = null;
                        List<QuerySortOrder> sortOrderList = query.getSortOrders();

                        if (!sortOrderList.isEmpty()) {
                            QuerySortOrder querySortOrder = sortOrderList.get(0);
                            sortOrder = new AbstractMap.SimpleEntry<>(querySortOrder.getSorted(),
                                    querySortOrder.getDirection().name());
                        }

                        Map<String, Object> filter = query.getFilter().orElse(null);

                        List<FormEntity> rv = null;
                        rv = registrationService.fetchForms(organizationDto.getIdentifier(), filter, query.getOffset(), query.getLimit(), sortOrder).getContent();
                        System.out.println("List size:" + rv.size());
                        return rv.stream();
                    },
                    query -> {
                        Map<String, Object> filter = query.getFilter().orElse(null);

                        long count = 0;
                        // 统计匹配当前过滤条件的总条目数
                        count = registrationService.fetchForms(organizationDto.getIdentifier(), filter, 0, 1, null).getTotalElements();
                        System.out.println("Count:" + count);

                        return (int) count;
                    }
            );

    return dataProvider.withConfigurableFilter();
}

问题根因

Grid的多选状态判断完全依赖实体类的equals()和hashCode()方法。当前实体类使用Lombok的@EqualsAndHashCode默认配置,会基于类下所有字段(包括关联的PatientEntity对象)生成相等性判断逻辑:

  1. 每次DataProvider回调查询返回的FormEntity实例,哪怕主键id相同,也会因为关联对象的引用差异、JPA代理对象状态差异,导致equals()判定为不相等。前端渲染行时,无法在选中集合中匹配到当前行对应的实例,因此不会显示选中样式。
  2. 点击全选时,因为新查询出来的条目和之前手动选中的条目equals()判定不相等,选中集合无法移除旧的手动选中条目,最终出现总选中数=总条目数+1的问题。

修复方案

JPA实体的相等性判断永远只需要基于主键实现,不要使用全字段生成equals/hashCode:

  1. 修改实体类的@EqualsAndHashCode注解配置,仅将主键id纳入相等性判断:
@Entity
// 关闭全字段equals生成,开启显式标记字段纳入判断
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@Table(name = "forms", schema = "click", catalog = "")
public class FormEntity {
    // 其余代码保持不变

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", nullable = false)
    @EqualsAndHashCode.Include // 仅将id作为equals/hashCode的判断依据
    public long getId() {
        return id;
    }

    // 其余方法保持不变
}
  1. 若使用JPA懒加载,避免将未初始化的代理关联对象传入Grid,可提前初始化关联字段,或直接使用只包含业务字段的DTO作为Grid的数据源,进一步避免相等性判断异常。

内容的提问来源于stack exchange,提问作者despoina.k

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 18:57:18