Skip to content

Full control with Crud

Crud is the most flexible CRUD base class in Mateu. It lets you define a separate type for every screen — filters, grid rows, read-only detail, edit form, and creation form — while the framework still handles all routing and navigation automatically.

Use it when AutoCrud<T> or FilteredAutoCrud<Filters,T> are not enough because different screens need genuinely different models.


Crud is a Listing that declares every capability — search box, filter bar, navigation, editing, creation, and deletion — with an explicit type for each screen:

public abstract class Crud<View, Editor, CreationForm, Filters, Row, IdType>
implements Listing<Row>,
Searchable,
Filterable<Filters>,
Navigable<View, IdType>,
Editable<Editor, IdType>,
Creatable<CreationForm, IdType>,
Deletable<IdType>
TypeMeaning
ViewThe object rendered in the read-only detail screen
EditorThe form shown in the edit screen — any class; no interface required
CreationFormThe form shown in the create screen — any class; no interface required
FiltersThe filter bar DTO
RowThe DTO shown as a grid row in the listing
IdTypeThe type of the entity identifier (usually String)

The editor and creation form are plain view models: Mateu renders their fields and hydrates them back from the submitted state. Persistence is the orchestrator’s job — save(httpRequest) and create(httpRequest) receive the submitted form state and decide how to store it.


RouteScreen
/your-routeListing with filter bar
/your-route/:idRead-only detail (View)
/your-route/:id/editEdit form (Editor)
/your-route/newCreate form (CreationForm)

The whole CRUD lifecycle lives on the orchestrator — there is no separate data-layer interface. Inject your services (query service, use cases, repository) into the orchestrator and call them from these methods:

MethodReturn typePurpose
search(request, httpRequest)ListingData<Row>Executes the search — request is a SearchRequest carrying searchText(), filters(), criteria(), and pageable()
view(id, httpRequest)ViewReturns the View object for the read-only detail screen
edit(id, httpRequest)EditorReturns the Editor object for the edit screen
creationForm(httpRequest)CreationFormReturns a blank (or pre-populated) CreationForm
save(httpRequest)IdTypePersists the edit form state and returns the record id (used to navigate back to the detail view)
create(httpRequest)IdTypePersists the creation form state and returns the new record’s id
deleteAllById(ids, httpRequest)voidDeletes the selected rows

getIdFieldForRow() has a default (the @PrimaryKey/id field of Row); override it only when the identifier lives in a differently-named field. toId(String) converts the route id into IdType automatically for strings, well-known scalars (Integer, Long, UUID, enums, …) and single-String-constructor types — override it for anything else.


public record ProductFilters(
String name,
ProductStatus status
) {}
public record ProductRow(
@PrimaryKey String id,
String name,
BigDecimal price,
ProductStatus status
) implements Identifiable {}
public record ProductView(
String id,
String name,
String description,
BigDecimal price,
ProductStatus status
) {}
public class ProductEditor {
public String id;
@NotEmpty
public String name;
public String description;
@NotNull
public BigDecimal price;
public ProductStatus status;
}
public class ProductCreationForm {
@NotEmpty
public String name;
@NotNull
public BigDecimal price;
}

Both are plain view models — no interface to implement. Persistence happens in the orchestrator’s save()/create() below.

@Service
@UI("/products")
public class ProductOrchestrator
extends Crud<ProductView, ProductEditor, ProductCreationForm, ProductFilters, ProductRow, String> {
private final ProductService service;
public ProductOrchestrator(ProductService service) {
this.service = service;
}
@Override
public ListingData<ProductRow> search(SearchRequest request, HttpRequest httpRequest) {
return service.search(request.searchText(), filters(request), request.pageable());
}
@Override
public ProductView view(String id, HttpRequest httpRequest) {
return service.findView(id);
}
@Override
public ProductEditor edit(String id, HttpRequest httpRequest) {
return service.findEditor(id);
}
@Override
public ProductCreationForm creationForm(HttpRequest httpRequest) {
return new ProductCreationForm();
}
@Override
public void deleteAllById(List<String> ids, HttpRequest httpRequest) {
service.deleteAll(ids);
}
@Override
public String save(HttpRequest httpRequest) {
var editor = httpRequest.getComponentState(ProductEditor.class);
service.update(editor.id, editor.name, editor.description, editor.price, editor.status);
return editor.id;
}
@Override
public String create(HttpRequest httpRequest) {
var form = httpRequest.getComponentState(ProductCreationForm.class);
return service.create(form.name, form.price);
}
}

All capability annotations available on AutoCrud<T> also work on Crud:

AnnotationEffect
@ReadOnlyHides New, Edit, and Delete — shorthand for @NotCreatable @NotEditable @NotDeletable
@NotCreatableHides the New button
@NotEditableHides the Edit button in the detail view
@NotDeletableHides the Delete button
@NotNavigableHides the View button column — rows are not clickable

MethodDefaultOverride to…
readOnly()falsemake the whole orchestrator read-only programmatically
searchable()truehide the search bar
selectionEnabled()truedisable row selection
title()class nameoverride the page title

ClassFilter typeRow typeWriteSeparate forms
AutoCrud<T>TT✓ (or @ReadOnly)
FilteredAutoCrud<Filters,T>FiltersT✓ (or @ReadOnly)
Crud<V,E,C,F,R,Id>FR✓ (or @ReadOnly)

Move to Crud only when the view, editor, or creation forms must differ from each other or from the row model. The simpler variants cover most real-world cases. And when you don’t want the whole pack, don’t extend Crud at all — implement Listing<Row> plus just the capability interfaces you need.