Skip to content

The attribute 'id' is not mapped to a Graph property in ReactiveNeo4jTemplate #2676

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
yacosta738 opened this issue Feb 17, 2023 · 13 comments
Closed
Labels
status: works-as-expected An issue that is not valid as the error or outcome is expected.

Comments

@yacosta738
Copy link

Hello everyone, I'm having a problem with my domain layer. I have an entity that is shared between Postgres and Neo4j. These are my models.

import com.astrum.data.ModifiableULIDEntity
import com.astrum.data.annotation.Key
import org.springframework.data.neo4j.core.schema.Node
import org.springframework.data.relational.core.mapping.Table

@Table("persons")
@Node("persons")
data class Person(
    @Key
    var name: String,
    var age: Int,
) : ModifiableULIDEntity()
import com.astrum.data.annotation.GeneratedValue
import java.time.Instant

abstract class ModifiableULIDEntity : ULIDEntity(), Modifiable {
    @GeneratedValue
    override var createdAt: Instant? = null

    @GeneratedValue
    override var updatedAt: Instant? = null
}
import com.astrum.data.annotation.GeneratedValue
import com.astrum.ulid.ULID
import org.springframework.data.annotation.Id
import org.springframework.data.neo4j.core.schema.Id as IdGraph

abstract class ULIDEntity : Entity<ULID>() {
    @Id
    @IdGraph
    @GeneratedValue
    override var id: ULID = ULID.randomULID()
}
abstract class Entity<ID> {
    abstract var id: ID

    override fun hashCode(): Int {
        return id.hashCode()
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as Entity<*>

        return id == other.id
    }
}

The problem is with the Neo4j part when I try to save a Person type entity. In the following class I use in a test it fails with the following error.

The attribute 'id' is not mapped to a Graph property
org.springframework.data.mapping.MappingException: The attribute 'id' is not mapped to a Graph property
	at app//org.springframework.data.neo4j.core.mapping.DefaultNeo4jPersistentProperty.getPropertyName(DefaultNeo4jPersistentProperty.java:261)
	at app//org.springframework.data.neo4j.core.mapping.DefaultNeo4jPersistentEntity.computeIdDescription(DefaultNeo4jPersistentEntity.java:420)
Create Person migration

Required identifier property not found for class com.astrum.data.entity.Person
java.lang.IllegalStateException: Required identifier property not found for class com.astrum.data.entity.Person
	at org.springframework.data.mapping.PersistentEntity.getRequiredIdProperty(PersistentEntity.java:135)
	at org.springframework.data.neo4j.core.mapping.DefaultNeo4jIsNewStrategy.basedOn(DefaultNeo4jIsNewStrategy.java:57)
	at org.springframework.data.neo4j.core.mapping.DefaultNeo4jPersistentEntity.getFallbackIsNewStrategy(DefaultNeo4jPersistentEntity.java:196)
import com.astrum.data.entity.Person
import com.astrum.data.migration.Migration
import kotlinx.coroutines.reactive.awaitFirstOrNull
import org.springframework.data.neo4j.core.ReactiveNeo4jTemplate

class CreatePerson(
    private val neo4jTemplate: ReactiveNeo4jTemplate
) : Migration {
    override suspend fun up() {
        println("Create Person migration")
        neo4jTemplate.save<Person>(Person("test", 10)).awaitFirstOrNull()
    }

    override suspend fun down() {
        neo4jTemplate.deleteAll(Person::class.java).awaitFirstOrNull()
    }
}
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Feb 17, 2023
@yacosta738
Copy link
Author

This is my helper class

import org.neo4j.configuration.GraphDatabaseSettings
import org.neo4j.configuration.GraphDatabaseSettings.DEFAULT_DATABASE_NAME
import org.neo4j.dbms.api.DatabaseManagementService
import org.neo4j.dbms.api.DatabaseManagementServiceBuilder
import org.neo4j.driver.AuthTokens
import org.neo4j.driver.Driver
import org.neo4j.driver.GraphDatabase
import org.neo4j.graphdb.GraphDatabaseService
import org.springframework.data.neo4j.core.ReactiveNeo4jClient
import org.springframework.data.neo4j.core.ReactiveNeo4jTemplate
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext
import java.io.File
import java.time.Duration
import kotlin.io.path.createTempDirectory

