{"id":19359968,"url":"https://github.com/darrachequesne/spring-data-jpa-datatables","last_synced_at":"2025-05-14T20:02:24.169Z","repository":{"id":36197909,"uuid":"40502132","full_name":"darrachequesne/spring-data-jpa-datatables","owner":"darrachequesne","description":"Spring Data JPA extension to work with the great jQuery plugin DataTables (https://datatables.net/)","archived":false,"fork":false,"pushed_at":"2025-02-18T00:11:07.000Z","size":263,"stargazers_count":454,"open_issues_count":18,"forks_count":174,"subscribers_count":43,"default_branch":"main","last_synced_at":"2025-04-07T04:01:34.283Z","etag":null,"topics":["datatables","hacktoberfest","hacktoberfest2021","java","spring","spring-data"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/darrachequesne.png","metadata":{"files":{"readme":"README.md","changelog":"HISTORY.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2015-08-10T19:38:33.000Z","updated_at":"2025-02-18T00:11:10.000Z","dependencies_parsed_at":"2024-03-03T12:25:39.497Z","dependency_job_id":"06350bfc-749a-47e0-87db-503e05d311e1","html_url":"https://github.com/darrachequesne/spring-data-jpa-datatables","commit_stats":{"total_commits":121,"total_committers":5,"mean_commits":24.2,"dds":0.03305785123966942,"last_synced_commit":"4a9653b408238daa7f7bceadd80eca0ef601e4e1"},"previous_names":[],"tags_count":20,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/darrachequesne%2Fspring-data-jpa-datatables","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/darrachequesne%2Fspring-data-jpa-datatables/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/darrachequesne%2Fspring-data-jpa-datatables/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/darrachequesne%2Fspring-data-jpa-datatables/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/darrachequesne","download_url":"https://codeload.github.com/darrachequesne/spring-data-jpa-datatables/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248837042,"owners_count":21169373,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["datatables","hacktoberfest","hacktoberfest2021","java","spring","spring-data"],"created_at":"2024-11-10T07:16:41.689Z","updated_at":"2025-04-14T06:40:26.575Z","avatar_url":"https://github.com/darrachequesne.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Build Status](https://github.com/darrachequesne/spring-data-jpa-datatables/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/darrachequesne/spring-data-jpa-datatables/actions)\n[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.darrachequesne/spring-data-jpa-datatables/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.darrachequesne/spring-data-jpa-datatables)\n\n# spring-data-jpa-datatables\n\nThis project is an extension of the [Spring Data JPA](https://github.com/spring-projects/spring-data-jpa) project to ease its use with jQuery plugin [DataTables](http://datatables.net/) with **server-side processing enabled**.\n\nThis will allow you to handle the Ajax requests sent by DataTables for each draw of the information on the page (i.e. when paging, ordering, searching, etc.) from Spring **@RestController**.\n\nFor a MongoDB counterpart, please see [spring-data-mongodb-datatables](https://github.com/darrachequesne/spring-data-mongodb-datatables).\n\n**Example:**\n\n```java\n@RestController\npublic class UserRestController {\n\n  @Autowired\n  private UserRepository userRepository;\n\n  @RequestMapping(value = \"/data/users\", method = RequestMethod.GET)\n  public DataTablesOutput\u003cUser\u003e getUsers(@Valid DataTablesInput input) {\n    return userRepository.findAll(input);\n  }\n}\n```\n\n![Example](https://user-images.githubusercontent.com/13031701/43364754-92f8de16-9320-11e8-9ee2-cc072e1eef8c.gif)\n\n\n## Contents\n\n- [Maven dependency](#maven-dependency)\n- [Getting started](#getting-started)\n  - [Step 1 - Enable the use of the `DataTablesRepository` factory](#step-1---enable-the-use-of-the-datatablesrepository-factory)\n  - [Step 2 - Create a new entity](#step-2---create-a-new-entity)\n  - [Step 3 - Extend the DataTablesRepository interface](#step-3---extend-the-datatablesrepository-interface)\n  - [Step 4 - Use the repository in your controllers](#step-4---use-the-repository-in-your-controllers)\n  - [Step 5 - On the client-side, create a new DataTable object](#step-5---on-the-client-side-create-a-new-datatable-object)\n  - [Step 6 - Fix the serialization / deserialization of the query parameters](#step-6---fix-the-serialization--deserialization-of-the-query-parameters)\n- [API](#api)\n- [How to](#how-to)\n  - [Apply filters](#apply-filters)\n  - [Manage non-searchable fields](#manage-non-searchable-fields)\n  - [Limit the exposed attributes of the entities](#limit-the-exposed-attributes-of-the-entities)\n  - [Search on a rendered column](#search-on-a-rendered-column)\n  - [Use with the SearchPanes extension](#use-with-the-searchpanes-extension)\n  - [Handle `@OneToMany` and `@ManyToMany` relationships](#handle-onetomany-and-manytomany-relationships)\n  - [Search for a specific value in a column](#search-for-a-specific-value-in-a-column)\n- [Examples of additional specification](#examples-of-additional-specification)\n  - [Specific date](#specific-date)\n  - [Range of integers](#range-of-integers)\n  - [Range of dates](#range-of-dates)\n- [Troubleshooting](#troubleshooting)\n\n## Maven dependency\n\n```xml\n\u003cdependency\u003e\n  \u003cgroupId\u003ecom.github.darrachequesne\u003c/groupId\u003e\n  \u003cartifactId\u003espring-data-jpa-datatables\u003c/artifactId\u003e\n  \u003cversion\u003e7.0.1\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\nCompatibility with Spring Boot:\n\n| Version       | Spring Boot version   |\n|---------------|-----------------------|\n| 7.x           | `\u003e= 3.4.0`            |\n| 6.x           | `\u003e= 3.0.0 \u0026\u0026 \u003c 3.4.0` |\n| 5.x           | `\u003e= 2.O.0 \u0026\u0026 \u003c 3.0.0` |\n| 4.x and below | `\u003e= 1.O.0 \u0026\u0026 \u003c 2.0.0` |\n\n\nBack to [top](#contents).\n\n\n## Getting started\n\nPlease see the [sample project](https://github.com/darrachequesne/spring-data-jpa-datatables-sample) for a complete example.\n\n### Step 1 - Enable the use of the `DataTablesRepository` factory\n\nWith either\n\n```java\n@Configuration\n@EnableJpaRepositories(repositoryFactoryBeanClass = DataTablesRepositoryFactoryBean.class)\npublic class DataTablesConfiguration {}\n```\n\nor its XML counterpart\n\n```xml\n\u003cjpa:repositories factory-class=\"org.springframework.data.jpa.datatables.repository.DataTablesRepositoryFactoryBean\" /\u003e\n```\n\nYou can restrict the scope of the factory with `@EnableJpaRepositories(repositoryFactoryBeanClass = DataTablesRepositoryFactoryBean.class, basePackages = \"my.package.for.datatables.repositories\")`. In that case, only the repositories in the given package will be instantiated as `DataTablesRepositoryImpl` on run.\n\n```java\n@Configuration\n@EnableJpaRepositories(basePackages = \"my.default.package\")\npublic class DefaultJpaConfiguration {}\n\n@Configuration\n@EnableJpaRepositories(repositoryFactoryBeanClass = DataTablesRepositoryFactoryBean.class, basePackages = \"my.package.for.datatables.repositories\")\npublic class DataTablesConfiguration {}\n```\n\n### Step 2 - Create a new entity\n\n```java\n@Entity\npublic class User {\n\n  private Integer id;\n\n  private String mail;\n\n  @ManyToOne\n  @JoinColumn(name = \"id_address\")\n  private Address address;\n\n}\n```\n\n### Step 3 - Extend the DataTablesRepository interface\n\n```java\npublic interface UserRepository extends DataTablesRepository\u003cUser, Integer\u003e {}\n```\n\nThe `DataTablesRepository` interface extends both [PagingAndSortingRepository](https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/repository/PagingAndSortingRepository.html) and [JpaSpecificationExecutor](https://docs.spring.io/spring-data/jpa/docs/current/api/org/springframework/data/jpa/repository/JpaSpecificationExecutor.html).\n\n### Step 4 - Use the repository in your controllers\n\n```java\n@RestController\n@RequiredArgsConstructor\npublic class MyController {\n  private final UserRepository userRepository;\n\n  @RequestMapping(value = \"/data/users\", method = RequestMethod.GET)\n  public DataTablesOutput\u003cUser\u003e getUsers(@Valid DataTablesInput input) {\n    return userRepository.findAll(input);\n  }\n}\n```\n\n### Step 5 - On the client-side, create a new DataTable object\n\n```javascript\n$(document).ready(function() {\n  var table = $('table#sample').DataTable({\n    ajax : '/data/users',\n    serverSide : true,\n    columns : [{\n      data : 'id'\n    }, {\n      data : 'mail'\n    }, {\n      data : 'address.town',\n      render: function (data, type, row) {\n        return data || '';\n      }\n    }]\n  });\n}\n```\n\n### Step 6 - Fix the serialization / deserialization of the query parameters\n\nBy default, the [parameters](https://datatables.net/manual/server-side#Sent-parameters) sent by the plugin cannot be deserialized by Spring MVC and will throw the following exception: `InvalidPropertyException: Invalid property 'columns[0][data]' of bean class [org.springframework.data.jpa.datatables.mapping.DataTablesInput]`.\n\nThere are multiple solutions to this issue:\n\n- [Solution n°1 - custom serialization](#solution-n1---custom-serialization)\n- [Solution n°2 - POST requests](#solution-n2---post-requests)\n- [Solution n°3 - manual serialization](#solution-n3---manual-serialization)\n\n#### Solution n°1 - custom serialization\n\nYou need to include the [jquery.spring-friendly.js](jquery.spring-friendly.js) file found at the root of the repository.\n\n```html\n\u003cscript src=\"jquery.spring-friendly.js\" /\u003e\n```\n\nIt overrides the default serialization of the HTTP request parameters to allow Spring MVC to correctly map them, by changing `column[0][data]` into `column[0].data` in the request payload.\n\n#### Solution n°2 - POST requests\n\nClient-side:\n\n```javascript\n$('table#sample').DataTable({\n  ajax: {\n    contentType: 'application/json',\n    url: '/data/users',\n    type: 'POST',\n    data: function(d) {\n      return JSON.stringify(d);\n    }\n  }\n})\n```\n\nServer-side:\n\n```java\n@RestController\npublic class MyController {\n  @RequestMapping(value = '/data/users', method = RequestMethod.POST)\n  public DataTablesOutput\u003cUser\u003e getUsers(@Valid @RequestBody DataTablesInput input) {\n    return userRepository.findAll(input);\n  }\n}\n```\n\n#### Solution n°3 - manual serialization\n\n```javascript\n\nfunction flatten(params) {\n  params.columns.forEach(function (column, index) {\n    params['columns[' + index + '].data'] = column.data;\n    params['columns[' + index + '].name'] = column.name;\n    params['columns[' + index + '].searchable'] = column.searchable;\n    params['columns[' + index + '].orderable'] = column.orderable;\n    params['columns[' + index + '].search.regex'] = column.search.regex;\n    params['columns[' + index + '].search.value'] = column.search.value;\n  });\n  delete params.columns;\n\n  params.order.forEach(function (order, index) {\n    params['order[' + index + '].column'] = order.column;\n    params['order[' + index + '].dir'] = order.dir;\n  });\n  delete params.order;\n\n  params['search.regex'] = params.search.regex;\n  params['search.value'] = params.search.value;\n  delete params.search;\n\n  return params;\n}\n\n$('table#sample').DataTable({\n  'ajax': {\n    'url': '/data/users',\n    'type': 'GET',\n    'data': flatten\n  }\n})\n```\n\nBack to [top](#contents).\n\n\n## API\n\nThe repositories now expose the following methods:\n\n```java\npublic interface DataTablesRepository\u003cT, ID extends Serializable\u003e {\n\n    DataTablesOutput\u003cT\u003e findAll(\n        DataTablesInput input\n    );\n\n    DataTablesOutput\u003cR\u003e findAll(\n        DataTablesInput input,\n        Function\u003cT, R\u003e converter\n    );\n\n    DataTablesOutput\u003cT\u003e findAll(\n        DataTablesInput input,\n        Specification\u003cT\u003e additionalSpecification\n    );\n\n    DataTablesOutput\u003cT\u003e findAll(\n        DataTablesInput input,\n        Specification\u003cT\u003e additionalSpecification,\n        Specification\u003cT\u003e preFilteringSpecification\n    );\n\n    DataTablesOutput\u003cR\u003e findAll(\n        DataTablesInput input,\n        Specification\u003cT\u003e additionalSpecification,\n        Specification\u003cT\u003e preFilteringSpecification,\n        Function\u003cT, R\u003e converter\n    );\n}\n```\n\n**Note**: since version 2.0, QueryDSL is also supported:\n* replace `DataTablesRepositoryFactoryBean` with `QDataTablesRepositoryFactoryBean`\n* replace `DataTablesRepository` with `QDataTablesRepository`\n\nand your repositories will now expose:\n\n```java\npublic interface QDataTablesRepository\u003cT, ID extends Serializable\u003e {\n\n    DataTablesOutput\u003cT\u003e findAll(\n        DataTablesInput input\n    );\n\n    DataTablesOutput\u003cR\u003e findAll(\n        DataTablesInput input,\n        Function\u003cT, R\u003e converter\n    );\n\n    DataTablesOutput\u003cT\u003e findAll(\n        DataTablesInput input,\n        Predicate additionalPredicate\n    );\n\n    DataTablesOutput\u003cT\u003e findAll(\n        DataTablesInput input,\n        Predicate additionalPredicate,\n        Predicate preFilteringPredicate\n    );\n\n    DataTablesOutput\u003cR\u003e findAll(\n        DataTablesInput input,\n        Predicate additionalPredicate,\n        Predicate preFilteringPredicate,\n        Function\u003cT, R\u003e converter\n    );\n}\n```\n\nYour controllers should be able to handle the parameters sent by DataTables:\n\n```java\n@RestController\npublic class UserRestController {\n\n  @Autowired\n  private UserRepository userRepository;\n\n  @RequestMapping(value = \"/data/users\", method = RequestMethod.GET)\n  public DataTablesOutput\u003cUser\u003e getUsers(@Valid DataTablesInput input) {\n    return userRepository.findAll(input);\n  }\n\n  // or with some preprocessing\n  @RequestMapping(value = \"/data/users\", method = RequestMethod.GET)\n  public DataTablesOutput\u003cUser\u003e getUsers(@Valid DataTablesInput input) {\n    ColumnParameter parameter0 = input.getColumns().get(0);\n    Specification additionalSpecification = getAdditionalSpecification(parameter0.getSearch().getValue());\n    parameter0.getSearch().setValue(\"\");\n    return userRepository.findAll(input, additionalSpecification);\n  }\n\n  // or with an additional filter allowing to 'hide' data from the client (the filter will be applied on both the count and the data queries, and may impact the recordsTotal in the output)\n  @RequestMapping(value = \"/data/users\", method = RequestMethod.GET)\n  public DataTablesOutput\u003cUser\u003e getUsers(@Valid DataTablesInput input) {\n    return userRepository.findAll(input, null, removeHiddenEntitiesSpecification);\n  }\n}\n```\n\nThe `DataTablesInput` class maps the fields sent by the client (listed [there](https://datatables.net/manual/server-side)).\n\n[Spring documentation](http://docs.spring.io/spring-data/jpa/docs/current/reference/html/#specifications) for `Specification`\n\n## How to\n\n### Apply filters\n\nBy default, the main search field is applied to all columns.\n\nYou can apply specific filter on a column with `table.columns(\u003cyour column id\u003e).search(\u003cyour filter\u003e).draw();` (or `table.columns(\u003cyour column name\u003e:name)...`) (see [documentation](https://datatables.net/reference/api/columns().search())).\n\n**Supported filters:**\n\n* Strings (`WHERE \u003ccolumn\u003e LIKE %\u003cinput\u003e%`)\n* Booleans\n* Array of values (`WHERE \u003ccolumn\u003e IN (\u003cinput\u003e)` where input is something like 'PARAM1+PARAM2+PARAM4')\n* `NULL` values are also supported: 'PARAM1+PARAM3+NULL' becomes `WHERE (\u003ccolumn\u003e IN ('PARAM1', 'PARAM3') OR \u003ccolumn\u003e IS NULL)` (to actually search for 'NULL' string, please use `\\NULL`)\n\nAlso supports paging and sorting.\n\n**Example:**\n\n```\n{\n  \"draw\": 1,\n  \"columns\": [\n    {\n      \"data\": \"id\",\n      \"name\": \"\",\n      \"searchable\": true,\n      \"orderable\": true,\n      \"search\": {\n        \"value\": \"\",\n        \"regex\": false\n      }\n    },\n    {\n      \"data\": \"firstName\",\n      \"name\": \"\",\n      \"searchable\": true,\n      \"orderable\": true,\n      \"search\": {\n        \"value\": \"\",\n        \"regex\": false\n      }\n    },\n    {\n      \"data\": \"lastName\",\n      \"name\": \"\",\n      \"searchable\": true,\n      \"orderable\": true,\n      \"search\": {\n        \"value\": \"\",\n        \"regex\": false\n      }\n    }\n  ],\n  \"order\": [\n    {\n      \"column\": 0,\n      \"dir\": \"asc\"\n    }\n  ],\n  \"start\": 0,\n  \"length\": 10,\n  \"search\": {\n    \"value\": \"john\",\n    \"regex\": false\n  }\n}\n```\n\nis converted into the following SQL (through the [Criteria API](https://www.objectdb.com/java/jpa/query/criteria)):\n\n```sql\nSELECT\n    user0_.id AS id1_0_0_,\n    user0_.first_name AS first_na3_0_0_,\n    user0_.last_name AS last_nam4_0_0_\nFROM\n    users user0_\nWHERE\n    user0_.id LIKE \"%john%\"\n    OR user0_.first_name LIKE \"%john%\"\n    OR user0_.last_name LIKE \"%john%\"\nORDER BY user0_.id ASC\nLIMIT 10\n```\n\n**Note**: the `regex` flag is currently ignored because JPQL only supports `LIKE` expressions (with `%` and `_` tokens).\n\nYet you should be able to use the DBMS-specific regex operator with the `CriteriaBuilder.function()` method.\n\nExample with H2 [REGEXP_LIKE](http://www.h2database.com/html/functions.html#regexp_like):\n\n```java\nColumn column = input.getColumn(\"my_column\");\ncolumn.setSearchable(false); // so the default filter will not be applied\nString regexValue = column.getSearch().getValue();\nDataTablesOutput\u003c...\u003e output = repository.findAll(input, (root, query, builder) -\u003e {\n  Expression\u003cString\u003e regex = builder.function(\"REGEXP_LIKE\", String.class, root.get(\"my_column\"), builder.literal(regexValue));\n  return builder.equal(regex, builder.literal(1));\n});\n```\n\n### Manage non-searchable fields\n\nIf you have a column that does not match an attribute on the server-side (for example, an 'Edit' button), you'll have to set the [searchable](https://datatables.net/reference/option/columns.searchable) and [orderable](https://datatables.net/reference/option/columns.orderable) attributes to `false`.\n\n```javascript\n$(document).ready(function() {\n  var table = $('table#sample').DataTable({\n    'ajax' : '/data/users',\n    'serverSide' : true,\n    columns : [{\n      data: 'id'\n    }, {\n      data: 'mail'\n    }, {\n      searchable: false,\n      orderable: false\n    }]\n  });\n});\n```\n\n### Limit the exposed attributes of the entities\n\nThere are several ways to restrict the attributes of an entity on the server-side:\n\n- [with a DTO](#with-a-dto)\n- [with `@JsonView`](#with-jsonview)\n- [with `@JsonIgnore`](#with-jsonignore)\n\n#### With a DTO\n\n```java\n@RestController\npublic class UserRestController {\n\n  @Autowired\n  private UserRepository userRepository;\n\n  @RequestMapping(value = \"/data/users\", method = RequestMethod.GET)\n  public DataTablesOutput\u003cUserDTO\u003e getUsers(@Valid DataTablesInput input) {\n    return userRepository.findAll(input, toUserDTO);\n  }\n}\n```\n\nThe `toUserDTO()` method converts the `User` entity into a `UserDTO` object. You can also use a mapping framework such as [Orika](https://github.com/orika-mapper/orika) or [MapStruct](https://mapstruct.org/).\n\n#### With `@JsonView`\n\n```java\n@Entity\npublic class User {\n\n  @JsonView(DataTablesOutput.View.class)\n  private Integer id;\n\n  // ignored\n  private String mail;\n\n}\n\n@RestController\npublic class UserRestController {\n\n  @Autowired\n  private UserRepository userRepository;\n\n  @JsonView(DataTablesOutput.View.class)\n  @RequestMapping(value = \"/data/users\", method = RequestMethod.GET)\n  public DataTablesOutput\u003cUser\u003e getUsers(@Valid DataTablesInput input) {\n    return userRepository.findAll(input);\n  }\n}\n\n```\n\n#### With `@JsonIgnore`\n\n```java\n@Entity\npublic class User {\n\n  private Integer id;\n\n  @JsonIgnore\n  private String mail;\n\n}\n```\n\n### Search on a rendered column\n\nLet's say you have an `User` entity with two attributes, `firstName` and `lastName`.\n\nTo display the rendered column on the client-side:\n\n```js\n$('table#sample').DataTable({\n  ajax: '/data/users',\n  serverSide: true,\n  columns : [\n    {\n      data: 'fullName',\n      render: (_, __, row) =\u003e `${row.firstName} ${row.lastName}`,\n      searchable: false,\n      orderable: false\n    }\n  ]\n});\n```\n\nBoth `searchable` and `orderable` option are necessary, because the `User` entity has no`fullName` attribute.\n\nTo filter on the server-side, you'll have to manually create the matching specification:\n\n```java\n@RequestMapping(value = \"/data/users\", method = RequestMethod.GET)\npublic DataTablesOutput\u003cUser\u003e list(@Valid DataTablesInput input) {\n    String searchValue = escapeContent(input.getSearch().getValue());\n    input.getSearch().setValue(\"\"); // prevent search on other fields\n\n    Specification\u003cUser\u003e fullNameSpecification = (Specification\u003cUser\u003e) (root, query, criteriaBuilder) -\u003e {\n        if (!hasText(searchValue)) {\n            return null;\n        }\n        String[] parts = searchValue.split(\" \");\n        Expression\u003cString\u003e firstNameExpression = criteriaBuilder.lower(root.get(\"firstName\"));\n        Expression\u003cString\u003e lastNameExpression = criteriaBuilder.lower(root.get(\"lastName\"));\n        if (parts.length == 2 \u0026\u0026 hasText(parts[0]) \u0026\u0026 hasText(parts[1])) {\n            return criteriaBuilder.or(\n                    criteriaBuilder.and(\n                            criteriaBuilder.equal(firstNameExpression, parts[0]),\n                            criteriaBuilder.like(lastNameExpression, parts[1] + \"%\", '~')\n                    ),\n                    criteriaBuilder.and(\n                            criteriaBuilder.equal(lastNameExpression, parts[0]),\n                            criteriaBuilder.like(firstNameExpression, parts[1] + \"%\", '~')\n                    )\n            );\n        } else {\n            return criteriaBuilder.or(\n                    criteriaBuilder.like(firstNameExpression, searchValue + \"%\", '~'),\n                    criteriaBuilder.like(lastNameExpression, searchValue + \"%\", '~')\n            );\n        }\n    };\n    return userRepository.findAll(input, fullNameSpecification);\n}\n\nprivate String escapeContent(String content) {\n    return content\n            .replaceAll(\"~\", \"~~\")\n            .replaceAll(\"%\", \"~%\")\n            .replaceAll(\"_\", \"~_\")\n            .trim()\n            .toLowerCase();\n}\n```\n\nYou can find a complete example [here](https://github.com/darrachequesne/spring-data-jpa-datatables-sample).\n\nBack to [top](#contents).\n\n### Use with the SearchPanes extension\n\nServer-side:\n\n```java\n@RestController\n@RequiredArgsConstructor\npublic class UserRestController {\n  private final UserRepository userRepository;\n\n  @RequestMapping(value = \"/data/users\", method = RequestMethod.GET)\n  public DataTablesOutput\u003cUser\u003e getUsers(@Valid DataTablesInput input, @RequestParam Map\u003cString, String\u003e queryParams) {\n    input.parseSearchPanesFromQueryParams(queryParams, Arrays.asList(\"position\", \"status\"));\n    return userRepository.findAll(input);\n  }\n}\n```\n\nClient-side:\n\n```js\n$(document).ready(function() {\n  var table = $('table#sample').DataTable({\n    ajax : '/data/users',\n    serverSide: true,\n    dom: 'Pfrtip',\n    columns : [{\n      data : 'id'\n    }, {\n      data : 'mail'\n    }, {\n      data : 'position'\n    }, {\n      data : 'status'\n    }]\n  });\n});\n```\n\nWith the [`searchPanes.cascadePanes`](https://datatables.net/reference/feature/searchPanes.cascadePanes) feature:\n\n```js\n$(document).ready(function() {\n  var table = $('table#sample').DataTable({\n    ajax : '/data/users',\n    serverSide: true,\n    dom: 'Pfrtip',\n    columns : [{\n      data : 'id'\n    }, {\n      data : 'mail'\n    }, {\n      data : 'position',\n      searchPanes: {\n        show: true,\n      }\n    }, {\n      data : 'status',\n      searchPanes: {\n        show: true,\n      }\n    }],\n    searchPanes: {\n      cascadePanes: true,\n    },\n  });\n});\n```\n\nRegarding the deserialization issue detailed [above](#step-6---fix-the-serialization--deserialization-of-the-query-parameters), here is the compatibility matrix:\n\n| Solution                                                    | Compatibility with the SearchPanes extension |\n|-------------------------------------------------------------|----------------------------------------------|\n| [Custom serialization](#solution-n1---custom-serialization) | YES                                          |\n| [POST requests](#solution-n2---post-requests)               | NO                                           |\n| [Manual serialization](#solution-n3---manual-serialization) | NO                                           |\n\n### Handle `@OneToMany` and `@ManyToMany` relationships\n\nFor performance reasons, the library will not fetch `@OneToMany` and `@ManyToMany` relationships. This means that you may encounter the following errors:\n\n- when accessing a related entity in your code:\n\n```\norg.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: xxxx: could not initialize proxy - no Session\n```\n\n- or during the serialization to JSON:\n\n```\norg.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: failed to lazily initialize a collection of role [...]\n```\n\nThere are several possible solutions to this problem:\n\n| Method                                                                          | Total number of queries |\n|---------------------------------------------------------------------------------|-------------------------|\n| [With `FetchType.EAGER`](#with-fetchtypeeager)                                  | `3 + 1 per entity`      |\n| [With `spring.jpa.open-in-view` to `true`](#with-springjpaopen-in-view-to-true) | `3 + 1 per entity`      |\n| [With `Hibernate.initialize()`](#with-hibernateinitialize)                      | `3 + 1 per entity`      |\n| [With a FETCH JOIN query](#with-a-fetch-join-query) (recommended)               | `4`                     |\n\n#### With `FetchType.EAGER`\n\nBy using `FetchType.EAGER`, the related entities will automatically be loaded by the persistence provider.\n\n```java\n@Entity\npublic class User {\n  @Id private long id;\n\n  @ManyToMany(fetch = FetchType.EAGER)\n  @JoinTable(\n    name = \"users_groups\",\n    joinColumns = { @JoinColumn(name = \"user_id\") },\n    inverseJoinColumns = { @JoinColumn(name = \"group_id\") }\n  )\n  private List\u003cGroup\u003e groups = new ArrayList\u003c\u003e();\n\n  // ...\n}\n```\n\nDownside: the `Group` entities will be loaded whenever you load an `User` entity, which may not always be necessary.\n\nNote: you can log the SQL queries by setting `spring.jpa.show-sql` to `true` in your configuration:\n\n```\nHibernate: select count(*) from users u1_0\nHibernate: select u1_0.id,u1_0.name from users u1_0 where 1=1 order by u1_0.id asc offset ? rows fetch first ? rows only\nHibernate: select g1_0.user_id,g1_1.id,g1_1.name from user_groups g1_0 join groups g1_1 on g1_1.id=g1_0.group_id where g1_0.user_id=?\nHibernate: select g1_0.user_id,g1_1.id,g1_1.name from user_groups g1_0 join groups g1_1 on g1_1.id=g1_0.group_id where g1_0.user_id=?\n[...] (one per user entity in the output)\nHibernate: select count(u1_0.id) from users u1_0 where 1=1\n```\n\n#### With `spring.jpa.open-in-view` to `true`\n\nWith `spring.jpa.open-in-view: true` in your configuration, Spring will create a new Hibernate Session available during the whole HTTP request, so the related entities will automatically be loaded when needed.\n\nDownside: this option might lead to performance issues and is generally not recommended, so please use with caution.\n\nSee also: https://www.baeldung.com/spring-open-session-in-view\n\n#### With `Hibernate.initialize()`\n\n```java\n\n@Repository\npublic class CustomUserRepository {\n  private final UserRepository userRepository;\n\n  @Transactional(readOnly = true)\n  public DataTablesOutput\u003cUser\u003e findAll(DataTablesInput input) {\n    DataTablesOutput\u003cUser\u003e output = userRepository.findAll(input);\n\n    output.getData().forEach(user -\u003e Hibernate.initialize(user.getGroups()));\n\n    return output;\n  }\n}\n```\n\nDownside: like the previous solutions, this method will generate one additional SQL query per user entity.\n\n#### With a FETCH JOIN query\n\n```java\n@Repository\npublic class CustomUserRepository {\n  private final UserRepository userRepository;\n  private final EntityManager entityManager;\n\n  @Transactional(readOnly = true)\n  public DataTablesOutput\u003cUser\u003e findAll(DataTablesInput input) {\n    DataTablesOutput\u003cUser\u003e output = userRepository.findAll(input);\n\n    List\u003cLong\u003e ids = output.getData().stream().map(User::getId).collect(Collectors.toList());\n\n    Map\u003cLong, List\u003cGroup\u003e\u003e userGroups = entityManager\n      .createQuery(\"SELECT u FROM User u LEFT JOIN FETCH u.groups WHERE u.id IN :ids\", User.class)\n      .setParameter(\"ids\", ids)\n      .getResultList()\n      .stream()\n      .collect(Collectors.toMap(User::getId, User::getGroups));\n\n    output.getData().forEach(user -\u003e user.setGroups(userGroups.get(user.getId())));\n\n    return output;\n  }\n}\n```\n\nWhile a bit harder to read, this method only triggers one additional SQL query to fetch all the `Group` entities.\n\n\n### Search for a specific value in a column\n\nBy default, adding a search value will be converted to a `WHERE \u003ccolumn\u003e LIKE %\u003cvalue\u003e%` clause.\n\nYou can have an exact match (`WHERE \u003ccolumn\u003e IN (\u003cvalues\u003e)`) with:\n\n```js\n// single value\nmyTable.column($columnIndex).search(\"value1+\").draw();\n\n// multiple values\nmyTable.column($columnIndex).search(\"value1+value2+value3\").draw();\n```\n\nNote: the column in the database will be inferred as a string. For more complex use cases, you will need a proper specification.\n\n\n## Examples of additional specification\n\n- [Specific date](#specific-date)\n- [Range of integers](#range-of-integers)\n- [Range of dates](#range-of-dates)\n\n### Specific date\n\n```java\nclass DateSpecification implements Specification\u003cMyEntity\u003e {\n  private final LocalDate value;\n\n  DateSpecification(Column column) {\n    String value = column.getSearch().getValue();\n    column.setSearchable(false); // either here or in the table definition\n    this.value = parseValue(value);\n  }\n\n  private LocalDate parseValue(String value) {\n    if (hasText(value)) {\n      try {\n        return LocalDate.parse(value);\n      } catch (DateTimeParseException e) {\n        return null;\n      }\n    }\n    return null;\n  }\n\n  @Override\n  public Predicate toPredicate(Root\u003cMyEntity\u003e root, CriteriaQuery\u003c?\u003e query, CriteriaBuilder criteriaBuilder) {\n    Expression\u003cLocalDate\u003e expr = root.get(\"myColumn\").as(LocalDate.class);\n    if (this.value != null) {\n      return criteriaBuilder.equal(expr, this.value);\n    } else {\n      return criteriaBuilder.conjunction();\n    }\n  }\n}\n```\n\nAnd then:\n\n```java\n@RestController\npublic class MyController {\n\n  @RequestMapping(value = \"/entities\", method = RequestMethod.GET)\n  public DataTablesOutput\u003cMyEntity\u003e list(@Valid DataTablesInput input) {\n    return myRepository.findAll(input, new DateSpecification(input.getColumn(\"myField\")));\n  }\n}\n```\n\n### Range of integers\n\n```java\nclass IntegerRangeSpecification implements Specification\u003cMyEntity\u003e {\n  private final Integer minValue;\n  private final Integer maxValue;\n\n  IntegerRangeSpecification(Column column) {\n    String value = column.getSearch().getValue();\n    column.setSearchable(false); // either here or in the table definition\n    if (!hasText(value)) {\n      minValue = maxValue = null;\n      return;\n    }\n    String[] bounds = value.split(\";\");\n    minValue = parseValue(bounds, 0);\n    maxValue = parseValue(bounds, 1);\n  }\n\n  private Integer parseValue(String[] bounds, int index) {\n    if (bounds.length \u003e index \u0026\u0026 hasText(bounds[index])) {\n      try {\n        return Integer.valueOf(bounds[index]);\n      } catch (NumberFormatException e) {\n        return null;\n      }\n    }\n    return null;\n  }\n\n  @Override\n  public Predicate toPredicate(Root\u003cMyEntity\u003e root, CriteriaQuery\u003c?\u003e query, CriteriaBuilder criteriaBuilder) {\n    Expression\u003cInteger\u003e expr = root.get(\"myColumn\").as(Integer.class);\n    if (this.minValue != null \u0026\u0026 this.maxValue != null) {\n      return criteriaBuilder.between(expr, this.minValue, this.maxValue);\n    } else if (this.minValue != null) {\n      return criteriaBuilder.greaterThanOrEqualTo(expr, this.minValue);\n    } else if (this.maxValue != null) {\n      return criteriaBuilder.lessThanOrEqualTo(expr, this.maxValue);\n    } else {\n      return criteriaBuilder.conjunction();\n    }\n  }\n}\n```\n\nAnd then:\n\n```java\n@RestController\npublic class MyController {\n\n  @RequestMapping(value = \"/entities\", method = RequestMethod.GET)\n  public DataTablesOutput\u003cMyEntity\u003e list(@Valid DataTablesInput input) {\n    return myRepository.findAll(input, new IntegerRangeSpecification(input.getColumn(\"myField\")));\n  }\n}\n```\n\nWith two text inputs on the client side:\n\n```js\nconst minValueInput = $(\"input#minValue\");\nconst maxValueInput = $(\"input#maxValue\");\n\nconst onBoundChange = () =\u003e {\n  table.column($columnIndex).search(minValueInput.val() + ';' + maxValueInput.val()).draw();\n};\n\nminValueInput.on(\"input\", onBoundChange);\nmaxValueInput.on(\"input\", onBoundChange);\n```\n\n### Range of dates\n\n```java\nclass DateRangeSpecification implements Specification\u003cMyEntity\u003e {\n  private final LocalDate minValue;\n  private final LocalDate maxValue;\n\n  DateRangeSpecification(Column column) {\n    String value = column.getSearch().getValue();\n    column.setSearchable(false); // either here or in the table definition\n    if (!hasText(value)) {\n      minValue = maxValue = null;\n      return;\n    }\n    String[] bounds = value.split(\";\");\n    minValue = parseValue(bounds, 0);\n    maxValue = parseValue(bounds, 1);\n  }\n\n  private LocalDate parseValue(String[] bounds, int index) {\n    if (bounds.length \u003e index \u0026\u0026 hasText(bounds[index])) {\n      try {\n        return LocalDate.parse(bounds[index]);\n      } catch (DateTimeParseException e) {\n        return null;\n      }\n    }\n    return null;\n  }\n\n  @Override\n  public Predicate toPredicate(Root\u003cMyEntity\u003e root, CriteriaQuery\u003c?\u003e query, CriteriaBuilder criteriaBuilder) {\n    Expression\u003cLocalDate\u003e expr = root.get(\"myColumn\").as(LocalDate.class);\n    if (this.minValue != null \u0026\u0026 this.maxValue != null) {\n      return criteriaBuilder.between(expr, this.minValue, this.maxValue);\n    } else if (minValue != null) {\n      return criteriaBuilder.greaterThanOrEqualTo(expr, this.minValue);\n    } else if (maxValue != null) {\n      return criteriaBuilder.lessThanOrEqualTo(expr, this.maxValue);\n    } else {\n      return criteriaBuilder.conjunction();\n    }\n  }\n}\n```\n\nAnd then:\n\n```java\n@RestController\npublic class MyController {\n\n  @RequestMapping(value = \"/entities\", method = RequestMethod.GET)\n  public DataTablesOutput\u003cMyEntity\u003e list(@Valid DataTablesInput input) {\n    return myRepository.findAll(input, new DateRangeSpecification(input.getColumn(\"myField\")));\n  }\n}\n```\n\nBack to [top](#contents).\n\n\n## Troubleshooting\n\n- `Invalid property 'columns[0][data]' of bean class [org.springframework.data.jpa.datatables.mapping.DataTablesInput]`\n\nPlease see [here](#step-6---fix-the-serialization--deserialization-of-the-query-parameters).\n\n- `java.lang.IllegalArgumentException: Unable to locate Attribute with the the given name ...`\n\nIt seems you have a column with a `data` attribute that does not match the attribute of the `@Entity` on the server-side.\n\nPlease see [here](#manage-non-searchable-fields).\n\n- `java.lang.NoClassDefFoundError: org/hibernate/jpa/criteria/path/AbstractPathImpl`\n\nThe versions `\u003e= 5.0.0` of the library are not compatible with Spring 4 (Spring Boot 1.x), please use the previous versions.\n\n\nBack to [top](#contents).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdarrachequesne%2Fspring-data-jpa-datatables","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdarrachequesne%2Fspring-data-jpa-datatables","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdarrachequesne%2Fspring-data-jpa-datatables/lists"}