Search code examples
neo4jkotlinneo4j-ogm

Neo4j - OGM throws not an Entity in Kotlin


as the tile above, I have been trying to work with neo4j-ogm and kotlin without success. If I try to persit my data, Neo4j throws an exception, "Class xxxx is not a valid Entity".

package com.asofttz.micros.administrator.users.testmodels

import org.neo4j.ogm.annotation.GeneratedValue
import org.neo4j.ogm.annotation.Id
import org.neo4j.ogm.annotation.NodeEntity
import org.neo4j.ogm.annotation.Relationship

@NodeEntity
class Actor(var name: String = "") {

    @Id
    @GeneratedValue
    open var id: Long? = null

    @Relationship(type = "ACTS_IN", direction = "OUTGOING")
    open val movies = hashSetOf<Movie>()

    fun actsIn(movie: Movie) {
        movies.add(movie)
        movie.actors.plus(this)
    }
}
@NodeEntity
class Movie(var title: String = "", var released: Int = 2000) {

    @Id
    @GeneratedValue
    open var id: Long? = null
    @Relationship(type = "ACTS_IN", direction = "INCOMING")
    open var actors = setOf<Actor>()
}

Is there a way around? Is there an Alternative to persist data to a Neo4j database with kotlin?

N:B. I am using kotlin version 1.2.60 and Neo4j-OGM v3.2.1


Update

Below is the rest of my code

import com.asofttz.micros.administrator.users.testmodels.Actor
import com.asofttz.micros.administrator.users.testmodels.Movie
import org.neo4j.ogm.config.Configuration
import org.neo4j.ogm.session.SessionFactory
import java.util.*


object Neo4j {
    val configuration = Configuration.Builder()
            .uri("bolt://localhost")
            .credentials("neo4j", "password")
            .build()

    val sessionFactory = SessionFactory(configuration, "test.movies.domain")

    fun save() {

        val session = sessionFactory.openSession()

        val movie = Movie("The Matrix", 1999)

        session.save(movie)

        val matrix = session.load(Movie::class.java, movie.id)
        for (actor in matrix.actors) {
            println("Actor: " + actor.name)
        }
    }
}

build.gradle file looks like this

apply plugin: 'kotlin'
apply plugin: 'application'
apply plugin: "org.jetbrains.kotlin.plugin.noarg"

repositories {
    jcenter()
    mavenCentral()
    maven { url "http://dl.bintray.com/kotlin/ktor" }
    maven { url "https://dl.bintray.com/kotlin/kontlinx" }
}

noArg {
    annotation("org.neo4j.ogm.annotation.NodeEntity")
    annotation("org.neo4j.ogm.annotation.RelationshipEntity")
}

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
    compile "io.ktor:ktor:$ktor_version"
    compile "io.ktor:ktor-server-netty:$ktor_version"

    compile project(":asoftlibs:micros:administrator:users:users-jvm")

    compile 'org.neo4j:neo4j-ogm-core:3.1.2'
    compile 'org.neo4j:neo4j-ogm-bolt-driver:3.1.2'
}

kotlin {
    experimental {
        coroutines "enable"
    }
}

compileKotlin {
    kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
    kotlinOptions.jvmTarget = "1.8"
}
sourceCompatibility = "1.8"

I get the class 'com.asofttz.micros.administrator.users.testmodels.Movie is not a valid entity' further help would be appreciated.

Note: I also attempted in making the movie class open with a no pram contructor, but id ddnt help either. Another attempt was to change the version of neo4j-ogm, so I tested 2.1.5, 3.0.1 and 3.1.2. No success


Solution

  • Edit: Super short answer without explanation is: In your example, you are configuring the wrong package for class scanning. You're open the session with val sessionFactory = SessionFactory(configuration, "test.movies.domain") but it needs to be val sessionFactory = SessionFactory(configuration, "com.asofttz.micros.administrator.users.testmodels") judging from the package declaration of your models. But in addition, please see my longer version for some best practices and explanation:

    Find the complete and working example as a Gist here: Minimal Kotlin/Gradle Example for Neo4j OGM

    Let me walk you through it:

    In build.gradle, define the No-arg compiler plugin as a build script dependency.

    buildscript {
        dependencies {
            classpath "org.jetbrains.kotlin:kotlin-noarg:1.2.51"
        }
    }
    

    And than use a noArg block to define for which classes an no-arguments constructor should be synthesized:

    noArg {
        annotation("org.neo4j.ogm.annotation.NodeEntity")
        annotation("org.neo4j.ogm.annotation.RelationshipEntity")
    }
    

    That means: All classes annotated with @NodeEntity and @RelationshipEntity should have a synthetic no-args constructor.

    I absolutely agree with Jasper that this is the better approach than defaulting all constructor parameters of your domain class, for reference, the kotlin-noarg docs:

    The no-arg compiler plugin generates an additional zero-argument constructor for classes with a specific annotation.

    The generated constructor is synthetic so it can’t be directly called from Java or Kotlin, but it can be called using reflection.

    On to the domain classes: Classes mapped by Neo4j OGM need not to be final. But we don't support final fields and as such, no pure immutable classes. This is just the way things are at the moment.

    So here are both domain classes:

    @NodeEntity
    class Actor(var name: String) {
    
        @Id
        @GeneratedValue
        var id: Long? = null
    
        @Relationship(type = "ACTS_IN", direction = "OUTGOING")
        var movies = mutableSetOf<Movie>()
    
        fun actsIn(movie: Movie) {
            movies.add(movie)
            movie.actors.add(this)
        }
    }
    
    @NodeEntity
    class Movie(var title: String, var released: Int) {
    
        @Id
        @GeneratedValue
        var id: Long? = null
        @Relationship(type = "ACTS_IN", direction = "INCOMING")
        var actors = mutableSetOf<Actor>()
    }
    

    Notice that all fields are var, not val. You can safely omit the the open keyword here. Also notice that I did remove the default parameters of the "real" business information (here, title and release-year).

    We have to take special care of the sets: I removed the explicit hashSetOf and instead use the mutableSetOf. We can than use #add to mutate the sets itself.

    If you prefer a more Kotlin idiomatic way, use setOf and make use of the fact that our attributes are not final anymore and mutate the fields itself:

    @NodeEntity
    class Actor(var name: String) {
    
        @Id
        @GeneratedValue
        var id: Long? = null
    
        @Relationship(type = "ACTS_IN", direction = "OUTGOING")
        var movies = setOf<Movie>()
    
        fun actsIn(movie: Movie) {
            movies += movie
            movie.actors += this
        }
    }
    
    @NodeEntity
    class Movie(var title: String, var released: Int) {
    
        @Id
        @GeneratedValue
        var id: Long? = null
        @Relationship(type = "ACTS_IN", direction = "INCOMING")
        var actors = setOf<Actor>()
    }
    

    Take note: In your original example, you have a statement like movie.actors.plus(this). This does not mutate the set but creates a new one, exactly like the + operator for sets does.

    On a modelling level: I personally would not map the relationship in both directions. This tends to bites you sooner or later, as much as it does in the JPA/ORM world. Map the direction you need for your logic and execute other queries for paths etc. separately.

    Please let me know if this helps. I'm closing the GH issue you created now.