AG Grid JPA Adapter

website·Indexed 29 days ago

https://smolcan.github.io/ag-grid-jpa-adapter

A lightweight Maven library for integrating AG Grid Server-Side Row Model with JPA. It provides support for server-side filtering (Text, Number, Date, Set, Multi, and Advanced filters), sorting, pagination, row grouping, tree data, pivoting, and master-detail views.

Tokens
13.5K
Snippets
60
Records
81
Agent score
75%

What's inside ag-grid-jpa-adapter

  1. Overview of ag-grid-jpa-adapter filtering types

    The ag-grid-jpa-adapter supports four primary types of user-controlled filtering and one system-level predicate for server-side data restriction:

    1. Column Filter: Filters data based on values within a specific column.
    2. Advanced Filter: A filter builder for complex conditions spanning multiple columns.
    3. External Filter: An application-level filter that operates independently of the grid's internal filtering.
    4. Quick Filter: A global text-based search that filters rows across all columns.
    5. Always Applied Predicate: A server-side predicate automatically added to every query, typically used for row-level security or tenant isolation.
  2. Choose a Column Filter type for ag-grid-jpa-adapter

    Column Filters are applied to data at the column level. The adapter provides several built-in filter implementations, as well as the ability to create custom filters. The available provided filters include:

    • Text Filter: Used for string comparisons.
    • Number Filter: Used for number comparisons.
    • Date Filter: Used for date comparisons.
    • Set Filter: An Excel-style filter for selecting specific values.
    • Multi Filter: A composite filter that allows multiple column filters to be used together.
  3. Enable Master/Detail view in AG Grid JPA adapter

    To enable the Master/Detail view, which allows nesting grids inside rows, you must call .masterDetail(true) on the QueryBuilder. The builder requires the master entity class, the master's @Id attribute, and the detail entity class.

    Key configuration requirements:

    • Master ID: Pass the master entity's @Id attribute as the second argument to QueryBuilder.builder().
    • MasterDetailParams: A configuration object that defines the detail entity class, columns to display, and the relationship mapping between master and detail records.
    QueryBuilder.builder(MasterEntity.class, MasterEntity_.id, DetailEntity.class, em)
        .masterDetail(true)
        .masterDetailParams(
            QueryBuilder.MasterDetailParams.<MasterEntity, Long, DetailEntity>builder()
                .detailClass(DetailEntity.class)
                .detailColDefs(
                    ColDef.builder(DetailEntity_.detailId).build(),
                    ColDef.builder(DetailEntity_.amount).build()
                )
                .detailMasterReferenceField(DetailEntity_.parentEntity) // Relationship mapping
                .build()
        )
        .build();
  4. Stream rows for data export

    The ag-grid-jpa-adapter does not generate CSV or Excel files directly; it provides the data rows, leaving the file formatting and column ordering to the application. Use the streamRows method to retrieve all rows that match the current grid state (filters, grouping, and sorting). Unlike getRows, streamRows ignores startRow and endRow to ensure the entire result set is processed. To prevent data duplication or loss during paged offset retrieval, rows are ordered by the request's sort model and the primary key (or group columns).
    @Transactional(readOnly = true)
    public void writeCsv(ServerSideGetRowsRequest request, Writer writer) {
        this.queryBuilder.streamRows(request).forEach(row -> writeLine(writer, row));
    }
  5. Request and handle grand total data in the frontend

    Grand total row data is returned in loadSuccessParams.grandTotalData as part of the standard getRows response. The adapter only computes this data if the request body includes needsGrandTotal: true.

    Important Caveat: When needsGrandTotal is false, the adapter returns grandTotalData as null. Because AG Grid interprets null as a command to explicitly remove the existing grand total row, you should forward it as undefined if you wish to keep the previously cached total intact.

    body: JSON.stringify({ ...params.request, needsGrandTotal: params.needsGrandTotal })
  6. Implement multiple filters on a single column using AgMultiColumnFilter

    The AgMultiColumnFilter allows multiple column filters (either provided filters or custom filters) to be applied to the same column, providing greater flexibility for data filtering. This is configured within the ColDef by assigning an AgMultiColumnFilter instance and providing a list of filters via MultiFilterParams.
    var colDef = ColDef.builder(Trade_.tradeId)
        .filter(new AgMultiColumnFilter<Long>()
            .filterParams(
                MultiFilterParams.<Long>builder()
                    .filters(
                        new AgNumberColumnFilter<Long>(),
                        new CustomNumberFilter<Long>()
                    )
                    .build()
            )
        )
        .build()
  7. Filter data and generated columns in pivot mode

    In pivot mode, filters restrict the rows used for aggregation and determine which columns are generated. Note that expanding a group does not affect which columns are generated, but applying a filter does. Filters on aggregated values follow the standard rules for aggregated value filtering. The pivotMaxGeneratedColumns limit is evaluated after filters are applied, meaning a filter can reduce the number of generated columns to bring a request back under the allowed limit.
  8. Configure Lazy Loading for Detail rows

    Lazy loading is the default behavior in the AG Grid JPA adapter. In this mode, detail data is not included in the initial master row response. When a user expands a row, AG Grid sends a separate server-side request. To support this, you must expose a separate backend endpoint that invokes getDetailRowData(masterRow).
  9. Create a custom Text Formatter for advanced filtering

    Use a textFormatter to modify text (e.g., removing accents or forcing lowercase) before the filter comparison logic is applied. The formatter takes a CriteriaBuilder and an Expression<String> and must return a new Expression<String>.

    Important: When a textFormatter is provided, the caseSensitive parameter is ignored. You must handle case conversion manually within the formatter if case-insensitivity is required.

    var colDef = ColDef.builder(Trade_.portfolio)
        .filter(new AgTextColumnFilter()
            .filterParams(
                TextFilterParams.builder()
                    .textFormatter((cb, expr) -> {
                        Expression<String> newExpression = expr;
                        // Force lowercase for case-insensitivity
                        newExpression = cb.lower(newExpression);
                        // Remove accents using SQL TRANSLATE
                        newExpression = cb.function("TRANSLATE", String.class, newExpression,
                            cb.literal("áéíóúÁÉÍÓÚüÜñÑ"),
                            cb.literal("aeiouAEIOUuUnN"));
                        
                        return newExpression;
                    })
                    .build()
            )
        )
        .build()
  10. Implement external filtering with ag-grid-jpa-adapter

    Since AG Grid's frontend does not natively wire external filters in the Server-Side Row Model, this adapter implements the logic server-side. The external filter value is passed in the request body and converted into a JPA predicate via the QueryBuilder.
  11. Implement a custom server-side filter in ag-grid-jpa-adapter

    To create a custom filter for the ag-grid-jpa-adapter, you must implement three components: a filter model, filter parameters, and the filter logic itself by extending the IFilter abstract class. The IFilter<T, FM, FP> class is parameterized by the column value type T, the filter model FM, and the filter parameters FP.
    // 1. Define the Filter Model
    public class CustomNumberFilterModel implements IFilterModel {
        private String value; // e.g., "Even", "Odd", or "All"
        // getters and setters
    }
    
    // 2. Define Filter Parameters
    public class CustomNumberFilterParams implements IFilterParams {
        private boolean includeNullValues;
        // getters and setters
    }
    
    // 3. Implement the Filter Logic
    public class CustomNumberFilter<T extends Number> extends IFilter<T, CustomNumberFilterModel, CustomNumberFilterParams> {
        
        @Override
        public CustomNumberFilterModel recognizeFilterModel(Map<String, Object> map) {
            CustomNumberFilterModel model = new CustomNumberFilterModel();
            model.setValue(map.get("value").toString());
            return model;
        }
    
        @Override
        public CustomNumberFilterParams getDefaultFilterParams() {
            return new CustomNumberFilterParams();
        }
    
        @Override
        public Predicate toPredicate(CriteriaBuilder cb, Expression<T> expression, CustomNumberFilterModel model) {
            String value = model.getValue();
            if (value == null || value.equalsIgnoreCase("All")) {
                return cb.and(); // Always true
            }
    
            Expression<Integer> integerExpression = expression.as(Integer.class);
            Predicate predicate;
            if (value.equalsIgnoreCase("Even")) {
                predicate = cb.equal(cb.mod(integerExpression, 2), 0);
            } else {
                predicate = cb.notEqual(cb.mod(integerExpression, 2), 0);
            }
            
            if (this.filterParams.isIncludeNullValues()) {
                predicate = cb.or(predicate, cb.isNull(expression));
            }
            
            return predicate;
        }
    }