Spring – pt 1

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

  • head over to /src/main/resource
  • in application.yml add
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


Beitrag veröffentlicht

in

von

Schlagwörter: