Spring Data i Vavr -> nie mogę zapisać Mapy do mongodb

0

Cześć,

chcę użyć io.vavr.collection.Map, ale niestety wywala mi się przy próbie zapisu do bazy MongoDB, niestety nie wiem jak to obejść.

public class Location {

    public final Map<String, String> region;
    public final List<String> list = List.of("vavr list works fine");

    public Location() {
        this.region = HashMap.of("City", "Warsaw", "Country", "Poland");
    }
}

public interface LocationRepository extends CrudRepository<Location, String> {
}

@SpringBootApplication
public class VavrMapMongoApplication {

	public static void main(String[] args) {
		SpringApplication.run(VavrMapMongoApplication.class, args);
	}

	@Bean
	public ObjectMapper jacksonBuilder() {
		ObjectMapper mapper = new ObjectMapper();
		return mapper.registerModule(new VavrModule());
	}

	@Bean
	public CommandLineRunner importData(LocationRepository repository) {
		return (String... args) -> {
			final Location location = new Location();
			repository.save(location);
		};
	}
}

Mój pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>test</groupId>
    <artifactId>vavr-map-mongo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>vavr-map-mongo</name>
    <description>Demo project for Spring Boot</description>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>io.vavr</groupId>
            <artifactId>vavr</artifactId>
            <version>0.10.0</version>
        </dependency>
        <dependency>
            <groupId>io.vavr</groupId>
            <artifactId>vavr-jackson</artifactId>
            <version>0.10.0</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>11</source>
                    <target>11</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

I oczywiście błąd z jakim mi się to wywala:

java.lang.IllegalStateException: Failed to execute CommandLineRunner
	at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:816) ~[spring-boot-2.1.4.RELEASE.jar:2.1.4.RELEASE]
	at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:797) ~[spring-boot-2.1.4.RELEASE.jar:2.1.4.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:324) ~[spring-boot-2.1.4.RELEASE.jar:2.1.4.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1260) ~[spring-boot-2.1.4.RELEASE.jar:2.1.4.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1248) ~[spring-boot-2.1.4.RELEASE.jar:2.1.4.RELEASE]
	at test.vavrmapmongo.VavrMapMongoApplication.main(VavrMapMongoApplication.java:14) ~[classes/:na]
Caused by: java.lang.ClassCastException: class io.vavr.collection.HashMap cannot be cast to class java.util.Map (io.vavr.collection.HashMap is in unnamed module of loader 'app'; java.util.Map is in module java.base of loader 'bootstrap')
	at org.springframework.data.mongodb.core.convert.MappingMongoConverter.writePropertyInternal(MappingMongoConverter.java:582) ~[spring-data-mongodb-2.1.6.RELEASE.jar:2.1.6.RELEASE]
	at org.springframework.data.mongodb.core.convert.MappingMongoConverter.writeProperties(MappingMongoConverter.java:550) ~[spring-data-mongodb-2.1.6.RELEASE.jar:2.1.6.RELEASE]
	at org.springframework.data.mongodb.core.convert.MappingMongoConverter.writeInternal(MappingMongoConverter.java:526) ~[spring-data-mongodb-2.1.6.RELEASE.jar:2.1.6.RELEASE]
	at org.springframework.data.mongodb.core.convert.MappingMongoConverter.writeInternal(MappingMongoConverter.java:499) ~[spring-data-mongodb-2.1.6.RELEASE.jar:2.1.6.RELEASE]
	at org.springframework.data.mongodb.core.convert.MappingMongoConverter.write(MappingMongoConverter.java:443) ~[spring-data-mongodb-2.1.6.RELEASE.jar:2.1.6.RELEASE]
	at org.springframework.data.mongodb.core.convert.MappingMongoConverter.write(MappingMongoConverter.java:80) ~[spring-data-mongodb-2.1.6.RELEASE.jar:2.1.6.RELEASE]
	at org.springframework.data.mongodb.core.EntityOperations$MappedEntity.toMappedDocument(EntityOperations.java:509) ~[spring-data-mongodb-2.1.6.RELEASE.jar:2.1.6.RELEASE]
	at org.springframework.data.mongodb.core.MongoTemplate.doInsert(MongoTemplate.java:1241) ~[spring-data-mongodb-2.1.6.RELEASE.jar:2.1.6.RELEASE]
	at org.springframework.data.mongodb.core.MongoTemplate.insert(MongoTemplate.java:1178) ~[spring-data-mongodb-2.1.6.RELEASE.jar:2.1.6.RELEASE]
	at org.springframework.data.mongodb.repository.support.SimpleMongoRepository.save(SimpleMongoRepository.java:81) ~[spring-data-mongodb-2.1.6.RELEASE.jar:2.1.6.RELEASE]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:567) ~[na:na]
	at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:359) ~[spring-data-commons-2.1.6.RELEASE.jar:2.1.6.RELEASE]
	at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:200) ~[spring-data-commons-2.1.6.RELEASE.jar:2.1.6.RELEASE]
	at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:644) ~[spring-data-commons-2.1.6.RELEASE.jar:2.1.6.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.doInvoke(RepositoryFactorySupport.java:608) ~[spring-data-commons-2.1.6.RELEASE.jar:2.1.6.RELEASE]
	at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.lambda$invoke$3(RepositoryFactorySupport.java:595) ~[spring-data-commons-2.1.6.RELEASE.jar:2.1.6.RELEASE]
	at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.invoke(RepositoryFactorySupport.java:595) ~[spring-data-commons-2.1.6.RELEASE.jar:2.1.6.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:59) ~[spring-data-commons-2.1.6.RELEASE.jar:2.1.6.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:93) ~[spring-aop-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.data.repository.core.support.SurroundingTransactionDetectorMethodInterceptor.invoke(SurroundingTransactionDetectorMethodInterceptor.java:61) ~[spring-data-commons-2.1.6.RELEASE.jar:2.1.6.RELEASE]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212) ~[spring-aop-5.1.6.RELEASE.jar:5.1.6.RELEASE]
	at com.sun.proxy.$Proxy53.save(Unknown Source) ~[na:na]
	at test.vavrmapmongo.VavrMapMongoApplication.lambda$importData$0(VavrMapMongoApplication.java:27) ~[classes/:na]
	at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:813) ~[spring-boot-2.1.4.RELEASE.jar:2.1.4.RELEASE]
	... 5 common frames omitted


Process finished with exit code 1

Wie ktoś jak serializować vavrową mapę do mongodb ze spring data?

2

Myślę że będziesz musiał to przekonwertować, dodając convertery do MongoCustomConversions

Zobacz @WrtingConverter i MongoCustomConversions.class

0

Znalazłem niby jakiś przykładowy projekt... ale chyba tam nie ma tego problemu.
zobacz https://github.com/Hejwo/spring-data-mongo-vavr
Powalcz z tym co napisał Kermiii.....
I Napisz czy coś Ci się udało... bo przejrzałem kod Springa i IMO mają tam zwałę - trzeba będzie poprawkę im wrzucić. (Ale może coś przeoczyłem).

0

a czemu se swojego konwetera do org.bson.Document nie napiszesz? To jest 20 linijek roboty. A jak bedziesz mial document to juz latwo wbic do Mongo.

0

Vavrowa Mapa ma metodę toJavaMap, więc może wystarczy ją odpalić przed zapisem?

1

Dzięki za odpowiedzi ;) Tak jak @Kermii napisał, dodałem taką klasę konfiguracyjną:

@Configuration
public class VavrCustomConverter {

    @Bean
    public MongoCustomConversions customConversions() {
        return new MongoCustomConversions(asList(new VavrMapToMapConverter(), new MapToVavrMapConverter()));
    }

    @WritingConverter
    private static class VavrMapToMapConverter implements Converter<Map, java.util.Map> {

        @Override
        public java.util.Map convert(Map map) {
            return map.toJavaMap();
        }
    }

    @ReadingConverter
    private static class MapToVavrMapConverter implements Converter<java.util.Map, Map> {

        @Override
        public Map convert(java.util.Map map) {
            return HashMap.ofAll(map);
        }
    }
}

I generalnie działa. Zastanawiam się jeszcze, czy można jakoś vavr-jackson do tego zaciągnąć i mieć z palca wszystkie kolekcje. Z drugiej strony nie ma ich tak wiele, a po dodaniu takiego konwertera jest czysty dokument w mongo - bez tych wszystkich vavrowych headów, taili i _class.

0

Powyższe rozwiązanie działa, ale tylko jeśli w vavrowej mapie mamy typy proste. Przy wrzuceniu do niej obiektu, np:

public class Region {
    public final String country;
    public final String city;

    public Region(String country, String city) {
        this.country = country;
        this.city = city;
    }
}

public class Location {
    public final Map<String, Region> region;

    public Location(Map<String, Region> region) {
        this.region = region;
    }
}

podczas próby zapisu do bazy wali:

Caused by: org.bson.codecs.configuration.CodecConfigurationException: Can't find a codec for class test.vavrmapmongo.Region.
    at org.bson.codecs.configuration.CodecCache.getOrThrow(CodecCache.java:46) ~[bson-3.8.2.jar:na]
    at org.bson.codecs.configuration.ProvidersCodecRegistry.get(ProvidersCodecRegistry.java:63) ~[bson-3.8.2.jar:na]

Próbowałem też z org.bson.Document tak jak @Julian_ proponował, na wzór tego https://docs.spring.io/spring-data/mongodb/docs/2.1.6.RELEASE/reference/html/#mapping-explicit-converters, ale nadal miałem ten sam wyjątek. Być może trzeba zrobić coś jak tutaj https://stackoverflow.com/questions/50493371/spring-data-reactive-mongo-class-type-converter, ale nie bardzo czaję co tam się dzieje i nie ma co się kopać z koniem. Na razie olewam wszystko to co powyżej i korzystam z java.util.Map. O tak:

public class Location {
    private final Map<String, Region> region;

    public Location(Map<String, Region> region) {
        this.region = region;
    }

    public io.vavr.collection.Map<String, Region> getRegion() {
        return HashMap.ofAll(region);
    }
}

i działa bez problemu.

1 użytkowników online, w tym zalogowanych: 0, gości: 1