javajpamicronautmicronaut-data

Micronaut Data DTO projection with properties from joined entities


I use Micronaut Data with JPA and have two entities. The first one is Recipe:

@Entity
public class Recipe {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String name;

    @ManyToOne
    private Category category;

    @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
    private Set<Step> steps;

// + other fields, getters and setters
}

The second one is ParseError which refers to Recipe:

@Entity
@Table(name = "parse_error")
public class ParseError implements Serializable {
    @Id
    @ManyToOne(fetch = FetchType.LAZY)
    private Recipe recipe;

    @Id
    @Enumerated(EnumType.ORDINAL)
    @Column(name = "problem_area")
    private ProblemArea problemArea;

    private String message;

// + other fields, getters and setters
}

Now I would like to provide DTO in API with ParseError properties but not with whole Recipe entity because it contains ManyToOne and OneToMany relations which are not needed in this case. So I created projection DTO for that:

@Introspected
public class ParseErrorDto {
    private Integer recipeId;

    private String recipeName;

    private ParseError.ProblemArea problemArea;

    private String message;

// + getters and setters
}

And added listAll() method into ParseErrorRepository:

@Repository
public interface ParseErrorRepository extends CrudRepository<ParseError, Integer> {
    List<ParseErrorDto> listAll();
}

But it seems that Micronaut Data is not able to project properties from nested entities or I missed something in the DTO or the repository method:

ParseErrorRepository.java:22: error: Unable to implement Repository method: ParseErrorRepository.listAll(). Property recipeId is not present in entity: ParseError

I also tried to create RecipeDto:

@Introspected
public class RecipeDto {
    private Integer id;

    private String name;

    // + getters and setters
}

And updated ParseErrorDto accordingly:

@Introspected
public class ParseErrorDto {
    private RecipeDto recipe;

    private ParseError.ProblemArea problemArea;

    private String message;

    // + getters and setters
}

Again no success:

ParseErrorRepository.java:22: error: Unable to implement Repository method: ParseErrorRepository.listAll(). Property [recipe] of type [RecipeDto] is not compatible with equivalent property declared in entity: ParseError

Is Micronaut Data able to handle this use case by DTO projection? If not then is there another way how can I solve it in Micronaut Data?


Solution

  • Now (in latest version 1.0.0.M1) it is not possible. So I created feature request issue for that: https://github.com/micronaut-projects/micronaut-data/issues/184

    Current workaround is to map entity bean into DTO bean in Java stream or reactive stream for example and do the properties mapping manually or by Mapstruct.


    Update: Here is an answer to question from comments with an example how to do the workaround using Mapstruct:

    Add Mapstruct dependency into build.gradle:

    implementation "org.mapstruct:mapstruct:$mapstructVersion"
    annotationProcessor "org.mapstruct:mapstruct-processor:$mapstructVersion"
    testAnnotationProcessor "org.mapstruct:mapstruct-processor:$mapstructVersion"
    

    Define mapper:

    import org.mapstruct.Mapper;
    
    @Mapper(
        componentModel = "jsr330"
    )
    public interface ParseErrorMapper {
        ParseErrorDto entityToDto(@NotNull ParseError parseError);
    
        EntityReference recipeToDto(@NotNull Recipe recipe);
    }
    

    And here is a usage of that mapper in the controller:

    @Controller("/parse-error")
    public class ParseErrorController {
        private final ParseErrorRepository repository;
        private final ParseErrorMapper mapper;
    
        public ParseErrorController(ParseErrorRepository repository, ParseErrorMapper mapper) {
            this.repository = repository;
            this.mapper = mapper;
        }
    
        @Get("all")
        @Transactional
        public Page<ParseErrorDto> getAll(final Pageable pageable) {
            return repository.findAll(pageable).map(mapper::entityToDto);
        }
    }
    

    Update (September 2023): feature implemented by https://github.com/micronaut-projects/micronaut-data/pull/2495