Neo4J

Description

Neo4J is a graph database written in Java

Motivation

To find a setup, which let us easily interact with graph data, we choose this kind of NoSQL database

Setup

  • install Neo4J Desktop: https://neo4j.com/download/
  • This will be our playground, the desktop variant brings us some extra tools, later we create a virtual environment with Vagrant and a Docker file, to roll out anything necessary, to run our program.
  • First, we set a password. There are several ways that didn’t work for me, I deleted the database, created a new one via Desktop app and set password to „secret“ – now start and visit http://localhost:7474/browser/, username is neo4j
  • Here we find a good explaination, how to setup Neo4J with Spring, we’ll follow this: [1]. Maybe have also a look at a demo project [2]
  • In build.gradle.kts we remove h2 database and spring-boot-starter-data-jpa and add
implementation("org.springframework.boot:spring-boot-starter-data-neo4j")
  • In application.yml we replace the h2 block with
data.neo4j:
uri: bolt://localhost:7687
username: neo4j
password: secret
  • Replace the Model.kt – here we have the challenge that the model is synced with the scheme on one hand and we have to annotate it for Neo4J on the other hand
package de.janni.backend
import org.neo4j.ogm.annotation.*
import org.springframework.data.repository.CrudRepository

enum class ParagraphType {
    PlainText,
    Snippet,
    Formula
}

@NodeEntity
data class Tag (
        @Id
        var name : String = "",

        @Relationship(type="HAS_TAGS", direction = Relationship.OUTGOING)
        var paragraphs: MutableSet<Paragraph>? = null
)

@NodeEntity
data class Paragraph (
        var content: String = "",

        var type : ParagraphType? = null,

        @Relationship(type="HAS_TAGS", direction = Relationship.INCOMING)
        var tags: MutableSet<Tag>? = null,

        @Id @GeneratedValue
        var id: Long = -1L
)

@NodeEntity
data class Section (
        var name : String = "",

        @Relationship(type="SUBPARAGRAPHS")
        var paragraphs: MutableSet<Paragraph>? = null,

        @Relationship(type="SUBSECTIONS")
        var subsections: MutableSet<Section>? = null,

        @Id @GeneratedValue
        var id: Long = -1L
)

interface TagRepository : CrudRepository<Tag, String>
interface SectionRepository : CrudRepository<Section, Long>
interface ParagraphRepository : CrudRepository<Paragraph, Long>
  • The CRUD Repositories mainly staying the same, except that our model of paragraphs now doesnt contain a sectionId anymore.
  • The Resolver changed also a little bit in GraphQL (the code looks a bit odd for now)
package de.janni.backend

import graphql.kickstart.tools.GraphQLMutationResolver
import graphql.kickstart.tools.GraphQLQueryResolver
import graphql.kickstart.tools.GraphQLResolver

class Query(val sectionRepository: SectionRepository,
            val paragraphRepository: ParagraphRepository,
            val tagRepository: TagRepository)
    : GraphQLQueryResolver {

    fun getSections(): Iterable<Section> {
        return sectionRepository.findAll()
    }

    fun getTags(): Iterable<Tag> {
        return tagRepository.findAll()
    }
}

class Mutation(val sectionRepository: SectionRepository,
               val paragraphRepository: ParagraphRepository,
               val tagRepository: TagRepository)
    : GraphQLMutationResolver {

    fun addSection(parentId: String, name: String): Section? {
        val parentIdLong = parentId.toLongOrNull()
        val parent =
            if (parentIdLong != null){
                sectionRepository.findById(parentIdLong).orElse(null)
            } else {
                null
            }

        val section = Section(name)

        return if (parent != null){
            val subsection = parent.subsections
            if (subsection == null){
                parent.subsections = mutableSetOf(section)
            } else {
                subsection.add(section)
            }
            sectionRepository.save(parent)
        } else {
            sectionRepository.save(section);
        }
    }

    fun addParagraph(parentId: String, type: ParagraphType, content: String): Paragraph? {
        return null;
    }

    fun addTag(paragraphId: String, name: String): Tag? {
        return null;
    }

    fun removeSection(sectionId: String): Section? {
        return null;
    }

    fun removeParagraph(paragaphId: String): Paragraph? {
        return null;
    }

    fun removeTag(name: String): Tag? {
        return null;
    }
}

class ParagraphResolver(val paragraphRepository: ParagraphRepository)
    : GraphQLResolver<Paragraph> {

    fun getParagraphs(section: Section): Iterable<Paragraph> {
        val paragraph = section.paragraphs
        return if (paragraph == null) {
            listOf()
        } else {
            paragraph.asIterable()
        }
    }
}
  • BackendApplication
package de.janni.backend

import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.CommandLineRunner
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;

val LOG = LoggerFactory.getLogger(BackendApplication::class.java)

@SpringBootApplication
@EnableNeo4jRepositories
class BackendApplication : CommandLineRunner{

	@Autowired
	lateinit var sectionRepository: SectionRepository

	@Autowired
	lateinit var paragraphRepository: ParagraphRepository

	@Autowired
	lateinit var tagRepository: TagRepository

	// initialise with data
	override fun run(args: Array<String>) {

		LOG.info("Start");
		val tinput = Tag("Tag 1")
		val toutput = tagRepository.save(tinput)
		LOG.info("Persisted Tag: {}", toutput);

		val pinput = Paragraph("Content 1", ParagraphType.PlainText)
		pinput.tags = mutableSetOf(toutput)
		val presult = paragraphRepository.save(pinput)
		LOG.info("Persisted Paragraph: {}", presult);

		val s2input = Section("SubSection1")
		s2input.paragraphs = mutableSetOf(presult)
		val s2result = sectionRepository.save(s2input)
		LOG.info("Persisted Section: {}", s2result);

		val s1input = Section("Section1")
		s1input.subsections = mutableSetOf(s2result)
		val s1result = sectionRepository.save(s1input)
		LOG.info("Persisted Section: {}", s1result);

		val pqueried = sectionRepository.findById(s2result.id)

		LOG.info("Queried Paragraph: {}", pqueried);
	}

	@Bean
	fun paragraphResolver(): ParagraphResolver {
		return ParagraphResolver(paragraphRepository)
	}

	@Bean
	fun query(): Query {
		return Query(sectionRepository, paragraphRepository, tagRepository)
	}

	@Bean
	fun mutation(): Mutation {
		return Mutation(sectionRepository, paragraphRepository, tagRepository)
	}

}

fun main(args: Array<String>) {
	runApplication<BackendApplication>(*args)
}

Observations

  • gradlew bootRun
  • we saw, the order of initialization has changed in the CommandLineRunner
  • the data model is simpler
  • but (for me) it’s not yet clear, if the complete model is kind of stored at once. At least in Neo4J this looks like
{
  getSections {
    id
    name
    subsections {
      id
      name
    }
  }
}
  • The result is like: section 1 with subsection 2 AND subsection 2 … interesting, so the query requires kind of a root probably, but totally in the area of what we expected
  • In a productive environment, you would probably write two models, one for the database and one for the frontend, then write an application service that collects the data from the repository/repositories, handle exceptions and perform business operations like sum up things. For this case a graphql -> Java code generator would be nice, probably exists yet.
  • There seems also an option, to use Neo4J raw as GraphQL database without custom interceptions, which is maybe more a usecase for read-only applications
  • Keep in mind a) we don’t have login yet, b) the configuration of spring could be secured by externalizing
  • So driven by a schema.graphql file, we now have defined a frontend and a backend, which is pretty close to start the actual work on both 😀

References


Beitrag veröffentlicht

in

von

Schlagwörter: