CrudRepository
CrudRepository<T> is the data-access contract consumed by AutoCrud. Implement it to back an automatic CRUD UI with your own storage — in-memory map, JPA repository, REST client, or anything else.
The type parameter T must implement Identifiable, which requires a single method:
public interface Identifiable { String id();}Interface
Section titled “Interface”public interface CrudRepository<T extends Identifiable> { Optional<T> findById(String id); String save(T entity); List<T> findAll(); void deleteAllById(List<String> selectedIds);
// Search + filter + sort + paginate. Ships with a default in-memory // implementation (over findAll()); override it to push everything to the DB. Page<T> find(String searchText, T filters, Pageable pageable);}Methods
Section titled “Methods”| Method | Description |
|---|---|
findById(id) | Return the entity with the given string ID, or Optional.empty() |
save(entity) | Persist the entity and return its ID |
findAll() | Return all entities for the listing view |
deleteAllById(ids) | Delete every entity whose ID appears in the list |
find(searchText, filters, pageable) | Search, filter, sort and paginate in one call, returning a Page<T>. Has a default implementation (in-memory over findAll()), so you only override it for DB-side paging. |
Full example
Section titled “Full example”The snippet below mirrors the pattern used in the Items Catalog e2e test app. It shows the entity, repository, and orchestrator in one place.
// 1. Entity — must implement Identifiable@Getter @Setter @NoArgsConstructor @AllArgsConstructorclass Item implements Identifiable {
String id; String name; double price;
@Override public String id() { return id; }
@Override public String toString() { return name != null ? "Item " + name : "New item"; }}
// 2. Repository — holds your storage logicclass ItemRepository implements CrudRepository<Item> {
private static final Map<String, Item> db = new HashMap<>(Map.of( "1", new Item("1", "Widget A", 9.99), "2", new Item("2", "Widget B", 19.99) ));
@Override public Optional<Item> findById(String id) { return db.containsKey(id) ? Optional.of(db.get(id)) : Optional.empty(); }
@Override public String save(Item entity) { db.put(entity.getId(), entity); return entity.getId(); }
@Override public List<Item> findAll() { return db.values().stream().toList(); }
@Override public void deleteAllById(List<String> selectedIds) { selectedIds.forEach(db::remove); }}
// 3. Orchestrator — the UI endpoint@UI("/items")public class ItemsCatalog extends AutoCrud<Item> {
@Override public CrudRepository<Item> repository() { return new ItemRepository(); }}Navigating to /items renders a full list-create-edit-delete UI with no additional configuration.
Spring Data JPA integration
Section titled “Spring Data JPA integration”CrudRepository is a plain Java interface, not a Spring Data one. The most common pattern in Spring Boot applications is to wrap a Spring Data JpaRepository in a thin adapter:
// Spring Data JPA repositorypublic interface ProductJpaRepository extends JpaRepository<Product, String> { List<Product> findByNameContainingIgnoreCase(String name);}// Mateu CrudRepository — wraps the JPA repo@Servicepublic class ProductRepository implements CrudRepository<Product> {
private final ProductJpaRepository jpa;
public ProductRepository(ProductJpaRepository jpa) { this.jpa = jpa; }
@Override public Optional<Product> findById(String id) { return jpa.findById(id); }
@Override public String save(Product entity) { return jpa.save(entity).id(); }
@Override public List<Product> findAll() { return jpa.findAll(); }
@Override public void deleteAllById(List<String> selectedIds) { jpa.deleteAllById(selectedIds); }}The adapter decouples Mateu’s API from Spring Data so the rest of your code is not tied to the framework.
The find method
Section titled “The find method”find(searchText, filters, pageable) is the single entry point AutoCrud uses to populate the listing view. It returns a Page<T>:
public record Page<T>( String searchSignature, int pageSize, int pageNumber, long totalElements, List<T> content) {}The Page already carries totalElements, so no separate count method is needed — a DB-backed implementation is expected to run both the count and the page query inside find and return them together.
Default (in-memory) behaviour
Section titled “Default (in-memory) behaviour”You get a working find for free. The default implementation:
- loads
findAll(), - keeps rows whose text contains every whitespace-separated word of
searchText, case-insensitively and in any order — usingSearchable.searchableText()when the entity implementsSearchable, otherwisetoString(), - applies the field-level filters (see below),
- sorts by
pageable.sort()(eachSortfield is read reflectively via getter / record accessor / field), and - slices out the requested page.
This is fine for small/medium datasets; everything happens in memory.
How the default decides a filter is set. The filters object is hydrated from the component
state, so fields the user never touched keep their initializers (or primitive zeros/falses). To
tell them apart, the default compares each basic field (strings, numbers, booleans, chars, enums)
against a freshly-constructed instance of the filters class — the no-arg constructor, or for
records the canonical constructor fed with null/zero/false. Only fields whose value differs from
that baseline filter the rows: strings by case-insensitive containment, everything else by
equality. The flip side is that filtering by a field’s default value (e.g. an initializer like
status = AVAILABLE) is invisible to the default — override find if you need that.
Range and multi-select criteria
Section titled “Range and multi-select criteria”An entity-shaped example object can’t say “price between 100 and 150” or “status in (A, B)”, so
those conditions travel separately as FilterCriterion records (io.mateu.uidl.data):
public record FilterCriterion(String field, FilterOperator operator, List<Object> values) {}// operators: between, gte, lte, in — values already coerced to the field's typeThey come from the smart search bar’s range widgets (temporal fields by default, numeric fields
annotated with @RangeFilter) and enum multi-selects, and reach the repository through a second
overload:
default Page<T> find(String searchText, T filters, List<FilterCriterion> criteria, Pageable pageable)The framework calls the 4-arg overload only when such criteria exist, so a repository that
only overrides the 3-arg find keeps working unchanged for plain searches — but a range/multi
search on it falls back to the in-memory default over findAll(). Override the 4-arg overload too
when you want ranges and value lists resolved in the database (translate between/gte/lte to
comparisons and in to an IN clause).
Overriding for database-side paging
Section titled “Overriding for database-side paging”For large tables, override find so the search, filtering, sorting and pagination all run in the database. Because Page carries totalElements, you run a count query plus a paged query:
@Servicepublic class ProductRepository implements CrudRepository<Product> {
private final ProductJpaRepository jpa;
public ProductRepository(ProductJpaRepository jpa) { this.jpa = jpa; }
// findById / save / findAll / deleteAllById as usual …
@Override public Page<Product> find(String searchText, Product filters, Pageable pageable) { var springPageable = org.springframework.data.domain.PageRequest.of( pageable.page(), pageable.size(), toSpringSort(pageable.sort())); var result = jpa.findByNameContainingIgnoreCase( searchText != null ? searchText : "", springPageable); return new Page<>( searchText, result.getSize(), result.getNumber(), result.getTotalElements(), result.getContent()); }}AutoCrud delegates to find automatically, so a repository that overrides it gets DB-side paging with no extra wiring. If you need access to the HttpRequest (e.g. tenant scoping), override fetchRows(searchText, filters, pageable, httpRequest) on the AutoCrud subclass instead — it wraps repository().find(...) by default.
CompositionCrudRepository
Section titled “CompositionCrudRepository”CompositionCrudRepository extends CrudRepository for child entities that belong to a parent (a foreign-key relationship expressed with @Composition).
public interface CompositionCrudRepository<EntityType extends Named, ParentIdType> extends CrudRepository<EntityType> {
ListingData<EntityType> search( String searchText, EntityType filters, ParentIdType parentId, Pageable pageable);}The type constraint on EntityType tightens to Named, which extends Identifiable and adds:
public interface Named extends Identifiable { String name();}Additional method
Section titled “Additional method”| Method | Description |
|---|---|
search(searchText, filters, parentId, pageable) | Return a paginated list of child entities scoped to the given parentId |
Use CompositionCrudRepository when an embedded child grid inside a parent form must fetch its rows from the server filtered by the parent’s ID. Annotate the parent field with @Composition to trigger this behaviour.
- CrudAdapter — the lower-level interface for full control over each operation
- AutoCrud — the orchestrator that consumes this repository