class Neo4jTestHelper : ResourceTestHelper {
    lateinit var reactiveNeo4jTemplate: ReactiveNeo4jTemplate
    private lateinit var driver: Driver
    private lateinit var reactiveNeo4jClient: ReactiveNeo4jClient
    private val databaseDirectory = createTempDirectory("neo4j-test-")
    private val managementService: DatabaseManagementService =
        DatabaseManagementServiceBuilder(databaseDirectory)
            .setConfig(GraphDatabaseSettings.transaction_timeout, Duration.ofSeconds(60))
            .setConfig(GraphDatabaseSettings.preallocate_logical_logs, true).build()

    override fun setUp() {
        driver =
            GraphDatabase.driver("bolt://localhost:7687", AuthTokens.basic("neo4j", "password"));
        reactiveNeo4jClient = ReactiveNeo4jClient.create(driver)
        val graphDb: GraphDatabaseService = managementService.database(DEFAULT_DATABASE_NAME)
        reactiveNeo4jTemplate =
            ReactiveNeo4jTemplate(
                reactiveNeo4jClient, Neo4jMappingContext.builder()
                    
                    .build()
            )
        if (graphDb.isAvailable) {
            graphDb.executeTransactionally("MATCH (n) DETACH DELETE n")
        }
    }

    override fun tearDown() {
        managementService.shutdown()
        // delete the database directory and all its contents recursively (if it exists) after the test
        deleteRecursively(databaseDirectory.toFile())
    }

    private fun deleteRecursively(file: File) {
        if (file.isDirectory) {
            val files = file.listFiles()
            if (files != null) {
                for (child in files) {
                    deleteRecursively(child)
                }
            }
        }
        if (!file.delete()) {
            throw RuntimeException("Failed to delete file: $file")
        }
    }
}

@michael-simons
Copy link
Collaborator

Please remove your @IdGraph annotation. @Id should be enough. It's basically double annotated because your aliasing the neo4j specific one.

@michael-simons michael-simons added status: waiting-for-feedback We need additional information before we can continue and removed status: waiting-for-triage An issue we've not yet triaged labels Feb 20, 2023
@yacosta738
Copy link
Author

yacosta738 commented Feb 20, 2023

I attempted to use both IDs, the default from Spring Data and the one from Spring Data Neo4j. Unfortunately, it still didn't work.

I tried using org.springframework.data.neo4j.core.schema.Id

import com.astrum.data.annotation.GeneratedValue
import com.astrum.ulid.ULID
//import org.springframework.data.annotation.Id
import org.springframework.data.neo4j.core.schema.Id

abstract class ULIDEntity : Entity<ULID>() {
    @Id
    @GeneratedValue
    override var id: ULID = ULID.randomULID()
}
import com.astrum.data.annotation.GeneratedValue
import com.astrum.ulid.ULID
import org.springframework.data.annotation.Id
//import org.springframework.data.neo4j.core.schema.Id

abstract class ULIDEntity : Entity<ULID>() {
    @Id
    @GeneratedValue
    override var id: ULID = ULID.randomULID()
}

@michael-simons I'm not sure if the problem is with creating the template when passing a predefined context created by the builder Neo4jMappingContext.builder().build()

override fun setUp() {
        driver =
            GraphDatabase.driver("bolt://localhost:7687", AuthTokens.basic("neo4j", "password"));
        reactiveNeo4jClient = ReactiveNeo4jClient.create(driver)
        val graphDb: GraphDatabaseService = managementService.database(DEFAULT_DATABASE_NAME)
        reactiveNeo4jTemplate =
            ReactiveNeo4jTemplate(
                reactiveNeo4jClient, Neo4jMappingContext.builder()
                    .build()
            )
        if (graphDb.isAvailable) {
            graphDb.executeTransactionally("MATCH (n) DETACH DELETE n")
        }
    }

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Feb 20, 2023
@yacosta738
Copy link
Author

The error is the same

