SD MongoDB polymorphism in subdocument - spring

I just started developing some app in Java with spring-data-mongodb and came across some issue that I haven't been able to solve:
Have a couple of document beans like this:
#Document(collection="myBeanBar")
public class BarImpl implements Bar {
String id;
Foo foo;
// More fields and methods ...
}
#Docuemnt
public class FooImpl implements Foo {
String id;
String someField;
// some more fields and methods ...
}
And I have a repository class with a method that simply invokes a find similar to this:
public List<? extends Bar> findByFooField(final String fieldValue) {
Query query = Query.query(Criteria.where("foo.someField").is(fieldValue));
return getMongoOperations().find(query, BarImpl.class);
}
Saving a Bar works just fine, it would save it in mongo along with the "_class" attribute for both Foo and Bar. However, finding by some attribute in Foo would throw an exception like this:
Exception in thread "main" java.lang.IllegalArgumentException: No property someField found on test.Foo!
at org.springframework.data.mapping.context.AbstractMappingContext.getPersistentPropertyPath(AbstractMappingContext.java:225)
at org.springframework.data.mongodb.core.convert.QueryMapper.getPath(QueryMapper.java:202)
at org.springframework.data.mongodb.core.convert.QueryMapper.getTargetProperty(QueryMapper.java:190)
at org.springframework.data.mongodb.core.convert.QueryMapper.getMappedObject(QueryMapper.java:86)
at org.springframework.data.mongodb.core.MongoTemplate.doFind(MongoTemplate.java:1336)
at org.springframework.data.mongodb.core.MongoTemplate.doFind(MongoTemplate.java:1322)
at org.springframework.data.mongodb.core.MongoTemplate.find(MongoTemplate.java:495)
at org.springframework.data.mongodb.core.MongoTemplate.find(MongoTemplate.java:486)
Which, after some digging, makes some sense, since nowhere in the query is the sub-document concrete type being specified, and the Entity Information of Bar says the type of foo is Foo (not FooImpl), which in turn can not have properties cause it is an interface.
My question is: Is there a way to specify it or work-around this issue without declaring the sub-document type as a concrete type?
I've been googling it for a couple of days and looking at the documentation and API and the source code but I can not find a clear way to do it. I'd really appreciate your help.
Thank you very much.

I had a similar problem, I have a class that implements an interface and when I use findAll I get the error:
org.springframework.data.mapping.model.MappingInstantiationException: Could not instantiate bean class [test.MetaClasse]: Specified class is an interface.
After debugging SpringData code, I realized that Mapper uses #TypeAlias to discover the type it has to instantiate, so I just put #TypeAlias("FullClassName") on my implementations of test.MetaClasse and it worked!
I tested with your situation and it will work!

Like mentioned in this comment, the solution with having full class name in the type alias is imperfect as it might make refactoring cumbersome.
Instead you can just configure type mappings and make it work automagically. Here's how:
First you'll need to annotate BarImpl and FooImpl with #TypeAlias. It doesn't have to be a full class name, could be anything else. For example #TypeAlias("bar_impl") and #TypeAlias("foo_impl") respectively.
Then we’re going to need the reflections library. Pick the latest version for the build tool of your choice here.
For example with Gradle:
implementation("org.reflections:reflections:0.10.2")
Now we’re going to need a small extension to DefaultMongoTypeMapper to make it easy to configure and instantiate. Here’s how it would look in Kotlin:
class ReflectiveMongoTypeMapper(
private val reflections: Reflections = Reflections("com.example")
) : DefaultMongoTypeMapper(
DEFAULT_TYPE_KEY,
listOf(
ConfigurableTypeInformationMapper(
reflections.getTypesAnnotatedWith(TypeAlias::class.java).associateWith { clazz ->
getAnnotation(clazz, TypeAlias::class.java)!!.value
}
),
SimpleTypeInformationMapper(),
)
)
where com.example is either your base package or the package with MongoDB models.
This way we will find all classes annotated with #TypeAlias and register alias to type mappings.
Next we'll need to adjust the app's mongo configuration a bit. The configuration has to extend AbstractMongoClientConfiguration and we need to override method mappingMongoConverter to make use of the mapper we created before. It should look like this:
override fun mappingMongoConverter(
databaseFactory: MongoDatabaseFactory,
customConversions: MongoCustomConversions,
mappingContext: MongoMappingContext,
) = super.mappingMongoConverter(databaseFactory, customConversions, mappingContext).apply {
setTypeMapper(ReflectiveMongoTypeMapper())
}
Done!
Now all alias to type mappings will be registered automatically on context startup and all your polymorphic fields will work just fine.
You can check the full code example on GitHub.
Also, here's a blog post where you can read about the root cause of this issue as well as check other ways to solve it (in case you don't want to rely on reflection): https://blog.monosoul.dev/2022/09/16/spring-data-mongodb-polymorphic-fields/

