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
- Now goto http://localhost:9000/graphiql and query
{
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 😀