Instructing Sping Data MongoDB to use correct mapping between ObjectId and its class - spring

I cannot retrieve the 2nd level nested objects in Spring Data MongoDB
I have nested collection in MongoDB to retrieve with Spring. Imagine this schema
#Data
#Builder
#Document(collection = "emitted")
#JsonInclude(JsonInclude.Include.NON_NULL)
public class Emitter{
#Id
private String id;
#Field("installation")
#DocumentReference(lazy = true)
private Installaton installation;
// other fields
#Data
#Builder
#Document(collection = "installation")
#JsonInclude(JsonInclude.Include.NON_NULL)
public class Installation {
#Id
private String id;
#Field("subject")
#DocumentReference(lazy = true)
private Subject subject;
// other fields
#Data
#Builder
#Document(collection = "subject")
#JsonInclude(JsonInclude.Include.NON_NULL)
public class Subject {
#Id
private String id;
// other fields
Plus, I have MapStruct to map nested object field to string, for the purpose of avoiding cyclic reference introducing the search by id of the collection:
#ObjectFactory
public <T> T map(#NonNull final String id, #TargetType Class<T> type) {
return mongoTemplate.findById(id, type);
}
Everything works at first level, but at nested level I have this error:
Caused by: org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [org.bson.types.ObjectId] to type [com.package.collections.Subject]
at org.springframework.core.convert.support.GenericConversionService.handleConverterNotFound(GenericConversionService.java:322)
at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:195)
at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:175)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.doConvert(MappingMongoConverter.java:1826)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.doConvert(MappingMongoConverter.java:1818)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.getPotentiallyConvertedSimpleRead(MappingMongoConverter.java:1337)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.getPotentiallyConvertedSimpleRead(MappingMongoConverter.java:1311)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter$DefaultConversionContext.convert(MappingMongoConverter.java:2371)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter$ConversionContext.convert(MappingMongoConverter.java:2174)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter$MongoDbPropertyValueProvider.getPropertyValue(MappingMongoConverter.java:1936)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.readProperties(MappingMongoConverter.java:638)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.populateProperties(MappingMongoConverter.java:549)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:527)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.readDocument(MappingMongoConverter.java:491)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:427)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:423)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:120)
at org.springframework.data.mongodb.core.MongoTemplate$ReadDocumentCallback.doWith(MongoTemplate.java:3326)
at org.springframework.data.mongodb.core.MongoTemplate.executeFindOneInternal(MongoTemplate.java:2940)
at org.springframework.data.mongodb.core.MongoTemplate.doFindOne(MongoTemplate.java:2618)
at org.springframework.data.mongodb.core.MongoTemplate.doFindOne(MongoTemplate.java:2588)
at org.springframework.data.mongodb.core.MongoTemplate.findById(MongoTemplate.java:922)
at com.package.myapp.services.mapper.ReferenceMapper.map(ReferenceMapper.java:26)
at com.package.myapp.services.mapper.InstallationMapperImpl.toEntity(InstallationMapperImpl.java:102)
When asking the conversion, the findById works correctly and retrieve the object and the nested one. It fails when the request is for 2nd level nested object, where the ObjectId is retrieved but cannot be converted and fails.