Required identifier property not found for class com.astrum.data.entity.Person java.lang.IllegalStateException: Required identifier property not found for class com.astrum.data.entity.Person at org.springframework.data.mapping.PersistentEntity.getRequiredIdProperty(PersistentEntity.java:135) at org.springframework.data.neo4j.core.mapping.DefaultNeo4jIsNewStrategy.basedOn(DefaultNeo4jIsNewStrategy.java:57) at org.springframework.data.neo4j.core.mapping.DefaultNeo4jPersistentEntity.getFallbackIsNewStrategy(DefaultNeo4jPersistentEntity.java:196) at org.springframework.data.mapping.model.BasicPersistentEntity.lambda$new$1(BasicPersistentEntity.java:124) at org.springframework.data.util.Lazy.getNullable(Lazy.java:245) at org.springframework.data.util.Lazy.get(Lazy.java:114) at org.springframework.data.mapping.model.BasicPersistentEntity.isNew(BasicPersistentEntity.java:391) at org.springframework.data.neo4j.core.ReactiveNeo4jTemplate.saveImpl(ReactiveNeo4jTemplate.java:394) at org.springframework.data.neo4j.core.ReactiveNeo4jTemplate.save(ReactiveNeo4jTemplate.java:318) at com.astrum.data.repository.neo4j.migration.CreatePerson.up(CreatePerson.kt:13) at com.astrum.data.migration.MigrationManager$up$2.invokeSuspend(MigrationManager.kt:66) at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106) at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539) at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) at java.base/java.lang.Thread.run(Thread.java:833)

@michael-simons
Copy link
Collaborator

Oh wow, I didn't notice you are not using any test slices or AbstractReactiveNeo4jConfig.java in your tests…

Would you please try out if you initialise it with your entity classes?

import org.springframework.data.neo4j.config.Neo4jEntityScanner;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;

public class Foo {
	
	void f() throws ClassNotFoundException {
		var ctx = Neo4jMappingContext.builder().build();
		ctx.setInitialEntitySet(Neo4jEntityScanner.get().scan("com.astrum.data.entity"));
	}
}

