Статьи

GraphQL по рою Wildfly

«GraphQL — это язык запросов для API и среда выполнения для выполнения этих запросов с вашими существующими данными. GraphQL предоставляет полное и понятное описание данных в вашем API, дает клиентам возможность запрашивать именно то, что им нужно, и ничего более, упрощает разработку API со временем и предоставляет мощные инструменты для разработчиков ».

— с https://graphql.org/

Любой, кто создал службы REST , которые используются несколькими потребителями, например другие службы, веб-сайты или мобильные устройства, будет знать, что очень трудно создать идеальную конечную точку, которая удовлетворяет всем потребностям. Вы, как правило, в конечном итоге с вариантами одного и того же сервиса, для всех этих особых случаев 🙂

Теперь мы все знаем, что мы должны просто использовать HATEOAS … и это было в моем списке TODO (обещаю!), Пока я не наткнулся на GraphQL .

Итак, в этом сообщении в блоге я объясню, как вы можете без особых усилий добавить GraphQL в существующее приложение JAX-RS .

Пример проекта

Пример проекта доступен на Github , и его очень легко начать

1
2
3
git clone https://github.com/phillip-kruger/membership.git
cd membership
mvn clean install

Это запустит фиджара-демона-роя с примером приложения http: // localhost: 8080 / members /

Высокий уровень

Примером является базовая служба членства, где вы можете получить всех участников или конкретного участника. Вы можете добавлять, редактировать и удалять участников.

Приложение является типичным JAX-RS, CDI, EJB, JPA, приложением Java EE для проверки Bean-компонентов, и мы добавляем новую конечную точку GraphQL.

Часть GraphQL использует следующие библиотеки:

Единственные java-классы, которые я добавил, чтобы представить мой существующий JAX-RS как GraphQL:

  • MembershipGraphQLListener — зарегистрировать прослушиватель сервлета «/ graphql».
  • MembershipGraphQLApi — конечная точка GraphQL. Просто обернуть существующий сервис @Stateless .
  • MembershipErrorHandler — для обработки исключений.

Используя аннотации из graphQL-spqr , класс MembershipGraphQLApi действительно просто описывает и оборачивает существующий сервис @Stateless :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
@RequestScoped
    public class MembershipGraphQLApi {
     
        @Inject
        private MembershipService membershipService;
         
        // ...
        @GraphQLQuery(name = "memberships")
        public List<Membership> getAllMemberships(Optional<MembershipFilter> filter,
                                    @GraphQLArgument(name = "skip") Optional<Integer> skip,
                                    @GraphQLArgument(name = "first") Optional<Integer> first) {
            return membershipService.getAllMemberships(filter, skip, first);  
        }
        // ...
    }

Я надеюсь — у нас скоро будет JAX-QL (или что-то еще) как часть Java EE (или Jakarta EE, или MicroProfile), чтобы сделать это еще проще !!

Сначала немного ОТДЫХА

Я использую MicroProfile OpenAPI и Swagger UI для создания определений Open API для конечной точки REST.

Вы можете протестировать некоторые запросы, используя http: // localhost: 8080 / members / rest / openapi-ui /

Пример — Получение всех членств:

GET http://localhost:8080/membership/rest

Это вернет:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
[
      {
        "membershipId": 1,
        "owner": {
          "id": 1,
          "names": [
            "Natus",
            "Phillip"
          ],
          "surname": "Kruger"
        },
        "type": "FULL"
      },
      {
        "membershipId": 2,
        "owner": {
          "id": 2,
          "names": [
            "Charmaine",
            "Juliet"
          ],
          "surname": "Kruger"
        },
        "type": "FULL"
      },
      {
        "membershipId": 3,
        "owner": {
          "id": 3,
          "names": [
            "Koos"
          ],
          "surname": "van der Merwe"
        },
        "type": "FULL"
      },
      {
        "membershipId": 4,
        "owner": {
          "id": 4,
          "names": [
            "Minki"
          ],
          "surname": "van der Westhuizen"
        },
        "type": "FREE"
      }
    ]

Пример — получение определенного членства (1):

GET http://localhost:8080/membership/rest/1

Это вернет:

01
02
03
04
05
06
07
08
09
10
11
12
{
      "membershipId": 1,
      "owner": {
        "id": 1,
        "names": [
          "Natus",
          "Phillip"
        ],
        "surname": "Kruger"
      },
      "type": "FULL"
    }

Теперь давайте посмотрим на GraphQL

Приложение включает в себя пользовательский интерфейс GraphiQL (как веб-jar), который позволяет легко тестировать некоторые запросы GraphQL

Вы можете протестировать некоторые запросы, используя http: // localhost: 8080 / members / graph / graphiql /

Итак, давайте посмотрим, выполнил ли GraphQL обещание «Нет больше перегрузок и недопоставок».

Получить все членство и все поля (так же, как REST получить все)

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
query Memberships {
        memberships{
            ...fullMembership
        }
    }
 
    fragment fullMembership on Membership {
        membershipId
        owner{
            ...owner
        }
        type
    }
 
    fragment owner on Person {
        id
        names
        surname 
    }

Это вернет все значения, однако теперь легко определить, какие поля должны быть включены …

Получить все членство, но только включить поле id

1
2
3
4
5
6
7
8
9
query Memberships {
        memberships{
            ...membershipIdentifiers
        }
    }
 
    fragment membershipIdentifiers on Membership {
        membershipId
    }

Полученная полезная нагрузка теперь намного меньше:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
{
      "data": {
        "memberships": [
          {
            "membershipId": 1
          },
          {
            "membershipId": 2
          },
          {
            "membershipId": 3
          },
          {
            "membershipId": 4
          }
        ]
      }
    }

Теперь давайте получим только определенные типы членства (так что получайте все БЕСПЛАТНЫЕ членства)

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
query FilteredMemberships {
        memberships(filter:{
            type:FREE
        }){
            ...fullMembership
        }
    }
 
    fragment fullMembership on Membership {
        membershipId
        owner{
            ...owner
        }
        type
    }
 
    fragment owner on Person {
        id
        names
        surname 
    }

Это вернет только бесплатное членство. Здорово !

Или, что еще лучше, все фамилии участников начинаются с «Kru»

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
query FilteredMemberships {
        memberships(filter:{
            surnameContains: "Kru"
        }){
            ...fullMembership
        }
    }
 
    fragment fullMembership on Membership {
        membershipId
        owner{
            ...owner
        }
        type
    }
 
    fragment owner on Person {
        id
        names
        surname 
    }

Большой !! Мы нашли двух человек:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
      "data": {
        "memberships": [
          {
            "membershipId": 1,
            "owner": {
              "id": 1,
              "names": [
                "Natus",
                "Phillip"
              ],
              "surname": "Kruger"
            },
            "type": "FULL"
          },
          {
            "membershipId": 2,
            "owner": {
              "id": 2,
              "names": [
                "Charmaine",
                "Juliet"
              ],
              "surname": "Kruger"
            },
            "type": "FULL"
          }
        ]
      }
    }

Получение определенного членства, используя переменную на клиенте:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
query Membership($id:Int!) {
        membership(membershipId:$id){
            ...fullMembership
        }
    }
 
    fragment fullMembership on Membership {
        membershipId
        owner{
          ...owner
        }
        type
    }
 
    fragment owner on Person {
        id
        names
        surname 
    }

Переменная:

1
{"id":1}

Включить поля при определенном условии:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
query Membership($id:Int!,$withOwner: Boolean!) {
        membership(membershipId:$id){
            ...fullMembership
        }
    }
 
    fragment fullMembership on Membership {
        membershipId
        owner @include(if: $withOwner){
          ...owner
        }
        type
    }
 
    fragment owner on Person {
        id
        names
        surname 
    }

Переменная:

1
{"id":1,"withOwner": false}

это исключит владельца (верно включить):

1
2
3
4
5
6
7
8
{
      "data": {
        "membership": {
          "membershipId": 1,
          "type": "FULL"
        }
      }
    }

пагинация

Давайте используем запрос get all, но разбиваем на страницы.

01
02
03
04
05
06
07
08
09
10
11
12
query Memberships($itemsPerPage:Int!,$pageNumber:Int!) {
        memberships(
            first:$itemsPerPage,
                skip:$pageNumber) {
            membershipId
                owner{
                    names
                    surname
                }
            type
        }
    }

Переменная:

1
{"itemsPerPage": 2,"pageNumber": 1}

Это вернет первые 2 результата, а затем вы можете выполнить пейджинг, увеличив значение «pageNumber».

Мутации

Создайте

1
2
3
4
5
mutation CreateMember {
        createMembership(membership: {type:FULL,owner: {names: "James",surname:"Small"}}) {
            membershipId
        }
    }

Это создаст новое членство и вернет идентификатор.

Обновить

1
2
3
4
5
mutation EditMember($membership: MembershipInput!) {
        createMembership(membership:$membership) {
            membershipId
        }
    }

Переменная:

01
02
03
04
05
06
07
08
09
10
11
12
13
{
        "membership": {
          "membershipId": 2,
            "owner": {
                "names": [
                "Charmaine",
                "Juliet"
              ],
                "surname": "Krüger"
            },
            "type": "FULL"
        }
    }

(добавлен умляут на Крюгера, теперь это должен быть Крюгер)

Удалить

1
2
3
4
5
mutation DeleteMembership($id:Int!){
        deleteMembership(membershipId:$id){
          membershipId
        }
    }

Переменная:

1
{"id":1}

Это удалит членство 1.

Исключение.

MembershipErrorHandler преобразует исключение ConstraintViolationException (которое выдается при сбое проверки компонента) и создает приятное сообщение об ошибке для GraphQL.

Итак, давайте попробуем создать участника с фамилией всего в одну букву.

1
2
3
4
5
mutation CreateMember($membership: MembershipInput!) {
        createMembership(membership:$membership) {
            membershipId
        }
    }

Переменная:

1
2
3
4
5
6
7
8
9
{
         "membership": {
             "owner": {
                 "names": "Christina",
                 "surname": "S"
             },
             "type": "FULL"
         }
     }

Это вернет сообщение об ошибке проверки бина:

01
02
03
04
05
06
07
08
09
10
11
12
{
      "data": {
        "createMembership": null
      },
      "errors": [
        {
          "message": "Surname 'S' is too short, minimum 2 characters",
          "path": null,
          "extensions": null
        }
      ]
    }

Если вы посмотрите на человека POJO:

1
2
3
@NotNull(message = "Surname can not be empty")
    @Size(min=2, message = "Surname '${validatedValue}' is too short, minimum {min} characters")
    private String surname;

интроспекция

Еще одна приятная вещь в GraphQL состоит в том, что у него есть система схем и типов, которую вы можете запросить:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
{
        __schema {
            queryType {
                name
                fields {
                    name
                }
            }
            mutationType{
                name
                fields{
                    name
                }
            }
            subscriptionType {
                name
                fields{
                    name
                }
            }
        }
    }

Выше будут описаны запросы и мутации, доступные в этой конечной точке.

Вы также можете описать свои модели:

01
02
03
04
05
06
07
08
09
10
11
12
{
        __type(name: "Membership") {
            name
            kind
            fields {
                name
                args {
                    name
                }
            }
        }
    }

Резюме

В этом примере мы не удалили REST, а просто добавили GraphQL как альтернативный вариант для потребителя.

К настоящему времени должно быть ясно, что у клиента гораздо больше возможностей фильтровать и запрашивать данные именно так, как им это нужно. Все это без необходимости выполнять дополнительную работу на сервере. Это учитывает быстрые итерации продукта на стороне клиента.

Полезная нагрузка по проводам оптимизирована, и мы экономим пропускную способность!

Опять же, я надеюсь — у нас скоро будет JAX-QL (или что-то еще) как часть Java EE (или Jakarta EE, или MicroProfile), чтобы сделать это еще проще!

Опубликовано на Java Code Geeks с разрешения Филиппа Крюгера, партнера нашей программы JCG . Смотреть оригинальную статью здесь: GraphQL о рою Wildfly

Мнения, высказанные участниками Java Code Geeks, являются их собственными.