Related

Not understanding why I can call interface methods without concrete implementation

I am working on a Spring Boot application which uses Spring Data Jpa. I cannot give you the exact code since I am not allowed to, but I can simplify my issue so you may get an idea:
#Query("SELECT u FROM User WHERE u.uuid = :#{#uuid}")
Optional<UserInterface> findProjectedUserByUuid(UUID uuid)
This Query return an interface of type UserInterface:
public interface UserInterface{
String getName();
String getEmail();
}
Somewhere in my service class I get to this point:
UserInterface userInterface = userRepository.findProjectedUserByUuid(uuid).get();
System.out.println(userInterface.getName());
The application correctly prints out the name of the user.
My question is: why can I call userInterface.getName() in the code and return the correct name. Isn't this supposed to be an interface? How is it holding data?
My guess is...but I am not really sure about this, hence why I am asking you guys, this is a case of polymorphism. I believe that the repository is actually returning a class which implements my interface? And maybe that is why I am able to call methods on it. But if this is true, is there any way to see which class gets returned or use it in any meaningful way?

List<List<String>> mapped to List<String>

I'm learning how to use Mapstruct in a Spring Boot and Kotlin project.
I've got a generated DTO (ThessaurusDTO) that has a List and I need this mapped into a List on my model (Vocab).
It makes sense that MapStruct can't map this automatically, but I know for a fact that the first list will always be size = 1. I have no control on the API the DTO model belongs to.
I found on the documentation that I can create define a default method implementation within the interface, which would loosely translate to a normal function in Kotlin
My mapper interface:
#Mapper
interface VocabMapper {
#Mappings(
// ...
)
fun thessaurusToVocab(thessaurusDTO: ThessaurusDTO): Vocab
fun metaSyns(nestedList: List<List<String>>): List<String>
= nestedList.flatten()
}
When I try to do a build I get the following error:
VocabMapper.java:16: error: Can't map collection element "java.util.List<java.lang.String>" to "java.lang.String ". Consider to declare/implement a mapping method: "java.lang.String map(java.util.List<java.lang.String> value)".
It looks like mapStruct is still trying to automatically do the mapping while ignoring my custom implementation. Am I missing something trivial here?
I found on the documentation that I can create define a default method implementation within the interface, which would loosely translate to a normal function in Kotlin
From my understand of what I found online, Kotlin does not properly translate an interface function into a default method in Java, but actually generates a class that implements the interface.
If that's the problem, you can annotate metaSyns with #JvmDefault:
Specifies that a JVM default method should be generated for non-abstract Kotlin interface member.
Usages of this annotation require an explicit compilation argument to be specified: either -Xjvm-default=enable or -Xjvm-default=compatibility.
See the link for the difference, but you probably need -Xjvm-default=enable.
I've seen to have fixed this by relying on an abstract based implementation, instead of using an interface.
From my understand of what I found online, Kotlin does not properly translate an interface function into a default method in Java, but actually generates a class that implements the interface.
https://github.com/mapstruct/mapstruct/issues/1577

Spring inject list of generic interface implementations in kotlin