Also, I assume you have a proper converter for ULID in place? This is not a type we handle by default (see https://docs.spring.io/spring-data/neo4j/docs/current/reference/html/#custom.conversions.attribute.types).

Thank you.

@michael-simons michael-simons added status: waiting-for-feedback We need additional information before we can continue and removed status: feedback-provided Feedback has been provided labels Feb 21, 2023
@michael-simons
Copy link
Collaborator

Also, what does com.astrum.data.annotation.GeneratedValue do?

We have a broad variety of tests for generated values and custom generators and custom ID generated types, for both Java and Kotlin. Maybe that support is already good enough instead of running your own thing on top of what is provided?

@yacosta738
Copy link
Author

yacosta738 commented Feb 21, 2023

I have implemented the converters for ULID as shown in this class.

package com.astrum.data.converter

import com.astrum.data.annotation.ConverterScope
import com.astrum.ulid.ULID
import org.springframework.core.convert.TypeDescriptor
import org.springframework.core.convert.converter.GenericConverter
import org.springframework.data.convert.WritingConverter
import org.springframework.stereotype.Component

@Component
@WritingConverter
@ConverterScope(ConverterScope.Type.NEO4J)
class BinaryToULIDConverter : GenericConverter{
    override fun getConvertibleTypes(): MutableSet<GenericConverter.ConvertiblePair>? {
        return mutableSetOf(GenericConverter.ConvertiblePair(ByteArray::class.java, ULID::class.java))
    }
    override fun convert(
        source: Any?,
        sourceType: TypeDescriptor,
        targetType: TypeDescriptor
    ): Any? {
        return ULID.fromBytes(source as ByteArray)
    }
}
package com.astrum.data.converter

import com.astrum.data.annotation.ConverterScope
import com.astrum.ulid.ULID
import org.springframework.core.convert.TypeDescriptor
import org.springframework.core.convert.converter.GenericConverter
import org.springframework.data.convert.WritingConverter
import org.springframework.stereotype.Component

@Component
@WritingConverter
@ConverterScope(ConverterScope.Type.NEO4J)
class ULIDToBinaryConverter : GenericConverter{
    override fun getConvertibleTypes(): MutableSet<GenericConverter.ConvertiblePair>? {
        return mutableSetOf(GenericConverter.ConvertiblePair(ULID::class.java, ByteArray::class.java))
    }

    override fun convert(
        source: Any?,
        sourceType: TypeDescriptor,
        targetType: TypeDescriptor
    ): Any? {
        return (source as ULID).toBytes()
    }
}

I have the configuration class where the Bean for conversions is created.

package com.astrum.data.configuration

import com.astrum.data.annotation.ConverterScope
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.convert.converter.GenericConverter
import org.springframework.data.convert.ReadingConverter
import org.springframework.data.convert.WritingConverter
import org.springframework.data.neo4j.core.convert.Neo4jConversions
import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepositories

@Configuration
@EnableReactiveNeo4jRepositories("com.astrum")
class Neo4jConfiguration (
    private val applicationContext: ApplicationContext
){

    @Bean
    fun neo4jConversions(): Neo4jConversions {
        val converters = applicationContext.getBeansOfType(GenericConverter::class.java)
            .values
            .filter { it.javaClass.annotations.any { annotation -> annotation is WritingConverter || annotation is ReadingConverter } }
            .filter {
                val scope = it.javaClass.annotations.filterIsInstance<ConverterScope>()
                scope.isEmpty() || scope.any { converterScope -> converterScope.type == ConverterScope.Type.NEO4J }
            }
        return Neo4jConversions(converters)
    }
}

Add the lines you mentioned to support entity scanning.

    override fun setUp() {
        driver =
            GraphDatabase.driver("bolt://localhost:7687", AuthTokens.basic("neo4j", "password"));
        reactiveNeo4jClient = ReactiveNeo4jClient.create(driver)
        val graphDb: GraphDatabaseService = managementService.database(DEFAULT_DATABASE_NAME)

        val ctx = Neo4jMappingContext.builder().build()
        ctx.setInitialEntitySet(Neo4jEntityScanner.get().scan("com.astrum.data.entity"))
        reactiveNeo4jTemplate =
            ReactiveNeo4jTemplate(reactiveNeo4jClient, ctx)
        if (graphDb.isAvailable) {
            graphDb.executeTransactionally("MATCH (n) DETACH DELETE n")
        }
    }

This is the complete class.

package com.astrum.data.test

import org.neo4j.configuration.GraphDatabaseSettings
import org.neo4j.configuration.GraphDatabaseSettings.DEFAULT_DATABASE_NAME
import org.neo4j.dbms.api.DatabaseManagementService
import org.neo4j.dbms.api.DatabaseManagementServiceBuilder
import org.neo4j.driver.AuthTokens
import org.neo4j.driver.Driver
import org.neo4j.driver.GraphDatabase
import org.neo4j.graphdb.GraphDatabaseService
import org.springframework.data.neo4j.config.Neo4jEntityScanner
import org.springframework.data.neo4j.core.ReactiveNeo4jClient
import org.springframework.data.neo4j.core.ReactiveNeo4jTemplate
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext
import java.io.File
import java.time.Duration
import kotlin.io.path.createTempDirectory

class Neo4jTestHelper : ResourceTestHelper {
    lateinit var reactiveNeo4jTemplate: ReactiveNeo4jTemplate
    private lateinit var driver: Driver
    private lateinit var reactiveNeo4jClient: ReactiveNeo4jClient
    private val databaseDirectory = createTempDirectory("neo4j-test-")
    private val managementService: DatabaseManagementService =
        DatabaseManagementServiceBuilder(databaseDirectory)
            .setConfig(GraphDatabaseSettings.transaction_timeout, Duration.ofSeconds(60))
            .setConfig(GraphDatabaseSettings.preallocate_logical_logs, true).build()

    override fun setUp() {
        driver =
            GraphDatabase.driver("bolt://localhost:7687", AuthTokens.basic("neo4j", "password"));
        reactiveNeo4jClient = ReactiveNeo4jClient.create(driver)
        val graphDb: GraphDatabaseService = managementService.database(DEFAULT_DATABASE_NAME)

        val ctx = Neo4jMappingContext.builder().build()
        ctx.setInitialEntitySet(Neo4jEntityScanner.get().scan("com.astrum.data.entity"))
        reactiveNeo4jTemplate =
            ReactiveNeo4jTemplate(reactiveNeo4jClient, ctx)
        if (graphDb.isAvailable) {
            graphDb.executeTransactionally("MATCH (n) DETACH DELETE n")
        }
    }

    override fun tearDown() {
        managementService.shutdown()
        // delete the database directory and all its contents recursively (if it exists) after the test
        deleteRecursively(databaseDirectory.toFile())
    }

    private fun deleteRecursively(file: File) {
        if (file.isDirectory) {
            val files = file.listFiles()
            if (files != null) {
                for (child in files) {
                    deleteRecursively(child)
                }
            }
        }
        if (!file.delete()) {
            throw RuntimeException("Failed to delete file: $file")
        }
    }
}

Unfortunately, I'm still having the same problem and the same exception.

The attribute 'id' is not mapped to a Graph property
org.springframework.data.mapping.MappingException: The attribute 'id' is not mapped to a Graph property
	at app//org.springframework.data.neo4j.core.mapping.DefaultNeo4jPersistentProperty.getPropertyName(DefaultNeo4jPersistentProperty.java:261)
	at app//org.springframework.data.neo4j.core.mapping.DefaultNeo4jPersistentEntity.computeIdDescription(DefaultNeo4jPersistentEntity.java:420)
	at app//org.springframework.data.util.Lazy.getNullable(Lazy.java:245)
	at app//org.springframework.data.neo4j.core.mapping.DefaultNeo4jPersistentEntity.getIdDescription(DefaultNeo4jPersistentEntity.java:154)
	at app//org.springframework.data.neo4j.core.mapping.DefaultNeo4jPersistentEntity.verifyIdDescription(DefaultNeo4jPersistentEntity.java:217)
	at app//org.springframework.data.neo4j.core.mapping.DefaultNeo4jPersistentEntity.verify(DefaultNeo4jPersistentEntity.java:204)
	at app//org.springframework.data.mapping.context.AbstractMappingContext.doAddPersistentEntity(AbstractMappingContext.java:425)
	at app//org.springframework.data.mapping.context.AbstractMappingContext.addPersistentEntity(AbstractMappingContext.java:379)
	at app//org.springframework.data.neo4j.core.mapping.Neo4jMappingContext.addPersistentEntity(Neo4jMappingContext.java:364)
	at app//org.springframework.data.mapping.context.AbstractMappingContext.getPersistentEntity(AbstractMappingContext.java:280)
	at app//org.springframework.data.neo4j.core.mapping.Neo4jMappingContext.getPersistentEntity(Neo4jMappingContext.java:354)
	at app//org.springframework.data.neo4j.core.mapping.Neo4jMappingContext.getPersistentEntity(Neo4jMappingContext.java:82)
	at app//org.springframework.data.mapping.context.AbstractMappingContext.getPersistentEntity(AbstractMappingContext.java:206)
	at app//org.springframework.data.neo4j.core.mapping.Neo4jMappingContext.createPersistentEntity(Neo4jMappingContext.java:297)
	at app//org.springframework.data.neo4j.core.mapping.Neo4jMappingContext.createPersistentEntity(Neo4jMappingContext.java:82)
	at app//org.springframework.data.mapping.context.AbstractMappingContext.doAddPersistentEntity(AbstractMappingContext.java:403)
	at app//org.springframework.data.mapping.context.AbstractMappingContext.addPersistentEntity(AbstractMappingContext.java:379)
	at app//org.springframework.data.neo4j.core.mapping.Neo4jMappingContext.addPersistentEntity(Neo4jMappingContext.java:364)
	at app//org.springframework.data.mapping.context.AbstractMappingContext.getPersistentEntity(AbstractMappingContext.java:280)
	at app//org.springframework.data.neo4j.core.mapping.Neo4jMappingContext.getPersistentEntity(Neo4jMappingContext.java:354)
	at app//org.springframework.data.neo4j.core.mapping.Neo4jMappingContext.getPersistentEntity(Neo4jMappingContext.java:82)
	at app//org.springframework.data.mapping.context.AbstractMappingContext.getPersistentEntity(AbstractMappingContext.java:206)
	at app//org.springframework.data.neo4j.core.mapping.Neo4jMappingContext.createPersistentEntity(Neo4jMappingContext.java:297)
	at app//org.springframework.data.neo4j.core.mapping.Neo4jMappingContext.createPersistentEntity(Neo4jMappingContext.java:82)
	at app//org.springframework.data.mapping.context.AbstractMappingContext.doAddPersistentEntity(AbstractMappingContext.java:403)
	at app//org.springframework.data.mapping.context.AbstractMappingContext.addPersistentEntity(AbstractMappingContext.java:379)
	at app//org.springframework.data.neo4j.core.mapping.Neo4jMappingContext.addPersistentEntity(Neo4jMappingContext.java:364)
	at app//org.springframework.data.mapping.context.AbstractMappingContext.getPersistentEntity(AbstractMappingContext.java:280)
	at app//org.springframework.data.neo4j.core.mapping.Neo4jMappingContext.getPersistentEntity(Neo4jMappingContext.java:354)
	at app//org.springframework.data.neo4j.core.mapping.Neo4jMappingContext.getPersistentEntity(Neo4jMappingContext.java:82)
	at app//org.springframework.data.mapping.context.AbstractMappingContext.getPersistentEntity(AbstractMappingContext.java:206)
	at app//org.springframework.data.mapping.context.AbstractMappingContext.getPersistentEntity(AbstractMappingContext.java:92)
	at app//org.springframework.data.mapping.context.MappingContext.getRequiredPersistentEntity(MappingContext.java:74)
	at app//org.springframework.data.neo4j.core.ReactiveNeo4jTemplate.saveImpl(ReactiveNeo4jTemplate.java:393)
	at app//org.springframework.data.neo4j.core.ReactiveNeo4jTemplate.save(ReactiveNeo4jTemplate.java:318)
	at app//com.astrum.data.repository.neo4j.migration.CreatePerson.up(CreatePerson.kt:13)
	at app//com.astrum.data.migration.MigrationManager$up$2.invokeSuspend(MigrationManager.kt:66)
	at app//kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at app//kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
	at [email protected]/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539)
	at [email protected]/java.util.concurrent.FutureTask.run(FutureTask.java:264)
	at [email protected]/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304)
	at [email protected]/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
	at [email protected]/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
	at [email protected]/java.lang.Thread.run(Thread.java:833)

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Feb 21, 2023
@yacosta738
Copy link
Author

