Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 614
andylamax Avatar asked Mar 06 '23 12:03

andylamax


1 Answers

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.

like image 135
Michael Simons Avatar answered Mar 11 '23 22:03

Michael Simons