Disclaimer: New to Kotlin, may be a easy to solve issue or misunderstood basics.
I am trying to inject a List of a specific interface's Spring implementations, in a regular java class this have been easy like this:
#Autowired
List<IMyClass> myClassList;
But in Kotlin doing the following gives me a error
#Autowired
lateinit private var myClassList: List<IMyClass<Any>>
// No beans of '? extends IMyClass<Object>' or 'List<? extends IMyClass<Object>>' types found
Doing it like this:
#Autowired
lateinit private var myClassList: List<IMyClass<*>>
Makes the injection work but doesn't allow me to use a function defined in the interface that takes a generic object as input
// Out-projected type 'IMyClass<*>' prohibits the use of 'public abstract fun myFunction(obj: T!): T! defined in com.path.IMyClass'
So how am I supposed to solve this? Is it easier to rewrite the interface in Kotlin with some specific syntac?
Thing you're doing in Java is just using implicit wildcard. So you have 2 ways here:
Try to refactor API and inject list of List<IMyClass<? extends SomeInterface>>
Use injected List<IMyClass<*>> but cast it explicitly to thing you need, i.e. myClassList as List<IMyClass<Any>>
Thing here is kotlin's erasure is more explicit. If you don't know what type is there — we can't guarantee your code will work, because there is such type in kotlin as Nothing, instance of which can't exist.
I faced a similar situation and as answered by asm0dey, casting was solution which I felt was better for me, but doing it in a cleaner way was a concern.
Just answering below how exactly I did it in the best possible way that I could think of:
#Service
class MyService(_myClassList: List<IMyClass<out Any>>) {
#Suppress("UNCHECKED_CAST")
val myClassList: List<IMyClass<Any>> = _myClassList.map { it as IMyClass<Any> }
// ...
}

How to implement Abstract Factory for windowing systems

I was just starting a new coding project. I may be ahead of myself, but I've gotten kinda stuck. I wanted to implement an Abstract Factory for the GUI, similar to the example on Wikipedia. However various systems have their own parameters for creating windows. At present I have come up with the following solutions to my dilemma:
Create a type which varies based on compiler directives
Don't use compiler directives and just put everything in a type that contains every possible data member
Create a polymorphic hierarchy and use dynamic casting inside each window function
Use some sort of intermediate singleton that holds the information. This seems esp. unhelpful and would likely also involve casting.
Use a different pattern, such as builder instead.
My objective is to create high level interfaces that are uniform, so that creating a window, etc. is the same for all platforms.
I hesitate to do #5 simply because it seems like this would be a common enough problem that there should already be a solution. This is just a toy, so it's more about learning than building a practical application. I know I could use existing code bases, but that wouldn't achieve my real objective.
Thanks in advance.
I think, it depends on the situation. But how about using abstract factory with builder (inside factory) and decorator with some default values for GUI componets, where decorator will have same interface for similar components from different GUI libraries and extends class from GUI library.
After reading more I've realized I can use Dependency Injection to create the concrete factory first. Since entry point knows what kind of factory it's using, that can be passed to the client. I can't believe I didn't see it before, but I don't think that Dependency Injection "clicked" until now.
I would put the system-specific parameters in the constructor for each abstract factory.
public interface WindowFactory {
public Window build();
}
public class WindowsWindowFactory implements WindowFactory {
private param1, param2, param3;
public WindowsWindowFactory(param1,param2,param3) {} // set params
public Window build() {} // use params
}
public class LinuxWindowFactory implements WindowFactory {
private param1, param2;
public LinuxWindowFactory(param1,param2) {} // set params
public Window build() {} // use params
}

Spring Interface injection - How to access implementation class specific methods

For clarity consider following design -
Interface MyIface{
myInterfaceContract(someObject);
};
Class Child1 implements MyIface{
myInterfaceContract(someObject){ //implemented }
myChild1Action(){ //implemented }
}
Class Child2 implements MyIface{
myInterfaceContract(someObject){ //implemented }
myChild2Action(){ //implemented }
}
I know how to achieve interface injection through spring annotation.
doubt is -
If interface based injection is done then how can I access methods directly implemented in my child class i.e. myChild1Action()/myChild2Action()
Am i doing or thinking drastically wrong?
I'd say your thinking is confused. If you are injecting based on the interface then that implies that the interface gives you all the functionality you need. If you need methods from the implementations then that interface isn't giving you what you need and injecting a concrete implementation is what you need to do.
Most of the time, if you are using instanceof to find what class you have so you can call some extra method, then it's a sign your design is broken.
You might find it helpful to define a "Finder" interface and have Child1 implement that as well and inject the bean into a Finder field as well as a MyIFace field.

Resources