@michael-simons , I still have the same problem I told you about. Now I suspect that the issue may be the ULID conversion, which causes it to not be recognized as an ID. I have given you access to my private project so that you can help me find the problem.

yacosta738/astrum#17

@michael-simons
Copy link
Collaborator

Please provide the issue in isolation, I don't have the cycles to make my way through a whole application. As far as I can tell, this is a generated application which doesn't help spotting the the issue.

We have explicit support for custom id types:

https://github.com/spring-projects/spring-data-neo4j/blob/main/src/test/java/org/springframework/data/neo4j/integration/conversion_imperative/CustomTypesIT.java#L256

https://github.com/spring-projects/spring-data-neo4j/blob/main/src/test/java/org/springframework/data/neo4j/integration/shared/conversion/PersonWithCustomId.java

@michael-simons
Copy link
Collaborator

michael-simons commented Feb 22, 2023

This won't be picked up by SDN… And you already indicate that by your custom scope annotation…

package com.astrum.data.converter

import com.astrum.data.annotation.ConverterScope
import com.astrum.ulid.ULID
import org.springframework.core.convert.converter.Converter
import org.springframework.data.convert.ReadingConverter
import org.springframework.stereotype.Component

@Component
@ReadingConverter
@ConverterScope(ConverterScope.Type.R2DBC)
class BytesToULIDConverter : Converter<ByteArray, ULID> {
    override fun convert(source: ByteArray): ULID {
        return ULID.fromBytes(source)
    }
}

I guess. this should work:

package com.astrum.data.configuration

import com.astrum.ulid.ULID
import org.neo4j.driver.Driver
import org.neo4j.driver.Value
import org.neo4j.driver.Values
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.convert.TypeDescriptor
import org.springframework.core.convert.converter.GenericConverter
import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider
import org.springframework.data.neo4j.core.convert.Neo4jConversions
import org.springframework.data.neo4j.core.transaction.ReactiveNeo4jTransactionManager
import org.springframework.data.neo4j.repository.config.EnableReactiveNeo4jRepositories
import org.springframework.data.neo4j.repository.config.ReactiveNeo4jRepositoryConfigurationExtension
import org.springframework.transaction.ReactiveTransactionManager

@Configuration
@EnableReactiveNeo4jRepositories("com.astrum")
class DatabaseConfiguration {
    @Bean(ReactiveNeo4jRepositoryConfigurationExtension.DEFAULT_TRANSACTION_MANAGER_BEAN_NAME)
    fun transactionManager(
        driver: Driver,
        databaseNameProvider: ReactiveDatabaseSelectionProvider
    ): ReactiveTransactionManager {
        return ReactiveNeo4jTransactionManager(driver, databaseNameProvider)
    }

    @Bean
    fun neo4jConversions(): Neo4jConversions? {

        val f = object: GenericConverter {
            override fun getConvertibleTypes(): Set<GenericConverter.ConvertiblePair>? {
                return setOf(
                        GenericConverter.ConvertiblePair(
                            ULID::class.java,
                            Value::class.java
                        ),
                        GenericConverter.ConvertiblePair(
                            Value::class.java,
                            ULID::class.java
                        )
                )
            }

            override fun convert(source: Any?, sourceType: TypeDescriptor, targetType: TypeDescriptor): Any? {
                if (source == null) {
                    return null
                }


                return if (ULID::class.java.isAssignableFrom(sourceType.getType())
                ) {

                    Values.value((source as ULID).toBytes())
                } else {
                    ULID.fromBytes((source as Value).asByteArray())
                }
            }

        }
        return Neo4jConversions(setOf(f))
    }
}

