Skip to content

Commit 370cda6

Browse files
authored
[generator] create Kotlin PropertyDataFetcher (#1018)
* [generator] create Kotlin PropertyDataFetcher `graphql-java` [PropertyDataFetcher](https://github.com/graphql-java/graphql-java/blob/master/src/main/java/graphql/schema/PropertyDataFetcher.java) is the default data fetcher/resolver that will automatically resolve fields on target POJOs. It does so by using reflections to find out the target getter function that needs to be invoked - it accepts a property name and attempts to find matching getter in available class methods. Since we are generating the schema directly from the source code, when we process the target property we already know which field it is so we can invoke it direcly and skip the redundant reflection logic from `graphql-java`. `graphql-java` caches those reflection lookups to avoid unncessary computations. I've run some basic benchmarks and as expected there is not much of a difference. Simple benchmark (after warmup of 100K executions -> there is a pretty big warmup hit on `graphql-java`): * per each datafetcher execute 100 iterations of 100K GraphQL query executions attempting to retrieve a property field `graphql-java` avg: 2558 ms per 100_000 executions `graphql-kotlin` avg: 2536 ms per 100_000 executions Resolves: #529 * update docs to point to new property data fetcher
1 parent 3d59a12 commit 370cda6

File tree

5 files changed

+124
-6
lines changed

5 files changed

+124
-6
lines changed

docs/schema-generator/execution/fetching-data.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ title: Fetching Data
66
Each field exposed in the GraphQL schema has a corresponding resolver (aka data fetcher) associated with it. `graphql-kotlin-schema-generator` generates the GraphQL schema
77
directly from the source code, automatically mapping all the fields either to use
88
[FunctionDataFetcher](https://github.com/ExpediaGroup/graphql-kotlin/blob/master/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/execution/FunctionDataFetcher.kt)
9-
to resolve underlying functions or the default data fetcher from graphql-java, [PropertyDataFetcher](https://www.graphql-java.com/documentation/v15/data-fetching/) to read a value from an underlying Kotlin property.
9+
to resolve underlying functions or the [PropertyDataFetcher](https://github.com/ExpediaGroup/graphql-kotlin/blob/master/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/execution/PropertyDataFetcher.kt)
10+
to read a value from an underlying Kotlin property.
1011

1112
While all the fields in a GraphQL schema are resolved independently to produce a final result, whether a field is backed by a function or a property can have significant
1213
performance repercussions. For example, given the following schema:

generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/execution/KotlinDataFetcherFactoryProvider.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019 Expedia, Inc
2+
* Copyright 2021 Expedia, Inc
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,7 +19,6 @@ package com.expediagroup.graphql.generator.execution
1919
import com.fasterxml.jackson.databind.ObjectMapper
2020
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
2121
import graphql.schema.DataFetcherFactory
22-
import graphql.schema.PropertyDataFetcher
2322
import kotlin.reflect.KClass
2423
import kotlin.reflect.KFunction
2524
import kotlin.reflect.KProperty
@@ -64,7 +63,7 @@ open class SimpleKotlinDataFetcherFactoryProvider(
6463
)
6564
}
6665

67-
override fun propertyDataFetcherFactory(kClass: KClass<*>, kProperty: KProperty<*>) = DataFetcherFactory<Any?> {
68-
PropertyDataFetcher(kProperty.name)
66+
override fun propertyDataFetcherFactory(kClass: KClass<*>, kProperty: KProperty<*>) = DataFetcherFactory {
67+
PropertyDataFetcher(kProperty.getter)
6968
}
7069
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2021 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.generator.execution
18+
19+
import graphql.schema.DataFetcher
20+
import graphql.schema.DataFetchingEnvironment
21+
import kotlin.reflect.KProperty
22+
23+
/**
24+
* Property [DataFetcher] that directly invokes underlying property getter.
25+
*
26+
* @param propertyGetter Kotlin property getter that will be invoked to resolve a field
27+
*/
28+
class PropertyDataFetcher(private val propertyGetter: KProperty.Getter<*>) : DataFetcher<Any?> {
29+
30+
/**
31+
* Invokes target getter function.
32+
*/
33+
override fun get(environment: DataFetchingEnvironment): Any? = environment.getSource<Any?>()?.let { instance ->
34+
propertyGetter.call(instance)
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2021 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.generator.execution
18+
19+
import com.expediagroup.graphql.generator.TopLevelObject
20+
import com.expediagroup.graphql.generator.testSchemaConfig
21+
import com.expediagroup.graphql.generator.toSchema
22+
import graphql.GraphQL
23+
import graphql.schema.DataFetchingEnvironment
24+
import io.mockk.every
25+
import io.mockk.mockk
26+
import org.junit.jupiter.api.Test
27+
import kotlin.test.assertEquals
28+
import kotlin.test.assertNotNull
29+
import kotlin.test.assertNull
30+
31+
class PropertyDataFetcherTest {
32+
33+
private val schema = toSchema(
34+
queries = listOf(TopLevelObject(PrefixedQuery())),
35+
config = testSchemaConfig
36+
)
37+
private val graphQL = GraphQL.newGraphQL(schema).build()
38+
39+
@Test
40+
fun `verify null source returns null`() {
41+
val dataFetcher = PropertyDataFetcher(propertyGetter = PrefixedFields::isBooleanValue.getter)
42+
val mockEnvironment: DataFetchingEnvironment = mockk {
43+
every { getSource<Any>() } returns null
44+
}
45+
assertNull(dataFetcher.get(mockEnvironment))
46+
}
47+
48+
@Test
49+
fun `verify is prefix resolution for boolean field`() {
50+
val result = graphQL.execute("{ prefixed { booleanValue isBooleanValue } }")
51+
val data: Map<String, Map<String, Any>>? = result.getData()
52+
53+
val prefixedResults = data?.get("prefixed")
54+
assertNotNull(prefixedResults)
55+
assertEquals(2, prefixedResults.size)
56+
assertEquals(true, prefixedResults["booleanValue"])
57+
assertEquals(false, prefixedResults["isBooleanValue"])
58+
}
59+
60+
@Test
61+
fun `verify properties are correctly resolved`() {
62+
val result = graphQL.execute("{ prefixed { islandName isWhatever } }")
63+
val data: Map<String, Map<String, Any>>? = result.getData()
64+
65+
val prefixedResults = data?.get("prefixed")
66+
assertNotNull(prefixedResults)
67+
assertEquals(2, prefixedResults.size)
68+
assertEquals("Iceland", prefixedResults["islandName"])
69+
assertEquals("whatever", prefixedResults["isWhatever"])
70+
}
71+
72+
class PrefixedQuery {
73+
fun prefixed() = PrefixedFields()
74+
}
75+
76+
data class PrefixedFields(
77+
val islandName: String = "Iceland",
78+
val booleanValue: Boolean = true,
79+
val isBooleanValue: Boolean = false,
80+
val isWhatever: String = "whatever"
81+
)
82+
}

generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/internal/types/GeneratePropertyTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import com.expediagroup.graphql.generator.annotations.GraphQLName
2424
import com.expediagroup.graphql.generator.directives.KotlinDirectiveWiringFactory
2525
import com.expediagroup.graphql.generator.directives.KotlinSchemaDirectiveWiring
2626
import com.expediagroup.graphql.generator.execution.KotlinDataFetcherFactoryProvider
27+
import com.expediagroup.graphql.generator.execution.PropertyDataFetcher
2728
import com.expediagroup.graphql.generator.hooks.SchemaGeneratorHooks
2829
import com.expediagroup.graphql.generator.internal.extensions.getSimpleName
2930
import com.expediagroup.graphql.generator.scalars.ID
@@ -36,7 +37,6 @@ import graphql.schema.FieldCoordinates
3637
import graphql.schema.GraphQLNamedType
3738
import graphql.schema.GraphQLNonNull
3839
import graphql.schema.GraphQLTypeUtil
39-
import graphql.schema.PropertyDataFetcher
4040
import io.mockk.every
4141
import io.mockk.mockk
4242
import io.mockk.spyk

0 commit comments

Comments
 (0)