5

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

andylamax
  • 1,858
  • 1
  • 14
  • 31
  • Can you post your Entities as well? – Jasper Blues Sep 03 '18 at 05:13
  • 1
    I have edited the question again. Entities included – andylamax Sep 03 '18 at 07:15
  • If you declare your class as `open class Actor(var name: String = "")` it should work. Alternatively you can use the all-open gradle plugin (or the kotlin-spring plugin, which is a wrapper around all-open) – Jasper Blues Sep 03 '18 at 12:56
  • @JasperBlues added it. ddnt work. it says 'Class class com.asofttz.micros.administrator.users.testmodels.Movie is not a valid entity class. Please check the entity mapping.' ps. I added the allOpen plugin – andylamax Sep 03 '18 at 13:48
  • Can you please try making the type `var name: String = ""` optional. Same for Movie class. Failing that pls zip and email to jasper@graphaware.com – Jasper Blues Sep 03 '18 at 22:25

2 Answers2

7

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.

Michael Simons
  • 4,640
  • 1
  • 27
  • 38
3

The OGM requires classes to be open and have a no-args constructor. Classes in Java exhibit these traits by default, however they do not in Kotlin.

You can mark the class as open, and add a default constructor manually, or you can use the 'no-args' and 'kotlin-spring' gradle plugins. Here is a sample application that uses Kotlin, Spring Data SDN and OGM. Note in the build file we have:

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

Which is the same as added a default constructor by hand, however:

  • Code is cleaner
  • The default constructor is specifically for libs to use at runtime, and is otherwise hidden.

As an alternative, you could use the bolt driver and map your query result manually. This is a good option for when you have custom queries for specific use cases - such as an application with high traffic and carefully tuned queries.

Here's a sample application showing the use of the bolt driver directly.

Jasper Blues
  • 28,258
  • 22
  • 102
  • 185
  • I fail to accomplish this, because the response is still the same. I have edited my question. Please note that I am not using spring. I am using ktor-server right out of the box – andylamax Sep 03 '18 at 03:53