I'm answering myself because I found a solution suited for my problem.
I only needed the entity object with the id, so I wrote a converter:
public class ObjectIdToSubjectConverter implements Converter<ObjectId, Subject> {
#Override
public Subject convert(ObjectId source) {
return Subject.builder().id(source.toString()).build();
}
}
And register it as a bean:
#Configuration
public class MongoConfig {
#Bean
public MongoCustomConversions mongoCustomConversions() {
return new MongoCustomConversions(Collections.singletonList(new ObjectIdToSubjectConverter());
}
}

Related

How to exclude/disable #Entity Annotation for particular class

I want to disable #Entity Annotation for particular class.
Here is my sample code.
#Component
public class GenericDropDown{
private Integer id;
private String key;
private String value;
// Standard getter and setter
The above class is used for fetching data from multiple table for rendering different dropdown list from different tables.
How I can achieve this without #Entity Annotation
Here is my sample code.
#Component
public class GenericDropDown{
private Integer id;
private String key;
private String value;
// Standard getter and setter
#Repository
public class DropDownDao {
#Autowired
private EntityManager entityManager;
public Object runNativeQuery() {
#SuppressWarnings("unchecked")
List<Priority> o= entityManager.createNativeQuery("select Id,PRKEY,PRVALUE from Priority",Priority.class)
.getResultList();
return o;
}
}
**Error:**Unknown entity: com.min.test.Project.entity.Priority; nested exception is org.hibernate.MappingException: Unknown entity: com.min.test.Project.entity.Priority
You can select List of Objects array and map them yourself.
List<Object[]> o = entityManager.createNativeQuery("select Id,PRKEY,PRVALUE from Priority").getResltList();
List<MyClass> result = o.stream().map(arr -> new MyClass((Long) arr[0], (String) arr[1])).collect(Collectors.toList());
Or you also can use a JdbcTemplate instead of EntityManager:
#Autowired
private JdbcTemplate jdbcTemplate;
public List<MyClass> runQuery() {
String select = "select Id,yourParameterHere from Priority";
return jdbcTemplate.query(select, (rs, rowNum) -> new MyClass(rs.getLong("Id"), rs.getString("yourParameterHere")));
}

How to do a Findby In Springboot Mongo repository for Nested Objects

Need your help here in a Spring Data CrudRepository on how to do a findby of nested fields.
My class is of following structure and I need to query using truckId which is of the nested objects
#Document(collection = "unt-truck")
public class TruckModelDTO {
private String type;
private TestDTO testDTO;
}
Class TestDTO.java
public class TestDTO{
private TruckDTO truckDTO;
Private String version;
}
Class TruckDTO.java
public class TruckDTO {
private String truckId;
private String legacySystem;
}
Class TruckRepository.java
#Repository
public interface TruckRepository extends MongoRepository<TruckModelDTO, String> {
// TruckModelDTO findByTruckId(String truckid);
}
So how should I use findby for truckId which is inside a nested class?
You can use with either Entity or DTO as response. but you have to mention the relation between two or three Entity based on you requirement.
I have created Entity. you have to create DTO with same based on entity then apply main DTO as return type in JPA Repository query.
1.Entity
#Entity
public class TruckModel {
private String type;
// mention the relation based on your requirement
private Test test;
}
Entity
#Entity
public class Test {
// here mention the relation based on you requirement
private Truck truck;
Private String version;
}
3.Entity
#Entity
public class Truck {
private String truckId;
private String legacySystem;
}
Repository
#Repository
public interface TruckModelRepository extends MongoRepository<TruckModel, String> {
TruckModelDTO findByTestTruckTruckId(String truckid);
}

Spring Jpa Specification unable to locate attribute in sub classes

so I have following hierarchy of entities:
#MappedSuperClass
public abstract class BaseEntity {
private Long id;
private Date createAt;
private Date updateAt;
}
#Entity
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public abstract class Post extends BaseEntity {
private String creatorId;
private String title;
private String content;
}
#Entity
public class Article extends Post {
private String category; // article has category
}
#Entity
public class Journal extends Post {
private Date expiration; // journal has expiration
}
now when I use Spring Jpa Specification to query for articles with certain category, it won't work:
// define specification
public class ArticleSpecifications {
public static Specification<Article> withCategory(String category) {
(root, query, criteriaBuilder) ->
criteriaBuilder.equal(root.get("category"), category)
}
}
// repository
public interface PostRepository<T extends Post> extends JpaRepository<T, Long>, JpaSpecificationExecutor<T> { ... }
// usage: in some service class
#Autowired
private PostRepository<Article> articleRepository;
...
public void someMethod {
...
// error here
articleRepository.findAll(ArticleSpecifications.withCategory("news"), pageable);
...
}
Error message:
java.lang.IllegalArgumentException: Unable to locate Attribute with the the given name [category] on this ManagedType [com.gladdev.galahad.base.BaseEntity]
Just trying to understand here why it tries to look up "category" in BaseEntity.
Every Specification accessing attributes defined in Post works just fine.
Is it some spring jpa specification bug or I missed something?
You can use the CriteriaBuilder.treat() method.
In Criteria API, downcasting an entity to a subclass entity can be performed by using one of the CriteriaBuilder.treat() methods:
See https://docs.oracle.com/javaee/7/api/javax/persistence/criteria/CriteriaBuilder.html#treat-javax.persistence.criteria.Root-java.lang.Class-
public static Specification<Article> withCategory(String category) {
return (root, query, criteriaBuilder) -> {
Root<Article> article = criteriaBuilder.treat(root, Article.class);
return criteriaBuilder.equal(article.get("category"), category);
};
}

Invalid parameter index! You seem to have declared too little query method parameters

I have an entity:
#Document(collection = "userData")
#Data
public class UserData {
#Id
#Field("_id")
private String id;
#Field("orderColumns")
private List<String> orderColumns;
#Field("ownerId")
private String ownerId;
}
And I have a repository:
#Repository
interface UserDataRepository extends MongoRepository<UserData, String> {
Optional<UserData> findByOwnerIdAndOrderColumnsExists(String ownerId);
}
When I execute this method, I get an exception:
Invalid parameter index! You seem to have declared too little query method parameters!

No converter found capable of converting from type to type

I am getting the following stacktrace:
org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [referencedata.ABDeadlineType] to type [referencedata.DeadlineType]
at org.springframework.core.convert.support.GenericConversionService.handleConverterNotFound(GenericConversionService.java:324)
at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:206)
at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:187)
at org.springframework.data.repository.query.ResultProcessor$ProjectingConverter.convert(ResultProcessor.java:256)
at org.springframework.data.repository.query.ResultProcessor$ChainingConverter$1.convert(ResultProcessor.java:201)
at org.springframework.data.repository.query.ResultProcessor$ChainingConverter.convert(ResultProcessor.java:212)
at org.springframework.data.repository.query.ResultProcessor.processResult(ResultProcessor.java:149)
at org.springframework.data.jpa.repository.query.AbstractJpaQuery.doExecute(AbstractJpaQuery.java:121)
at org.springframework.data.jpa.repository.query.AbstractJpaQuery.execute(AbstractJpaQuery.java:106)
at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.doInvoke(RepositoryFactorySupport.java:483)
at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.invoke(RepositoryFactorySupport.java:461)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:56)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:282)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:136)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:133)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.data.repository.core.support.SurroundingTransactionDetectorMethodInterceptor.invoke(SurroundingTransactionDetectorMethodInterceptor.java:57)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213)
at com.sun.proxy.$Proxy143.findAllSummarizedBy(Unknown Source)
at
My classes are the following
DeadlineType
#Data
public class DeadlineType extends DefaultIdAndText {
#Value("#{target.id}")
String id;
#Value("#{target.code}")
String text;
#Value("#{target.id}")
public String getId() {
return id;
}
#Value("#{target.code}")
public String getText() {
return text;
}
}
ABDeadlineType
#Data
#Entity
#Table(name = "deadline_type")
#AllArgsConstructor
#NoArgsConstructor
public class ABDeadlineType {
private #Id
String id;
private String code;
}
DefaultIdAndText
#Data #AllArgsConstructor
#NoArgsConstructor
public class DefaultIdAndText implements IdAndText {
public DefaultIdAndText(IdAndText idAndText){
this.id = idAndText.getId();
this.text = idAndText.getText();
}
#NotEmpty String id;
String text;
}
DeadlineTypeRepository
public interface DeadlineTypeRepository extends JpaRepository<ABDeadlineType, Long> {
List<DeadlineType> findAllSummarizedBy();
}
Update
Could it be an issue that the projection/mapping using #Value("#{target.id}") format, does not work correctly because these have been done on a class and not on an interface???
Return ABDeadlineType from repository:
public interface ABDeadlineTypeRepository extends JpaRepository<ABDeadlineType, Long> {
List<ABDeadlineType> findAllSummarizedBy();
}
and then convert to DeadlineType. Manually or use mapstruct.
Or call constructor from #Query annotation:
public interface DeadlineTypeRepository extends JpaRepository<ABDeadlineType, Long> {
#Query("select new package.DeadlineType(a.id, a.code) from ABDeadlineType a ")
List<DeadlineType> findAllSummarizedBy();
}
Or use #Projection:
#Projection(name = "deadline", types = { ABDeadlineType.class })
public interface DeadlineType {
#Value("#{target.id}")
String getId();
#Value("#{target.code}")
String getText();
}
Update:
Spring can work without #Projection annotation:
public interface DeadlineType {
String getId();
String getText();
}
You may already have this working, but the I created a test project with the classes below allowing you to retrieve the data into an entity, projection or dto.
Projection - this will return the code column twice, once named code and also named text (for example only). As you say above, you don't need the #Projection annotation
import org.springframework.beans.factory.annotation.Value;
public interface DeadlineTypeProjection {
String getId();
// can get code and or change name of getter below
String getCode();
// Points to the code attribute of entity class
#Value(value = "#{target.code}")
String getText();
}
DTO class - not sure why this was inheriting from your base class and then redefining the attributes. JsonProperty just an example of how you'd change the name of the field passed back to a REST end point
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
#Data
#AllArgsConstructor
public class DeadlineType {
String id;
// Use this annotation if you need to change the name of the property that is passed back from controller
// Needs to be called code to be used in Repository
#JsonProperty(value = "text")
String code;
}
Entity class
import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
#Data
#Entity
#Table(name = "deadline_type")
public class ABDeadlineType {
#Id
private String id;
private String code;
}
Repository - your repository extends JpaRepository<ABDeadlineType, Long> but the Id is a String, so updated below to JpaRepository<ABDeadlineType, String>
import com.example.demo.entity.ABDeadlineType;
import com.example.demo.projection.DeadlineTypeProjection;
import com.example.demo.transfer.DeadlineType;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface ABDeadlineTypeRepository extends JpaRepository<ABDeadlineType, String> {
List<ABDeadlineType> findAll();
List<DeadlineType> findAllDtoBy();
List<DeadlineTypeProjection> findAllProjectionBy();
}
Example Controller - accesses the repository directly to simplify code
#RequestMapping(value = "deadlinetype")
#RestController
public class DeadlineTypeController {
private final ABDeadlineTypeRepository abDeadlineTypeRepository;
#Autowired
public DeadlineTypeController(ABDeadlineTypeRepository abDeadlineTypeRepository) {
this.abDeadlineTypeRepository = abDeadlineTypeRepository;
}
#GetMapping(value = "/list")
public ResponseEntity<List<ABDeadlineType>> list() {
List<ABDeadlineType> types = abDeadlineTypeRepository.findAll();
return ResponseEntity.ok(types);
}
#GetMapping(value = "/listdto")
public ResponseEntity<List<DeadlineType>> listDto() {
List<DeadlineType> types = abDeadlineTypeRepository.findAllDtoBy();
return ResponseEntity.ok(types);
}
#GetMapping(value = "/listprojection")
public ResponseEntity<List<DeadlineTypeProjection>> listProjection() {
List<DeadlineTypeProjection> types = abDeadlineTypeRepository.findAllProjectionBy();
return ResponseEntity.ok(types);
}
}
Hope that helps
Les
I have met the same problem recently with spring-data-jpa:2.5.0.
Solution (for queries with no #Query annotation):
For class-based projection (DTOs), the problem is the #NoArgsConstructor in the DTO class. Revemoving it should make things work.
Something interesting I found during debug:
With the presence of a non argument constructor, the returnedType somehow was created with 0 input properties.
When a query is actually created, JpaQueryCreator (spring-data-jpa) would check if it needs to do custom construction according to the number of input properties.
As it's not the case with 0 input properties, it would then return the whole entity instance.
https://github.com/spring-projects/spring-data-jpa/blob/main/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryCreator.java#L169
Finally when the result is being returned, the target type and returned type don't match, as there is no converter available to convert from the entity instance to the projectiong dto. The error was thrown.
https://github.com/spring-projects/spring-data-commons/blob/main/src/main/java/org/springframework/data/repository/query/ResultProcessor.java#L162
Simple Solution::
use {nativeQuery=true} in your query.
for example
#Query(value = "select d.id,d.name,d.breed,d.origin from Dog d",nativeQuery = true)
List<Dog> findALL();
If you look at the exception stack trace it says that, it failed to convert from ABDeadlineType to DeadlineType. Because your repository is going to return you the objects of ABDeadlineType. How the spring-data-jpa will convert into the other one(DeadlineType). You should return the same type from repository and then have some intermediate util class to convert it into your model class.
public interface ABDeadlineTypeRepository extends JpaRepository<ABDeadlineType, Long> {
List<ABDeadlineType> findAllSummarizedBy();
}
Turns out, when the table name is different than the model name, you have to change the annotations to:
#Entity
#Table(name = "table_name")
class WhateverNameYouWant {
...
Instead of simply using the #Entity annotation.
What was weird for me, is that the class it was trying to convert to didn't exist. This worked for me.
Well I have another answer I have used Interfaces for Projections and Classes for
Dto's and I am using ModelMapper to map my Projections to Dto Class
So my 1 Dto class may have many Projections which can mapped to Dto and used to Taste
gradle
implementation 'org.modelmapper:modelmapper:3.1.0'
#Autowired
private ModelMapper modelMapper;
List<UserDto> usersdto = repository.findUserByRoleName().stream().map(userprojection -> modelMapper.map(userprojection, UserDto.class))
.collect(Collectors.toList());
My projection is like this
public interface UserProjection {
String getId();
String getEmail();
}
My dto is
#Data
#AllArgsConstructor
#NoArgsConstructor
public class UserDto {
private long id;
private String firstName;
private String lastName;
private String phone;
private String email;
}
And I am able to get fields from custom queries
Change the class name to DeadlineType inside
extends JpaRepository<class, type>
For example:
In your code, the repository where you placed you query extends JpaRepository that with class and id type <ABDeadlineType, Long>. So it expects to return ABDeadlineType data.
public interface DeadlineTypeRepository extends JpaRepository<ABDeadlineType, Long> {
List<DeadlineType> findAllSummarizedBy();
}
As you want to get DeadlineType data, you should keep your query in such repository like
public interface DeadlineTypeRepository extends JpaRepository<DeadlineType, Long>
So, either replace the class name inside JpaRepository<>
Or place your query in another repository. Then you don't need to do any mapping or write extra codes for it.
In my case, it worked.

Resources