{"id":25374195,"url":"https://github.com/jwcarman/jpa-utils","last_synced_at":"2025-10-24T05:40:09.400Z","repository":{"id":277468060,"uuid":"932518310","full_name":"jwcarman/jpa-utils","owner":"jwcarman","description":"A collection of Jakarta Persistence API utilities","archived":false,"fork":false,"pushed_at":"2025-02-25T03:07:01.000Z","size":49,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-09T09:37:03.859Z","etag":null,"topics":["jakarta-persistence","javaee","jpa"],"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/jwcarman.png","metadata":{"files":{"readme":"README.md","changelog":null,"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":"2025-02-14T03:25:13.000Z","updated_at":"2025-02-25T03:07:04.000Z","dependencies_parsed_at":null,"dependency_job_id":"f8ad1e65-298f-45da-b1c7-af37f2de5897","html_url":"https://github.com/jwcarman/jpa-utils","commit_stats":null,"previous_names":["jwcarman/jpa-utils"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/jwcarman/jpa-utils","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jwcarman%2Fjpa-utils","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jwcarman%2Fjpa-utils/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jwcarman%2Fjpa-utils/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jwcarman%2Fjpa-utils/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jwcarman","download_url":"https://codeload.github.com/jwcarman/jpa-utils/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jwcarman%2Fjpa-utils/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":270041298,"owners_count":24516813,"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","status":"online","status_checked_at":"2025-08-12T02:00:09.011Z","response_time":80,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["jakarta-persistence","javaee","jpa"],"created_at":"2025-02-15T03:36:52.445Z","updated_at":"2025-10-24T05:40:09.392Z","avatar_url":"https://github.com/jwcarman.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# JPA Utilities\n\n[![CI](https://github.com/jwcarman/jpa-utils/actions/workflows/maven.yml/badge.svg)](https://github.com/jwcarman/jpa-utils/actions/workflows/maven.yml)\n[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)\n[![Java](https://img.shields.io/badge/dynamic/xml?url=https://raw.githubusercontent.com/jwcarman/jpa-utils/main/pom.xml\u0026query=//*[local-name()='java.version']/text()\u0026label=Java\u0026color=orange)](https://openjdk.org/)\n[![Maven Central](https://img.shields.io/maven-central/v/org.jwcarman.jpa/jpa-utils)](https://central.sonatype.com/artifact/org.jwcarman.jpa/jpa-utils)\n\n[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=jwcarman_jpa-utils\u0026metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=jwcarman_jpa-utils)\n[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=jwcarman_jpa-utils\u0026metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=jwcarman_jpa-utils)\n[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=jwcarman_jpa-utils\u0026metric=security_rating)](https://sonarcloud.io/summary/new_code?id=jwcarman_jpa-utils)\n[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=jwcarman_jpa-utils\u0026metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=jwcarman_jpa-utils)\n[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=jwcarman_jpa-utils\u0026metric=alert_status)](https://sonarcloud.io/summary/new_code?id=jwcarman_jpa-utils)\n[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=jwcarman_jpa-utils\u0026metric=coverage)](https://sonarcloud.io/summary/new_code?id=jwcarman_jpa-utils)\n\nA collection of utilities for building REST APIs with [Jakarta Persistence API](https://jakarta.ee/specifications/persistence/), featuring framework-agnostic pagination, annotation-driven search, and optional Spring Data JPA integration.\n\n## Table of Contents\n\n- [Features](#features)\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n- [Core Modules](#core-modules)\n  - [Base Entity](#base-entity)\n  - [Pagination](#pagination)\n  - [Search](#search)\n  - [Auditing](#auditing)\n- [Spring Integration](#spring-integration)\n  - [REST Controller Integration](#rest-controller-integration)\n  - [Searchable Repository](#searchable-repository)\n- [Error Handling](#error-handling)\n- [Security Considerations](#security-considerations)\n- [Performance Optimization](#performance-optimization)\n- [Complete Example](#complete-example)\n- [Requirements](#requirements)\n- [Database Compatibility](#database-compatibility)\n- [License](#license)\n\n## Features\n\n- **🔑 UUID-based Entity IDs**: Stable, time-based UUID v7 identifiers with optimistic locking\n- **📄 Framework-Agnostic Pagination**: Clean architecture design decoupling web layer from domain\n- **🔍 Annotation-Driven Search**: Mark fields as `@Searchable` for automatic LIKE query generation\n- **🕒 Auditing Support**: Automatic tracking of creation/modification timestamps and users\n- **🌱 Spring Integration**: Optional utilities for Spring Data JPA and Spring Web\n- **🎯 Type-Safe Sorting**: Enum-based sort field definitions with automatic resolution\n- **✨ Clean Architecture**: Proper dependency direction (web → app → domain)\n\n## Installation\n\nAdd the dependency to your `pom.xml`:\n\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003eorg.jwcarman.jpa\u003c/groupId\u003e\n    \u003cartifactId\u003ejpa-utils\u003c/artifactId\u003e\n    \u003cversion\u003e1.0-SNAPSHOT\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\n## Quick Start\n\n### 1. Define Your Entity\n\n```java\n@Entity\npublic class Person extends BaseEntity {\n    @Searchable\n    private String firstName;\n\n    @Searchable\n    private String lastName;\n\n    private String email;\n\n    protected Person() {}\n\n    public Person(String firstName, String lastName, String email) {\n        this.firstName = firstName;\n        this.lastName = lastName;\n        this.email = email;\n    }\n\n    // Getters and setters...\n}\n```\n\n### 2. Define Sort Fields\n\n```java\npublic enum PersonSort implements SortPropertyProvider {\n    FIRST_NAME(\"firstName\"),\n    LAST_NAME(\"lastName\"),\n    EMAIL(\"email\");\n\n    private final String sortProperty;\n\n    PersonSort(String sortProperty) {\n        this.sortProperty = sortProperty;\n    }\n\n    @Override\n    public String getSortProperty() {\n        return sortProperty;\n    }\n}\n```\n\n### 3. Create a Repository\n\n```java\n@Repository\npublic interface PersonRepository extends SearchableRepository\u003cPerson, UUID\u003e {\n}\n```\n\n### 4. Build a REST Controller\n\n```java\n@RestController\n@RequestMapping(\"/api/persons\")\npublic class PersonController {\n\n    @Autowired\n    private PersonService personService;\n\n    @GetMapping\n    public PageDto\u003cPersonDto\u003e search(\n            @RequestParam(required = false) String query,\n            PageParams pageParams) {\n        return personService.search(query, pageParams);\n    }\n}\n```\n\n### 5. Implement the Service\n\n```java\n@Service\npublic class PersonService {\n\n    @Autowired\n    private PersonRepository personRepository;\n\n    public PageDto\u003cPersonDto\u003e search(String query, PageSpec pageSpec) {\n        Pageable pageable = Pageables.pageableOf(pageSpec, PersonSort.class);\n        Page\u003cPerson\u003e page = personRepository.search(query, pageable);\n        return Pages.pageDtoOf(page.map(PersonDto::fromEntity));\n    }\n}\n```\n\n**Example Request:**\n```\nGET /api/persons?query=john\u0026pageIndex=0\u0026pageSize=20\u0026sortBy=LAST_NAME\u0026sortDirection=ASC\n```\n\n## Core Modules\n\n### Base Entity\n\n`BaseEntity` provides UUID-based primary keys and optimistic locking for all entities.\n\n#### Key Features\n- **UUID v7 Identifiers**: Time-based epoch UUIDs for better database locality\n- **Stable IDs**: Generated at construction time, before persistence\n- **Optimistic Locking**: Built-in `@Version` field\n- **Final equals/hashCode**: Based on UUID for stable behavior\n\n#### Usage\n\n```java\n@Entity\npublic class Product extends BaseEntity {\n    private String name;\n    private BigDecimal price;\n\n    protected Product() {}\n\n    public Product(String name, BigDecimal price) {\n        this.name = name;\n        this.price = price;\n    }\n}\n```\n\n### Pagination\n\nFramework-agnostic pagination contracts that maintain clean architecture principles.\n\n#### Core Interfaces\n\n**`PageSpec`** - Pagination specification\n```java\npublic interface PageSpec {\n    Integer pageIndex();        // Zero-based page number\n    Integer pageSize();         // Items per page\n    String sortBy();           // Sort field name (e.g., \"LAST_NAME\")\n    SortDirection sortDirection(); // ASC or DESC\n}\n```\n\n**`SortPropertyProvider`** - Maps sort enum constants to JPA properties\n```java\npublic interface SortPropertyProvider {\n    String getSortProperty();  // Returns JPA property path\n}\n```\n\n**`PageDto`** - Response containing page data and metadata\n```java\npublic record PageDto\u003cT\u003e(\n    List\u003cT\u003e data,              // Page content\n    PaginationDto pagination   // Metadata (pageIndex, pageSize, totalPages, etc.)\n) {}\n```\n\n#### Example Sort Enum\n\n```java\npublic enum ProductSort implements SortPropertyProvider {\n    NAME(\"name\"),\n    PRICE(\"price\"),\n    CREATED_DATE(\"createdDate\"),\n    CATEGORY_NAME(\"category.name\");  // Nested property\n\n    private final String sortProperty;\n\n    ProductSort(String sortProperty) {\n        this.sortProperty = sortProperty;\n    }\n\n    @Override\n    public String getSortProperty() {\n        return sortProperty;\n    }\n}\n```\n\n### Search\n\nAnnotation-driven search functionality using JPA Criteria API.\n\n#### `@Searchable` Annotation\n\nMark fields that should be included in search queries:\n\n```java\n@Entity\npublic class Article extends BaseEntity {\n    @Searchable\n    private String title;\n\n    @Searchable\n    private String summary;\n\n    private String content;  // Not searchable\n\n    // ...\n}\n```\n\n#### `Searchables` Utility\n\nAutomatically builds case-insensitive LIKE predicates for all `@Searchable` String fields:\n\n```java\n// Automatically searches across title and summary fields\nSpecification\u003cArticle\u003e spec = (root, query, cb) -\u003e\n    Searchables.createSearchPredicate(\"spring boot\", root, cb);\n\nPage\u003cArticle\u003e results = articleRepository.findAll(spec, pageable);\n```\n\n**Features:**\n- Case-insensitive LIKE queries\n- Automatic SQL wildcard escaping (`%`, `_`, `\\`)\n- OR logic across all searchable fields\n- Null-safe (returns all results when search term is null/blank)\n\n### Auditing\n\n`AuditableEntity` extends `BaseEntity` with automatic tracking of creation and modification metadata.\n\n#### Fields\n\n- `createdDate` - When the entity was created\n- `modifiedDate` - When the entity was last modified\n- `createdBy` - Username/identifier who created the entity\n- `modifiedBy` - Username/identifier who last modified the entity\n\n#### Usage\n\n```java\n@Entity\npublic class Order extends AuditableEntity {\n    private BigDecimal total;\n\n    protected Order() {}\n\n    public Order(BigDecimal total) {\n        this.total = total;\n    }\n}\n```\n\n#### Spring Configuration\n\nEnable auditing and provide user information:\n\n```java\n@Configuration\n@EnableJpaAuditing\npublic class AuditConfig {\n\n    @Bean\n    public AuditorAware\u003cString\u003e auditorProvider() {\n        return () -\u003e Optional.ofNullable(SecurityContextHolder.getContext())\n                .map(SecurityContext::getAuthentication)\n                .filter(Authentication::isAuthenticated)\n                .map(Authentication::getName);\n    }\n}\n```\n\n## Spring Integration\n\n### REST Controller Integration\n\n#### Using `PageParams`\n\n`PageParams` is a record that implements `PageSpec` and automatically binds Spring Web query parameters:\n\n```java\n@RestController\n@RequestMapping(\"/api/products\")\npublic class ProductController {\n\n    @Autowired\n    private ProductService productService;\n\n    @GetMapping\n    public PageDto\u003cProductDto\u003e getProducts(PageParams pageParams) {\n        // Spring automatically binds these query parameters:\n        // - pageIndex\n        // - pageSize\n        // - sortBy\n        // - sortDirection\n        return productService.findAll(pageParams);\n    }\n}\n```\n\n**All parameters are optional:**\n```\nGET /api/products                                     # Uses defaults\nGET /api/products?pageIndex=0\u0026pageSize=10            # Pagination only\nGET /api/products?sortBy=PRICE\u0026sortDirection=DESC    # Sorting only\nGET /api/products?pageIndex=1\u0026pageSize=25\u0026sortBy=NAME\u0026sortDirection=ASC  # Full\n```\n\n#### Custom Parameter Names\n\nIf you need different URL parameter names:\n\n```java\n@GetMapping\npublic PageDto\u003cProductDto\u003e getProducts(\n        @RequestParam(required = false) Integer page,\n        @RequestParam(required = false) Integer size,\n        @RequestParam(required = false) String sortBy,\n        @RequestParam(required = false) SortDirection sortDirection) {\n\n    PageParams params = new PageParams(page, size, sortBy, sortDirection);\n    return productService.findAll(params);\n}\n\n// URL: /api/products?page=0\u0026size=10\u0026sortBy=NAME\u0026sortDirection=ASC\n```\n\n#### Converting with `Pageables` and `Pages`\n\n**`Pageables`** - Converts `PageSpec` to Spring's `Pageable`:\n\n```java\n@Service\npublic class ProductService {\n\n    @Autowired\n    private ProductRepository productRepository;\n\n    public PageDto\u003cProductDto\u003e findAll(PageSpec pageSpec) {\n        // Automatically resolves sort field string to ProductSort enum\n        Pageable pageable = Pageables.pageableOf(pageSpec, ProductSort.class);\n\n        // Or with custom default page size\n        // Pageable pageable = Pageables.pageableOf(pageSpec, ProductSort.class, 50);\n\n        Page\u003cProduct\u003e page = productRepository.findAll(pageable);\n        return Pages.pageDtoOf(page.map(ProductDto::fromEntity));\n    }\n}\n```\n\n**Default Values:**\n- Page index: 0 (first page)\n- Page size: 20\n- Sort: unsorted\n- Sort direction: ASC\n\n**`Pages`** - Converts Spring's `Page` to framework-agnostic `PageDto`:\n\n```java\nPage\u003cProduct\u003e springPage = productRepository.findAll(pageable);\nPageDto\u003cProductDto\u003e pageDto = Pages.pageDtoOf(\n    springPage.map(product -\u003e new ProductDto(product))\n);\n```\n\n### Searchable Repository\n\n`SearchableRepository` combines `JpaRepository` with built-in search support:\n\n```java\n@Repository\npublic interface ProductRepository extends SearchableRepository\u003cProduct, UUID\u003e {\n    // Inherits search() method automatically\n}\n```\n\n**Usage in Service:**\n\n```java\n@Service\npublic class ProductService {\n\n    @Autowired\n    private ProductRepository productRepository;\n\n    public PageDto\u003cProductDto\u003e search(String query, PageSpec pageSpec) {\n        Pageable pageable = Pageables.pageableOf(pageSpec, ProductSort.class);\n        Page\u003cProduct\u003e page = productRepository.search(query, pageable);\n        return Pages.pageDtoOf(page.map(ProductDto::fromEntity));\n    }\n}\n```\n\n**Controller:**\n\n```java\n@RestController\n@RequestMapping(\"/api/products\")\npublic class ProductController {\n\n    @Autowired\n    private ProductService productService;\n\n    @GetMapping\n    public PageDto\u003cProductDto\u003e search(\n            @RequestParam(required = false) String query,\n            PageParams pageParams) {\n        return productService.search(query, pageParams);\n    }\n}\n\n// Example: GET /api/products?query=laptop\u0026pageIndex=0\u0026pageSize=20\u0026sortBy=PRICE\u0026sortDirection=ASC\n```\n\n## Error Handling\n\n### Invalid Sort Fields\n\nWhen an invalid sort field is provided, `Pageables.pageableOf()` throws `UnknownSortByValueException`. This typically happens when a user provides a sort field name that doesn't match any of your defined enum constants.\n\n**Service Layer Handling:**\n\n```java\n@Service\npublic class ProductService {\n\n    @Autowired\n    private ProductRepository productRepository;\n\n    public PageDto\u003cProductDto\u003e findAll(PageSpec pageSpec) {\n        try {\n            Pageable pageable = Pageables.pageableOf(pageSpec, ProductSort.class);\n            Page\u003cProduct\u003e page = productRepository.findAll(pageable);\n            return Pages.pageDtoOf(page.map(ProductDto::fromEntity));\n        } catch (UnknownSortByValueException e) {\n            // e.getProvidedValue() - the invalid value the user sent\n            // e.getExpectedValues() - array of valid enum constant names\n            throw new BadRequestException(\"Invalid sort field: \" + e.getProvidedValue());\n        }\n    }\n}\n```\n\n**Spring Boot Controller Advice:**\n\nMap the exception to HTTP 400 Bad Request using RFC 7807 `ProblemDetail`:\n\n```java\n@RestControllerAdvice\npublic class UnknownSortByValueExceptionAdvice {\n\n    @ExceptionHandler(UnknownSortByValueException.class)\n    public ProblemDetail handleUnknownSortByValue(UnknownSortByValueException e) {\n        return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, e.getMessage());\n    }\n}\n```\n\n**Optional: Add custom properties for debugging:**\n\n```java\n@RestControllerAdvice\npublic class UnknownSortByValueExceptionAdvice {\n\n    @ExceptionHandler(UnknownSortByValueException.class)\n    public ProblemDetail handleUnknownSortByValue(UnknownSortByValueException e) {\n        ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(\n            HttpStatus.BAD_REQUEST,\n            e.getMessage()\n        );\n        problemDetail.setProperty(\"providedValue\", e.getProvidedValue());\n        problemDetail.setProperty(\"expectedValues\", e.getExpectedValues());\n        return problemDetail;\n    }\n}\n```\n\n**Example Error Response:**\n\nWhen a user requests `GET /api/products?sortBy=INVALID_FIELD`:\n\n```json\n{\n  \"type\": \"about:blank\",\n  \"title\": \"Bad Request\",\n  \"status\": 400,\n  \"detail\": \"Unknown sort by value \\\"INVALID_FIELD\\\", expecting one of NAME, PRICE, CREATED_DATE, CATEGORY_NAME.\"\n}\n```\n\n**With optional properties:**\n\n```json\n{\n  \"type\": \"about:blank\",\n  \"title\": \"Bad Request\",\n  \"status\": 400,\n  \"detail\": \"Unknown sort by value \\\"INVALID_FIELD\\\", expecting one of NAME, PRICE, CREATED_DATE, CATEGORY_NAME.\",\n  \"providedValue\": \"INVALID_FIELD\",\n  \"expectedValues\": [\"NAME\", \"PRICE\", \"CREATED_DATE\", \"CATEGORY_NAME\"]\n}\n```\n\n**Note:** Sort field matching is case-sensitive. If your enum constant is `LAST_NAME`, the client must send `sortBy=LAST_NAME` (not `last_name` or `lastName`).\n\n## Security Considerations\n\n### Search Term Length Validation\n\nFor production applications, validate search term length at the controller level to prevent excessively long LIKE patterns from impacting database performance:\n\n```java\n@RestController\n@RequestMapping(\"/api/products\")\npublic class ProductController {\n\n    @Autowired\n    private ProductService productService;\n\n    @GetMapping\n    public PageDto\u003cProductDto\u003e search(\n            @RequestParam(required = false)\n            @Size(max = 200, message = \"Search term must not exceed 200 characters\")\n            String query,\n            PageParams pageParams) {\n        return productService.search(query, pageParams);\n    }\n}\n```\n\nSpring Boot will automatically return HTTP 400 Bad Request when the validation fails:\n\n```json\n{\n  \"type\": \"about:blank\",\n  \"title\": \"Bad Request\",\n  \"status\": 400,\n  \"detail\": \"Invalid request content.\",\n  \"instance\": \"/api/products\"\n}\n```\n\n**Recommended Limits:**\n- **General search**: 100-200 characters (covers most legitimate search queries)\n- **Long-form content search**: 500 characters (for searching articles, descriptions, etc.)\n- **Never unlimited**: Always enforce some reasonable maximum\n\n### Page Size Limits\n\nThe library automatically clamps page sizes to `Pageables.MAX_PAGE_SIZE` (1000) to prevent memory exhaustion attacks. However, you may want to enforce stricter limits at the service or controller level for your specific use case:\n\n```java\n@GetMapping\npublic PageDto\u003cProductDto\u003e getProducts(\n        @RequestParam(required = false)\n        @Min(1)\n        @Max(100)\n        Integer pageSize,\n        PageParams pageParams) {\n    // Custom validation: max 100 items per page for this endpoint\n    return productService.findAll(pageParams);\n}\n```\n\n## Performance Optimization\n\n### Query Plan Caching\n\nThe library is already optimized for query plan caching with minimal configuration needed. Hibernate automatically uses bind parameters for String literals (the default `AUTO` mode), which means search queries are already benefiting from query plan reuse.\n\n**What's Already Optimized:**\n\nString literals in search patterns are automatically bound as parameters:\n```sql\n-- All searches use the same query plan (automatic with Hibernate's AUTO mode)\nWHERE LOWER(firstName) LIKE ?1 OR LOWER(lastName) LIKE ?2\n-- Parameters: ?1='%john%', ?2='%john%' (or '%jane%', etc.)\n```\n\nThe library also caches searchable field metadata (via reflection) internally, so field discovery only happens once per entity type.\n\n**Optional: Full BIND Mode (Future-Proofing)**\n\nIf you plan to extend the library with numeric or date field searching, you can configure Hibernate to use bind parameters for **all** literal types:\n\n```properties\n# Use bind parameters for all literals, including numbers (default: AUTO uses bind for Strings only)\nspring.jpa.properties.hibernate.criteria.literal_handling_mode=BIND\n\n# Optional: Adjust query plan cache size (default: 2048)\nspring.jpa.properties.hibernate.query.plan_cache_max_size=2048\n```\n\n**Hibernate Literal Handling Modes:**\n- `AUTO` (default): Uses bind parameters for Strings, inlines numeric values\n- `BIND`: Uses bind parameters for all literal types (Strings, numbers, dates)\n- `INLINE`: Inlines all literals (not recommended - poor caching, potential SQL injection risk)\n\n**Performance Impact:**\n- String-based `@Searchable` searches already benefit from query plan caching (no configuration needed)\n- `BIND` mode provides benefits if you add numeric or date field searching in the future\n- High-traffic search endpoints will see query compilation overhead reduced by 500%+ compared to inline literals\n\n## Complete Example\n\n### Entity\n\n```java\n@Entity\npublic class Book extends AuditableEntity {\n\n    @Searchable\n    private String title;\n\n    @Searchable\n    private String author;\n\n    private String isbn;\n\n    @Searchable\n    private String publisher;\n\n    private BigDecimal price;\n\n    protected Book() {}\n\n    public Book(String title, String author, String isbn,\n                String publisher, BigDecimal price) {\n        this.title = title;\n        this.author = author;\n        this.isbn = isbn;\n        this.publisher = publisher;\n        this.price = price;\n    }\n\n    // Getters and setters...\n}\n```\n\n### Sort Enum\n\n```java\npublic enum BookSort implements SortPropertyProvider {\n    TITLE(\"title\"),\n    AUTHOR(\"author\"),\n    PUBLISHER(\"publisher\"),\n    PRICE(\"price\"),\n    CREATED(\"createdDate\");\n\n    private final String sortProperty;\n\n    BookSort(String sortProperty) {\n        this.sortProperty = sortProperty;\n    }\n\n    @Override\n    public String getSortProperty() {\n        return sortProperty;\n    }\n}\n```\n\n### Repository\n\n```java\n@Repository\npublic interface BookRepository extends SearchableRepository\u003cBook, UUID\u003e {\n}\n```\n\n### Service\n\n```java\n@Service\npublic class BookService {\n\n    @Autowired\n    private BookRepository bookRepository;\n\n    public PageDto\u003cBookDto\u003e search(String query, PageSpec pageSpec) {\n        Pageable pageable = Pageables.pageableOf(pageSpec, BookSort.class);\n        Page\u003cBook\u003e page = bookRepository.search(query, pageable);\n        return Pages.pageDtoOf(page.map(BookDto::fromEntity));\n    }\n}\n```\n\n### Controller\n\n```java\n@RestController\n@RequestMapping(\"/api/books\")\npublic class BookController {\n\n    @Autowired\n    private BookService bookService;\n\n    @GetMapping\n    public PageDto\u003cBookDto\u003e search(\n            @RequestParam(required = false) String query,\n            PageParams pageParams) {\n        return bookService.search(query, pageParams);\n    }\n}\n```\n\n### Example Requests\n\n```bash\n# Get first page of all books, sorted by title\nGET /api/books?pageIndex=0\u0026pageSize=20\u0026sortBy=TITLE\u0026sortDirection=ASC\n\n# Search for \"spring\" in title, author, or publisher\nGET /api/books?query=spring\u0026pageIndex=0\u0026pageSize=10\n\n# Get page 2, sorted by price descending\nGET /api/books?pageIndex=1\u0026pageSize=25\u0026sortBy=PRICE\u0026sortDirection=DESC\n\n# Search with custom page size\nGET /api/books?query=java\u0026pageSize=50\n```\n\n### Example Response\n\n```json\n{\n  \"data\": [\n    {\n      \"id\": \"01234567-89ab-cdef-0123-456789abcdef\",\n      \"title\": \"Spring in Action\",\n      \"author\": \"Craig Walls\",\n      \"isbn\": \"978-1617294945\",\n      \"publisher\": \"Manning\",\n      \"price\": 44.99,\n      \"createdDate\": \"2024-01-15T10:30:00Z\",\n      \"modifiedDate\": \"2024-01-15T10:30:00Z\"\n    }\n  ],\n  \"pagination\": {\n    \"pageIndex\": 0,\n    \"pageSize\": 20,\n    \"totalElementCount\": 127,\n    \"totalPageCount\": 7,\n    \"hasNext\": true,\n    \"hasPrevious\": false\n  }\n}\n```\n\n## Requirements\n\n- **Java**: 21 or higher\n- **Jakarta Persistence API**: 3.1.0\n- **Spring Framework** (optional): 6.2.2 or higher\n- **Spring Data JPA** (optional): 2024.1.2 or higher\n\n## Database Compatibility\n\nThis library is thoroughly tested against multiple database platforms using [Testcontainers](https://testcontainers.com/) to ensure compatibility across different SQL dialects and implementations.\n\n**Tested Databases:**\n- ✅ **PostgreSQL 17.6** - Open source, standards-compliant\n- ✅ **MySQL 9.2** - Most widely deployed open source database\n- ✅ **MariaDB 11.6** - MySQL fork with enhancements\n- ✅ **CockroachDB v25.3** - Distributed SQL, PostgreSQL-compatible\n- ✅ **Oracle Database Free 23** - Enterprise standard\n- ✅ **H2** - Embedded database for testing\n\n**Test Coverage:**\n- 75+ integration tests across 5 database platforms\n- Comprehensive validation of pagination, sorting, and search functionality\n- SQL wildcard escaping and edge case handling\n- Multi-field search across different SQL dialects\n\nAll integration tests run automatically in CI/CD to ensure consistent behavior across all supported databases.\n\n## License\n\nThis project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjwcarman%2Fjpa-utils","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjwcarman%2Fjpa-utils","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjwcarman%2Fjpa-utils/lists"}