Spring Data GraphQL make easy to use Spring Data with graphql-java implementations like Kotlin GraphQL, DGS. If you use Spring Data R2DBC or Spring Data JDBC directly, you have to write a lot of custom DataLoader. Spring Data GraphQL solves the N+1 problem through an internally auto-generated DataLoader.
In addition, it satisfies relay specifications such as Global Object Identification, Node, and Connection.
※ Currently, only spring-data-r2dbc is supported.
repositories {
maven("https://github.com/wickedev/graphql-jetpack/raw/deploy/maven-repo")
}
dependencies {
implementation("io.github.wickedev:spring-data-graphql-r2dbc-starter:0.5.6")
}
Example (Github)
@SpringBootApplication
@EnableGraphQLR2dbcRepositories // <-- IMPORTANT!! Use this instead of @EnableR2dbcRepositories
class Application
@Table("users")
data class User(
@Id override val id: io.github.wickedev.graphql.types.ID, // <-- not com.expediagroup.graphql.generator.scalars.ID
val name: String
) : Node {
fun posts(
last: Int?, before: ID?,
env: DataFetchingEnvironment,
@GraphQLIgnore @Autowired postRepository: PostRepository,
): CompletableFuture<PostConnect> {
return postRepository.connectionByAuthorId(id, Backward(last, before), env)
.thenApply { PostConnect(it.edges.map { e -> PostEdge(e.node, e.cursor) }, it.pageInfo) }
}
}
@Table("post")
data class Post(
@Id override val id: ID,
val title: String,
val content: String,
@Relation(User::class) val authorId: ID,
) : Node {
fun author(
env: DataFetchingEnvironment,
@GraphQLIgnore @Autowired userRepository: UserRepository,
): CompletableFuture<User?> {
return userRepository.findById(authorId, env)
}
}
@Repository
interface UserRepository : GraphQLR2dbcRepository<User>
@Repository
interface PostRepository : GraphQLR2dbcRepository<Post>
@Component
class SampleQuery(
private val userRepository: UserRepository,
private val postRepository: PostRepository,
private val nodeRepository: GraphQLNodeRepository,
) : Query {
suspend fun node(id: ID, env: DataFetchingEnvironment): Node? {
return nodeRepository.findNodeById(id, env).await()
}
fun users(last: Int?, before: ID?, env: DataFetchingEnvironment): CompletableFuture<UserConnect> {
return userRepository.connection(Backward(last, before), env)
.thenApply { UserConnect(it.edges.map { e -> UserEdge(e.node, e.cursor) }, it.pageInfo) }
}
fun posts(last: Int?, before: ID?, env: DataFetchingEnvironment): CompletableFuture<PostConnect> {
return postRepository.connection(Backward(last, before), env)
.thenApply { PostConnect(it.edges.map { e -> PostEdge(e.node, e.cursor) }, it.pageInfo) }
}
}
Will generate as follow GraphQL Schema
type User {
id: ID!
name: String!
posts(before: ID, last: Int): PostConnect!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
}
"""
... PostConnection, UserConnection type is omitted.
"""
type Query {
node(id: ID!): Node
posts(before: ID, last: Int): PostConnect!
users(before: ID, last: Int): UserConnect!
}
Did you use @EnableGraphQLR2dbcRepositories
on Application class? If set correctly, the following logs are output.
Bootstrapping Spring Data GRAPHQL-R2DBC repositories in DEFAULT mode.
Finished Spring Data repository scanning in 249 ms. Found 1 GRAPHQL-R2DBC repository interfaces.
Spring Data GraphQL conforms to
the Relay GraphQL Server Specification.
The GraphQLNodeRepository.findNodeById
method delegates to the GraphQLRepository
or GraphQLDataLoaderRepository
implementations registered as Spring Beans. Entities not registered in GraphQLRepository
, GraphQLR2dbcRepository
,
or GraphQLCrudRepository
cannot be search.
interface Node {
val id: ID
}
interface GraphQLNodeRepository : Repository<Node, ID> {
fun findNodeById(id: ID, env: DataFetchingEnvironment): CompletableFuture<Node?>
}
GraphQLDataLoaderRepository
uses an internally auto-generated data loader to perform optimized query. For example,
if findById
is called multiple times in different Spring Data implementations, SELECT * FROM tables WHERE id = $id
query will be called multiple times. However, GraphQLDataLoaderRepository
queries SELECT * FROM tables WHERE id IN ($ids)
SQL only once with ids that have been deduplicated
through DataLoader
.
interface GraphQLDataLoaderRepository<T : Node> : Repository<T, ID> {
fun findById(id: ID, env: DataFetchingEnvironment): CompletableFuture<T?>
fun findAllById(ids: Iterable<ID>, env: DataFetchingEnvironment): CompletableFuture<List<T>>
fun existsById(id: ID, env: DataFetchingEnvironment): CompletableFuture<Boolean>
}
Returns a Connection conforming to the Relay Connection Spec.
interface GraphQLDataLoaderConnectionsRepository<T : Node> : Repository<T, ID> {
fun connection(backward: Backward, env: DataFetchingEnvironment): CompletableFuture<Connection<T>>
fun connection(forward: Forward, env: DataFetchingEnvironment): CompletableFuture<Connection<T>>
}
The repository interfaces below inherit all necessary interfaces, so you can conveniently access all query methods.
interface GraphQLRepository<T : Node> :
GraphQLDataLoaderConnectionsRepository<T>,
GraphQLDataLoaderRepository<T>
// for spring-data-r2dbc
interface GraphQLR2dbcRepository<T : Node> :
GraphQLRepository<T>,
R2dbcRepository<T, ID>
// for spring-data-jdbc (not yet support)
interface GraphQLCrudRepository<T : Node> :
GraphQLRepository<T>,
CrudRepository<T, ID>
interface PersonRepository : GraphQLRepository<Person> {
// Enabling GraphQL relay connection query
fun connectionByLastname(
lastname: String,
backward: Backward,
env: DataFetchingEnvironment
): CompletableFuture<Connection<Post>>
fun findByEmailAddressAndLastname(
emailAddress: EmailAddress,
lastname: String,
env: DataFetchingEnvironment
): CompletableFuture<List<Person>>
// Enables the distinct flag for the query
fun findDistinctPeopleByLastnameOrFirstname(
lastname: String,
firstname: String,
env: DataFetchingEnvironment
): CompletableFuture<List<Person>>
fun findPeopleDistinctByLastnameOrFirstname(
lastname: String,
firstname: String,
env: DataFetchingEnvironment
): CompletableFuture<List<Person>>
// Enabling ignoring case for an individual property
fun findByLastnameIgnoreCase(
lastname: String,
env: DataFetchingEnvironment
): CompletableFuture<List<Person>>
// Enabling ignoring case for all suitable properties
fun findByLastnameAndFirstnameAllIgnoreCase(
lastname: String,
firstname: String,
env: DataFetchingEnvironment
): CompletableFuture<List<Person>>
// Enabling static ORDER BY for a query
fun findByLastnameOrderByFirstnameAsc(
lastname: String,
env: DataFetchingEnvironment
): CompletableFuture<List<Person>>
fun findByLastnameOrderByFirstnameDesc(
lastname: String,
env: DataFetchingEnvironment
): CompletableFuture<List<Person>>
}
WARNING! THE FEATURES BELOW ARE NOT YET SUPPORTED.
Query and return according to the input convention. Inspired by TypeGraphQL Prisma.
type Query {
user(where: UserWhereUniqueInput!): User
users(
where: UserWhereInput
orderBy: [UserOrderByInput!]
cursor: UserWhereUniqueInput
take: Int
skip: Int
distinct: [UserScalarFieldEnum!]
): [User!]!
}
interface GraphQLDynamicQueryRepository<T : Node> : Repository<Node, ID> {
fun findBy(env: DataFetchingEnvironment): CompletableFuture<T?>
fun findAllBy(env: DataFetchingEnvironment): CompletableFuture<List<T>>
}