Description
Spring (Boot) is a Framwork for Java/Kotlin to create Webservers
Motivation
Caring about the backend, Spring is a convenient way of doing this. We’ll focus on a less mocked GraphQL API now. Sure we want to use a Neo4J Graphdatabase later on, but currently we follow this example with an H2 Database (relational, in-memory): https://github.com/eh3rrera/graphql-java-spring-boot-example
To be more modern, we’ll use Kotlin instead of Java.
Setup
- Java is a requirement, especially a JDK [1]
- I would suggest IntelliJ as IDE, but you can use others too [2]
- Instead of building on our own, we could potentially use a „kickstart“ from here [3]
- But we’ll start from the ground with Spring Initializr [4] with Gradle & Kotlin
- Click Generate, download & extract
- At first, we’ll add some dependencies in build.gradle.kts, result looks like
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id("org.springframework.boot") version "2.2.6.RELEASE" id("io.spring.dependency-management") version "1.0.9.RELEASE" kotlin("jvm") version "1.3.71" kotlin("plugin.spring") version "1.3.71" } group = "de.janni" version = "0.0.1-SNAPSHOT" java.sourceCompatibility = JavaVersion.VERSION_1_8 val developmentOnly by configurations.creating configurations { runtimeClasspath { extendsFrom(developmentOnly) } } repositories { jcenter() mavenCentral() } dependencies { // find recent versions via // https://mvnrepository.com developmentOnly("org.springframework.boot:spring-boot-devtools") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("com.graphql-java-kickstart:graphql-spring-boot-starter:7.0.1") runtimeOnly("com.graphql-java-kickstart:graphiql-spring-boot-starter:7.0.1") // implementation("com.graphql-java-kickstart:graphql-java-tools:6.0.2") implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-actuator") // database runtimeOnly("com.h2database:h2") testImplementation("org.springframework.boot:spring-boot-starter-test") { exclude(group = "org.junit.vintage", module = "junit-vintage-engine") } } tasks.withType<Test> { useJUnitPlatform() } tasks.withType<KotlinCompile> { kotlinOptions { freeCompilerArgs = listOf("-Xjsr305=strict") jvmTarget = "1.8" } }
- Now call: gradlew bootRun which will download dependencies and start de server – take a break for a coffee 😉
- (with gradlew tasks you get a summary of available actions)
- Merge the project .gitignore with the parent one by appending – duplicates are not relevant so far – we could also keep this per project, but I don’t like redundances.
Configure
spring: application: name: graphql-app h2.console: enable: true path: /h2-console server: port: 9000 logging.level: graphql.servlet: debug web: debug graphql: servlet: mapping: /graphql enabled: true corsEnabled: true cors: allowed-origins: http://some.domain.com # if you want to @ExceptionHandler annotation for custom GraphQLErrors exception-handlers-enabled: true contextSetting: PER_REQUEST_WITH_INSTRUMENTATION tracing-enabled: true tools: schema-location-pattern: "**/*.graphqls" # Enable or disable the introspection query. Disabling it puts your server in contravention of the GraphQL # specification and expectations of most clients, so use this option with caution introspection-enabled: true actuator-metrics: true graphiql: mapping: /graphiql endpoint: graphql: /graphql subscriptions: /subscriptions subscriptions: timeout: 30 reconnect: false static: basePath: / enabled: true pageTitle: GraphiQL management: endpoints: web: exposure: include=*:
- the example project contains a graphql folder, so we copy our scheme there and rename to schema.graphqls (damn, this is redundant)
- At least IntelliJ suggests us to install a fitting plugin
- The entry point of the application is the (in my case) BackendApplication.kt
- The main purpose of the backend, is to process an API call, by fetching data from a database, „do some stuff“ with it and return a result. Sounds simple, usually a repeating task.
- SpringBoot/Java/Kotlin uses annotations to „describe“ how this data is stored f.e. we’ll try to keep this as simple as possible
- One of the main challenges is to fit the data model to the API as well to the Database, as you can see in example for JPA [5]
Data Model
Usually a model is very complex, so one should apply an architecture around and separate „domain“ from „persistence“ layer kind of. We take a shortcut here for our Model.kt
package de.janni.backend import org.springframework.data.repository.CrudRepository import javax.persistence.* enum class ParagraphType { PlainText, Snippet, Formula } @Entity data class Tag ( @Id @Column(name = "id", nullable = false) var name : String = "", @ManyToMany(mappedBy = "tags", cascade = arrayOf( CascadeType.PERSIST, CascadeType.MERGE)) var paragraphs: MutableSet<Paragraph>? = null ) @Entity data class Paragraph ( @Column(nullable = false) var content: String = "", @Enumerated(EnumType.STRING) var type : ParagraphType? = null, @ManyToOne var section: Section? = null, @ManyToMany var tags: Set<Tag>? = null, @Id @GeneratedValue(strategy=GenerationType.AUTO) var id: Long = -1L ) @Entity data class Section ( @Column(nullable = false) var name : String = "", @ManyToOne var parent: Section? = null, @OneToMany(mappedBy = "section", orphanRemoval = true, cascade = arrayOf(CascadeType.ALL)) var paragraphs: Set<Paragraph>? = null, @OneToMany(mappedBy = "parent", orphanRemoval = true, cascade = arrayOf(CascadeType.ALL)) var subsections: Set<Section>? = null, @Id @GeneratedValue(strategy=GenerationType.AUTO) var id: Long = -1L ) interface TagRepository : CrudRepository<Tag, String> interface SectionRepository : CrudRepository<Section, Long> interface ParagraphRepository : CrudRepository<Paragraph, Long> { fun findBySectionId(id : Long) : List<Paragraph> }
GraphQL.kt
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 longId = parentId.toLong() val parent = sectionRepository.findById(longId).orElse(null) val section = Section(name, parent) sectionRepository.save(section) return 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) : List<Paragraph> { return paragraphRepository.findBySectionId(section.id) } }
BackendApplication.kt
package de.janni.backend import graphql.Scalars import graphql.schema.GraphQLObjectType import graphql.schema.GraphQLSchema 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 javax.transaction.Transactional val LOG = LoggerFactory.getLogger(BackendApplication::class.java) @SpringBootApplication class BackendApplication : CommandLineRunner{ @Autowired lateinit var sectionRepository: SectionRepository @Autowired lateinit var paragraphRepository: ParagraphRepository @Autowired lateinit var tagRepository: TagRepository // initialise with data @Transactional 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 s1input = Section("Section1") val s1result = sectionRepository.save(s1input) LOG.info("Persisted Section: {}", s1result); val s2input = Section("SubSection1", s1result) val s2result = sectionRepository.save(s2input) LOG.info("Persisted Section: {}", s2result); val pinput = Paragraph("Content 1", ParagraphType.PlainText, s2result, setOf(toutput)) val presult = paragraphRepository.save(pinput) LOG.info("Persisted Paragraph: {}", presult); val pqueried = paragraphRepository.findBySectionId(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) }
And because the schema gets validated against the actual code, we have to update schema.graphqls
enum ParagraphType { PlainText, Snippet, Formula } "This is a tag" type Tag { name : String } "This is a paragraph" type Paragraph { id: String! type: ParagraphType! content: String! tags: [Tag!] } "This is a section" type Section { id: String! name: String subsections: [Section!] paragraphs: [Paragraph!] } type Query { getSections: [Section] getTags: [Tag] } type Mutation { addSection(parentId: String!, name: String!) : Section addParagraph(parentId: String!, type: ParagraphType!, content: String!) : Paragraph addTag(paragraphId: String!, name : String!) : Tag removeSection(sectionId : String!) : Section removeParagraph(paragaphId : String!) : Paragraph removeTag(name : String!) : Tag } schema { query : Query mutation : Mutation }
- call gradlew.bat bootRun
Observations
- after a lot of (invisible iterations) about why Bean in the root namespace were not working via Kotlin and other things like „something“ is not in sync with the scheme and the implemented resolvers, we reached our goal.
- The constructors of the model class now has 3 different uses: 1. deserialized data from JSON (needs an empty constructor), 2. data transfer object from database (needs a full constructor or getter/setter), 3. the actual domain (a „business“ constructor f.e. without ids) – one could write separate classes for each layer and then map them. Because nullable data – depending on usecases – are a source of error.
- Most of the Annotations in the model are related to JPA and also make a lot of trouble often [6]
- The „CommandLineRunner“ in BackendApplications prefills the in-memory database with actions on the Repositories, we defined. These small interface definitions (see bottom of model) building up a full „Create-Read-Update-Delete“ ready-to-use service [7]
- Visit http://localhost:9000/h2-console/login.jsp & login with:
JDBC URL: jdbc:h2:mem:testdb
User: sa
Password empty - Now click on the sections table, a classic SQL select occurs (SELECT * FROM SECTION), click play, see result.
- Now go to http://localhost:9000/graphiql and firing up something like this
{
getSections {
id
name
}
}
- I’d call this a „breakthrough“. Now obviously we have to check if the API acts as expected and implement the rest. Since we know, it works „in principle“, it’s just a trial and error task.
- For example trying to query subsections does not work yet as expected.
- Attention: there is no security feature yet at all, also the h2 database is not persistent between restarts. We also recognize, that mapping our graph to JPA Entities – in this case just 4 kinds – becomes very complex quickly
References
- [1] https://www.oracle.com/java/technologies/javase-jdk8-downloads.html
- [2] https://www.jetbrains.com/de-de/idea/
- [3] https://github.com/graphql-java-kickstart/graphql-spring-boot
- [4] https://start.spring.io
- [5] https://thoughts-on-java.org/best-practices-many-one-one-many-associations-mappings/
- [6] https://stackoverflow.com/questions/2302802/how-to-fix-the-hibernate-object-references-an-unsaved-transient-instance-save
- [7] https://docs.spring.io/spring-data/jpa/docs/1.5.0.RELEASE/reference/html/repositories.html