- Adds generic support for using MongoDB repositorites directly with Hilla's Auto CRUD, Auto Grid, Auto Form components
- Inspired by the approach in the blog posts Hilla AutoGrid with MongoDB and Hilla AutoCrud with MongoDB
- Works with
String,Long, andObjectIdIds,- however you may hit issues with
ObjectId- see here
- however you may hit issues with
- Requires Java 24 with Lombok & JSpecify (trivial to backport / remove)
- feel free to copy / share / etc.
Last active
July 30, 2025 14:45
-
-
Save mands/2f5af1e27840bf88e66bf61943eee0f4 to your computer and use it in GitHub Desktop.
Hilla MongoDB AutoCrud Support
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package common.hilla; | |
| import module java.base; | |
| import lombok.extern.slf4j.Slf4j; | |
| import org.jspecify.annotations.NonNull; | |
| import org.jspecify.annotations.Nullable; | |
| import org.springframework.data.domain.Pageable; | |
| import org.springframework.data.mongodb.core.MongoTemplate; | |
| import org.springframework.data.mongodb.core.query.Criteria; | |
| import org.springframework.data.mongodb.core.query.Query; | |
| import com.vaadin.hilla.EndpointExposed; | |
| import com.vaadin.hilla.crud.CountService; | |
| import com.vaadin.hilla.crud.CrudService; | |
| import com.vaadin.hilla.crud.GetService; | |
| import com.vaadin.hilla.crud.filter.Filter; | |
| @Slf4j | |
| @EndpointExposed | |
| public class MongoCrudService<T, ID> implements CrudService<T, ID>, GetService<T, ID>, CountService { | |
| private final MongoTemplate mongoTemplate; | |
| private final Class<T> entityClass; | |
| private final String idField; | |
| public MongoCrudService(MongoTemplate mongoTemplate, Class<T> entityClass) { | |
| this(mongoTemplate, entityClass, "_id"); | |
| } | |
| public MongoCrudService(MongoTemplate mongoTemplate, Class<T> entityClass, String idField) { | |
| this.mongoTemplate = mongoTemplate; | |
| this.entityClass = entityClass; | |
| this.idField = idField; | |
| } | |
| // Public API methods | |
| @Override | |
| public List<@NonNull T> list(Pageable pageable, @Nullable Filter filter) { | |
| var query = buildQuery(filter); | |
| query.with(pageable); | |
| return mongoTemplate.find(query, entityClass); | |
| } | |
| @Override | |
| @Nullable | |
| public T save(T value) { | |
| return mongoTemplate.save(value); | |
| } | |
| @Override | |
| public void delete(ID id) { | |
| var query = new Query(Criteria.where(idField).is(id)); | |
| mongoTemplate.remove(query, entityClass); | |
| } | |
| // Additional methods | |
| @Override | |
| public long count(@Nullable Filter filter) { | |
| var query = buildQuery(filter); | |
| return mongoTemplate.count(query, entityClass); | |
| } | |
| @Override | |
| public boolean exists(ID id) { | |
| var query = new Query(Criteria.where(idField).is(id)); | |
| return mongoTemplate.exists(query, entityClass); | |
| } | |
| @Nullable | |
| public T findById(ID id) { | |
| return mongoTemplate.findById(id, entityClass); | |
| } | |
| @Override | |
| public Optional<@NonNull T> get(ID id) { | |
| return Optional.ofNullable(findById(id)); | |
| } | |
| /// build the query from the filter for the entity | |
| private Query buildQuery(@Nullable Filter filter) { | |
| var query = new Query(); | |
| if (filter != null) { | |
| var criteria = MongoFilterConverter.toCriteria(filter, entityClass); | |
| query.addCriteria(criteria); | |
| } | |
| return query; | |
| } | |
| // curently unused | |
| public record PageResponse<T>( | |
| List<T> content, | |
| long totalElements, | |
| int totalPages, | |
| int currentPage | |
| ) { | |
| } | |
| public PageResponse<T> listWithCount(Pageable pageable, @Nullable Filter filter) { | |
| var query = buildQuery(filter); | |
| var totalElements = mongoTemplate.count(query, entityClass); | |
| var totalPages = (int) Math.ceil((double) totalElements / pageable.getPageSize()); | |
| query.with(pageable); | |
| var content = mongoTemplate.find(query, entityClass); | |
| return new PageResponse<>(content, totalElements, totalPages, pageable.getPageNumber()); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package common.hilla; | |
| import module java.base; | |
| import lombok.extern.slf4j.Slf4j; | |
| import org.springframework.data.mongodb.core.query.Criteria; | |
| import com.vaadin.hilla.crud.filter.AndFilter; | |
| import com.vaadin.hilla.crud.filter.Filter; | |
| import com.vaadin.hilla.crud.filter.OrFilter; | |
| import com.vaadin.hilla.crud.filter.PropertyStringFilter; | |
| @Slf4j | |
| public final class MongoFilterConverter { | |
| private MongoFilterConverter() { | |
| } | |
| public static <T> Criteria toCriteria(Filter filter, Class<T> entityClass) { | |
| return switch (filter) { | |
| case AndFilter f when !f.getChildren().isEmpty() -> { | |
| var criteriaList = f.getChildren().stream() | |
| .map(f1 -> toCriteria(f1, entityClass)) | |
| .toArray(Criteria[]::new); | |
| yield new Criteria().andOperator(criteriaList); | |
| } | |
| case OrFilter f when !f.getChildren().isEmpty() -> { | |
| var criteriaList = f.getChildren().stream() | |
| .map(f1 -> toCriteria(f1, entityClass)) | |
| .toArray(Criteria[]::new); | |
| yield new Criteria().orOperator(criteriaList); | |
| } | |
| case PropertyStringFilter f -> buildPropertyCriteria(f, entityClass); | |
| // case null -> new Criteria(); | |
| default -> { | |
| log.warn("Unknown/empty filter type: {}, ignoring", filter.getClass()); | |
| yield new Criteria(); | |
| // throw new IllegalArgumentException("Unknown filter type: " + filter.getClass()); | |
| } | |
| }; | |
| } | |
| private static <T> Criteria buildPropertyCriteria(PropertyStringFilter filter, Class<T> entityClass) { | |
| var propertyPath = filter.getPropertyId(); | |
| var filterValue = filter.getFilterValue(); | |
| Object convertedValue = convertValue(filterValue, propertyPath, entityClass); | |
| var criteria = Criteria.where(propertyPath); | |
| return switch (filter.getMatcher()) { | |
| case EQUALS -> criteria.is(convertedValue); | |
| case CONTAINS -> switch (convertedValue) { | |
| case String s -> criteria.regex(".*" + s + ".*", "i"); | |
| default -> criteria.is(convertedValue); // Fallback for non-strings | |
| }; | |
| case LESS_THAN -> criteria.lt(convertedValue); | |
| case GREATER_THAN -> criteria.gt(convertedValue); | |
| }; | |
| } | |
| private static <T> Object convertValue(String value, String propertyPath, Class<T> entityClass) { | |
| // if (value == null) return null; | |
| try { | |
| var parts = propertyPath.split("\\."); | |
| Class<?> currentClass = entityClass; | |
| Field field = null; | |
| for (var part : parts) { | |
| field = currentClass.getDeclaredField(part); | |
| currentClass = field.getType(); | |
| } | |
| if (field == null) return value; | |
| return convertToType(value, field.getType()); | |
| } catch (Exception e) { | |
| return value; // Default to string | |
| } | |
| } | |
| private static Object convertToType(String value, Class<?> targetType) { | |
| return switch (targetType) { | |
| case Class<?> c when c == String.class -> value; | |
| case Class<?> c when c == Long.class || c == long.class -> Long.parseLong(value); | |
| case Class<?> c when c == Integer.class || c == int.class -> Integer.parseInt(value); | |
| case Class<?> c when c == Double.class || c == double.class -> Double.parseDouble(value); | |
| case Class<?> c when c == Float.class || c == float.class -> Float.parseFloat(value); | |
| case Class<?> c when c == Boolean.class || c == boolean.class -> Boolean.parseBoolean(value); | |
| case Class<?> c when c == Short.class || c == short.class -> Short.parseShort(value); | |
| case Class<?> c when c == Byte.class || c == byte.class -> Byte.parseByte(value); | |
| case Class<?> c when c == Character.class || c == char.class -> value.isEmpty() ? '\0' : value.charAt(0); | |
| case Class<?> c when c == LocalDate.class -> LocalDate.parse(value); | |
| case Class<?> c when c == LocalDateTime.class -> LocalDateTime.parse(value); | |
| case Class<?> c when c == Instant.class -> Instant.parse(value); | |
| case Class<?> c when c == UUID.class -> UUID.fromString(value); | |
| case Class<?> c when c == BigDecimal.class -> new BigDecimal(value); | |
| case Class<?> c when c.isEnum() -> convertEnum(value, c); | |
| default -> value; | |
| }; | |
| } | |
| @SuppressWarnings("unchecked") | |
| private static Object convertEnum(String value, Class<?> enumClass) { | |
| return Enum.valueOf((Class<? extends Enum>) enumClass, value); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment