Query
Feature Description
Without Jimmer's support, implementing GraphQL queries requires two types of work:
-
Query aggregate roots
This is work that needs to be done when developing any data service, whether it's a GraphQL service, REST service, or even a service using other custom protocols.
Developers only need to query and return standalone aggregate root objects without considering their association properties. So this is a relatively simple development task.
For Spring GraphQL, just add the
@QueryMapping
annotation to the query methods in controller classes. -
Query association properties and calculated properties
This is the bulk of the work required to implement GraphQL queries, requiring consideration of all current object associations and batch loading. There is some workload involved.
In other words, it is precisely because the server undertakes these responsibilities that using GraphQL feels free, convenient and powerful on the client side.
For Spring GraphQL, some schemes are provided for querying association properties, such as
When using Jimmer to implement GraphQL queries, developers only need to focus on querying aggregate roots, just ensuring that these aggregate roots have simple scalar fields. This is because
All association properties and calculated properties of entity objects are automatically mapped to GraphQL fields by Jimmer without any development.
So far, the GraphQL protocol does not support recursive queries for self-associated properties.
Therefore, it is not possible to expose functionality like recursive queries in object fetchers through GraphQL. This is a functionality sacrifice that must currently be accepted when using GraphQL.
Implement GraphQL Service
1. Simplest Implementation
- Java
- Kotlin
package com.example.business;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
...Omit other imports...
@Controller
public class BookStoreService {
private final JSqlClient sqlClient;
public BookStoreService(JSqlClient sqlClient) {
this.sqlClient = sqlClient;
}
@QueryMapping
public List<BookStore> bookStores(
@Argument @Nullable String name
) {
BookStoreTable table = Tables.BOOK_STORE_TABLE;
return sqlClient
.createQuery(table)
.where(table.name().ilikeIf(name))
.select(table)
.execute();
}
}
package com.example.business
import org.springframework.graphql.data.method.annotation.Argument
import org.springframework.graphql.data.method.annotation.QueryMapping
import org.springframework.stereotype.Controller
...Omit other imports...
@Controller
class BookStoreService(
private val sqlClient: KSqlClient
) {
@QueryMapping
fun bookStores(
@Argument name: String?
): List<BookStore> =
sqlClient
.createQuery(BookStore::class) {
where(table.name `ilike?` name)
select(table)
}
.execute()
}
Unlike previous REST services, this example does not use object fetchers, returning the simplest orphan objects (orphan objects contain neither associated properties nor calculated properties).
Although we return orphan objects here, if the client's GraphQL request contains associated objects, Jimmer will still automatically load the required associated properties.
However, there is a problem with this approach: in non-cache mode, unnecessary properties will also be queried, which has performance issues. We will introduce another approach with optimization capability later.
2. Optimize Performance
To avoid querying unnecessary non-associated properties in non-cache mode, Jimmer can create a Fetcher
object based on graphql.schema.DataFetchingEnvironment
and use this Fetcher to query the database with the object as a parameter.
In non-cache mode, not querying unnecessary non-associated properties is the essential difference between Jimmer GraphQL and other GraphQL implementations in the Java ecosystem.
- Java
- Kotlin
package com.example.business;
import org.springframework.graphql.data.method.annotation.Argument;
import graphql.schema.DataFetchingEnvironment;
import org.babyfish.jimmer.spring.graphql.DataFetchingEnvironments;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
...Omit other imports...
@Controller
public class BookStoreService {
private final JSqlClient sqlClient;
public BookStoreService(JSqlClient sqlClient) {
this.sqlClient = sqlClient;
}
@QueryMapping
public List<BookStore> bookStores(
@Argument @Nullable String name,
DataFetchingEnvironment env
) {
BookStoreTable table = Tables.BOOK_STORE_TABLE;
return sqlClient
.createQuery(table)
.where(table.name().ilikeIf(name))
.select(
table.fetch(
DataFetchingEnvironments.createQuery(
BookStore.class,
env
)
)
)
.execute();
}
}
package com.example.business
import org.springframework.graphql.data.method.annotation.Argument
import graphql.schema.DataFetchingEnvironment
import org.babyfish.jimmer.spring.graphql.toFetcher
import org.springframework.graphql.data.method.annotation.QueryMapping
import org.springframework.stereotype.Controller
...Omit other imports...
@Controller
class BookStoreService(
private val sqlClient: KSqlClient
) {
@QueryMapping
fun bookStores(
@Argument name: String?,
env: DataFetchingEnvironment
): List<BookStore> =
sqlClient
.createQuery(BookStore::class) {
where(table.name `ilike?` name)
select(
table.fetch(
env.toFetcher()
)
)
}
.execute()
}
Running Effect
In the accompanying example, the GraphQL related projects are:
Language | Example |
---|---|
Java | jimmer-examples/java/jimmer-sql-graphql |
Kotlin | jimmer-examples/kotlin/jimmer-sql-graphql-kt |
Select any project, open and start it. Finally, use the browser to visit http://localhost:8080/graphiql
Be careful, it's /graphiql, not /graphql
Execute the request:
query {
bookStores {
id
name
avgPrice ❶
books { ❷
id
name
edition
price
authors { ❸
id
firstName
fullName
gender
}
}
}
}
-
❶ Query calculated property
BookStore.avgPrice
-
❷ Query association property
BookStore.books
-
❸ Query association property
Book.authors
The return result is:
{
"data":{
"bookStores":[
{
"id":2,
"name":"MANNING",
"avgPrice":80.333333,
"books":[
{
"id":10,
"name":"GraphQL in Action",
"edition":1,
"price":80,
"authors":[
{
"id":5,
"firstName":"Samer",
"fullName":"Samer Buna",
"gender":"MALE"
}
]
},
{
"id":11,
...omitted...
},
{
"id":12,
...omitted...
}
]
},
{
"id":1,
"name":"O'REILLY",
"avgPrice":58.5,
"books":[
{
"id":4,
"name":"Effective TypeScript",
"edition":1,
"price":73,
"authors":[
{
"id":3,
"firstName":"Dan",
"fullName":"Dan Vanderkam",
"gender":"MALE"
}
]
},
{
"id":5,
...omitted...
},
{
"id":6,
...omitted...
},
{
"id":1,
"name":"Learning GraphQL",
"edition":1,
"price":50,
"authors":[
{
"id":2,
"firstName":"Alex",
"fullName":"Alex Banks",
"gender":"MALE"
},
{
"id":1,
"firstName":"Eve",
"fullName":"Eve Procello",
"gender":"FEMALE"
}
]
},
{
"id":2,
"name":"Learning GraphQL",
"edition":2,
"price":55,
"authors":[
{
"id":2,
"firstName":"Alex",
"fullName":"Alex Banks",
"gender":"MALE"
},
{
"id":1,
"firstName":"Eve",
"fullName":"Eve Procello",
"gender":"FEMALE"
}
]
},
{
"id":3,
"name":"Learning GraphQL",
"edition":3,
"price":51,
"authors":[
{
"id":2,
"firstName":"Alex",
"fullName":"Alex Banks",
"gender":"MALE"
},
{
"id":1,
"firstName":"Eve",
"fullName":"Eve Procello",
"gender":"FEMALE"
}
]
},
{
"id":7,
"name":"Programming TypeScript",
"edition":1,
"price":47.5,
"authors":[
{
"id":4,
"firstName":"Boris",
"fullName":"Boris Cherny",
"gender":"MALE"
}
]
},
{
"id":8,
"name":"Programming TypeScript",
"edition":2,
"price":45,
"authors":[
{
"id":4,
"firstName":"Boris",
"fullName":"Boris Cherny",
"gender":"MALE"
}
]
},
{
"id":9,
"name":"Programming TypeScript",
"edition":3,
"price":48,
"authors":[
{
"id":4,
"firstName":"Boris",
"fullName":"Boris Cherny",
"gender":"MALE"
}
]
}
]
}
]
}
}