@yacosta738
Copy link
Author

The problem was that the configuration where the converters are set was not being loaded in the tests, and since I created a ReativeNeo4jTemplate in that test, the converters were not loaded. Now, my problem is that the render is null and I'm not understanding very well how to create that render. I have doubts about how to construct the render that the ReativeNeo4jTemplate uses.

override fun setUp() {
        driver =
            GraphDatabase.driver("bolt://localhost:7687", AuthTokens.basic("neo4j", "password"))
        reactiveNeo4jClient = ReactiveNeo4jClient.create(driver)
        val graphDb: GraphDatabaseService = managementService.database(DEFAULT_DATABASE_NAME)

        val ctx = Neo4jMappingContext.builder()
            .withNeo4jConversions(Neo4jConversions(setOf(ULIDToValueConverter())))
            .build()
        ctx.setInitialEntitySet(Neo4jEntityScanner.get().scan("com.astrum.data.entity"))

        reactiveNeo4jTemplate =
            ReactiveNeo4jTemplate(reactiveNeo4jClient, ctx)
        if (graphDb.isAvailable) {
            graphDb.executeTransactionally("MATCH (n) DETACH DELETE n")
        }
    }

Screenshot 2023-02-23 at 09 42 51

Cannot invoke "org.neo4j.cypherdsl.core.renderer.Renderer.render(org.neo4j.cypherdsl.core.Statement)" because "this.renderer" is null java.lang.NullPointerException: Cannot invoke "org.neo4j.cypherdsl.core.renderer.Renderer.render(org.neo4j.cypherdsl.core.Statement)" because "this.renderer" is null at org.springframework.data.neo4j.core.ReactiveNeo4jTemplate.lambda$saveImpl$8(ReactiveNeo4jTemplate.java:414) at reactor.core.publisher.MonoSupplier$MonoSupplierSubscription.request(MonoSupplier.java:126) at reactor.core.publisher.MonoZip$ZipInner.onSubscribe(MonoZip.java:466) at reactor.core.publisher.MonoSupplier.subscribe(MonoSupplier.java:48) at reactor.core.publisher.MonoZip$ZipCoordinator.request(MonoZip.java:216) at reactor.core.publisher.MonoFlatMapMany$FlatMapManyMain.onSubscribe(MonoFlatMapMany.java:141) ....

@michael-simons
Copy link
Collaborator

Oh you are using the ReactiveNeo4jTemplate completely outside a Spring context… This is not something that we actively support, really. It shouldn't throw, but still… You won't have transactions or anything (like Spel support). This whole setup is doomed to fail, really…

But I consider the original issue invalid.

@michael-simons
Copy link
Collaborator

I will add a proper default value to both templates, but this is not an endorsement for the way you are running the tests.

Until I added the fix, this is a workaround for your test, but again, not really recommended:

GenericApplicationContext ctx = new GenericApplicationContext();
ctx.refresh();
reactiveNeo4jTemplate.setBeanFactory(ctx);

@michael-simons michael-simons added status: invalid An issue that we don't feel is valid status: works-as-expected An issue that is not valid as the error or outcome is expected. and removed status: feedback-provided Feedback has been provided status: invalid An issue that we don't feel is valid labels Feb 23, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: works-as-expected An issue that is not valid as the error or outcome is expected.
Projects
None yet
Development

No branches or pull requests

3 participants