Los fragmentos en GraphQL

Escrito por picodotdev el .
java planeta-codigo programacion
Comentarios

GraphQL

Los fragmentos en el contexto de GraphQL pueden ser de dos tipos, definidos previamente o definidos en linea. Los primeros permiten simplificar las consultas definiendo en un bloque una colección de datos a recuperar si tener que indicarlos explícitamente individualmente, lo que resulta útil para no repetir el mimo grupo de datos si se utiliza en varias consultas diferentes. Los fragmentos en línea permiten recuperar unos datos u otros en función del tipo de la instancia de la cual se quieren recuperar.

Teniendo dos consultas que recuperan los datos de una colección de libros sin los fragmentos habría que definir los mismos datos a recuperar dos veces en ambas consultas. En estas consultas de ejemplo se recupera una lista de libros y un libro determinado. Si en ambas se recuperan los datos id, title y date hay que indicar los campos a recuperar dos veces.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
query Books {
  books {
    id
    title
    date
  }
  book(id: 7) {
    id
    title
    date
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
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
{
  "data": {
    "books": [
      {
        "id": 7,
        "title": "Ojo en el cielo",
        "date": "1957-01-01"
      },
      {
        "id": 8,
        "title": "Muerte de la luz",
        "date": "1977-01-01"
      },
      {
        "id": 9,
        "title": "El nombre de la rosa",
        "date": "1980-01-01"
      },
      {
        "id": 10,
        "title": "Los tejedores de cabellos",
        "date": "1995-01-01"
      },
      {
        "id": 11,
        "title": "Ready Player One",
        "date": "2011-01-01"
      }
    ],
    "book": {
      "id": 7,
      "title": "Ojo en el cielo",
      "date": "1957-01-01"
    }
  }
}

Con un fragmento se definen esos campos comunes a recuperar en las consultas una sola vez. Si posteriormente cambian los datos a recuperar solo es necesario cambiarlo en un único punto. Los fragmentos definidos son una forma de simplificar las consultas y evitar tener que cambiar varias consultas si el grupo de datos cambia en todas ellas. Los datos obtenidos son los mismos que en el caso sin utilizar el fragmento.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
query Books {
  books {
    ...data
  }
  book(id: 7) {
    ...data
  }
}

fragment data on Book {
  id
  title
  date
}
Consultas sin y con un fragmento

Los fragmentos en línea o inline permiten por otra parte una funcionalidad adicional y es recuperar diferentes datos según el tipo de la entidad. En el ejemplo he añadido una nueva entidad Magazine además de la ya existente Book, en el código Java ambas heredan de Publication. Las entidades Book y Magazine no comparten las mismas propiedades dado que son entidades diferentes por lo que en la consulta es necesario tener un mecanismo con el cual poder recuperar los datos en función del tipo.

Estas son las definiciones de las entidades resumidas y la consulta publications que devuelve las publicaciones que incluye libros y revistas. Con la definición de una union se establece que una Publication puede ser un Book o Magazine.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
...

type Book {
    id: Long
    title: String
    author: Author
    isbn: String
    date: LocalDate
    ...
}

type Magazine {
    id: Long
    name: String
    pages: Long
}

...

union Publication = Book | Magazine

type Query {
    books(filter: BookFilter): [Book]!
    publications: [Publication]!
    book(id: Long): Book!
    authors: [Author]!
    author(id: Long): Author!
}

...
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
query Publications {
  publications {
    ... on Book {
      id
      title
      date
    }
    ... on Magazine {
      id
      name
      pages
    }
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
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
{
  "data": {
    "publications": [
      {
        "id": 7,
        "title": "Ojo en el cielo",
        "date": "1957-01-01"
      },
      {
        "id": 8,
        "title": "Muerte de la luz",
        "date": "1977-01-01"
      },
      {
        "id": 9,
        "title": "El nombre de la rosa",
        "date": "1980-01-01"
      },
      {
        "id": 10,
        "title": "Los tejedores de cabellos",
        "date": "1995-01-01"
      },
      {
        "id": 11,
        "title": "Ready Player One",
        "date": "2011-01-01"
      },
      {
        "id": 12,
        "name": "Muy interesante",
        "pages": 65
      },
      {
        "id": 13,
        "name": "PC Actual",
        "pages": 90
      }
    ]
  }
}

Para las publicaciones del tipo Book en este ejemplo se recuperan los campos id, title y date. Para las publicaciones de tipo Magazine se recuperan los campos id, name y pages. Las publicaciones Muy interesante y PC Actual son dos Magazine y el resto de publicaciones son del tipo Book.

Consulta con fragmentos en linea

Si es necesario hay que añadir la clases Java que representan a los tipos de GraphQL a la lista de clases del diccionario en la definición del esquema.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package io.github.picodotdev.blogbitix.graphql;

...

@SpringBootApplication
@ServletComponentScan
public class Main {

    ...

    @Bean
    public ServletRegistrationBean graphQLServletRegistrationBean(LibraryRepository libraryRepository) throws Exception {
        GraphQLSchema schema = SchemaParser.newParser()
                .schemaString(IOUtils.resourceToString("/library.graphqls", Charset.forName("UTF-8")))
                .resolvers(new Query(libraryRepository), new Mutation(libraryRepository), new BookResolver(libraryRepository), new MagazineResolver(libraryRepository))
                .scalars(new GraphQLScalarType("LocalDate", "LocalDate scalar", new LocalDateCoercing()))
                .dictionary(Magazine.class)
                .build()
                .makeExecutableSchema();
        ...
    }

    ...  
}

Para cada entidad hay una clase Java que la representa y un repositorio que contiene la consulta para obtener las publicaciones que no hace más que añadir en una lista el conjunto de libros y revistas en la librería.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package io.github.picodotdev.blogbitix.graphql;

...

public class Book extends Publication {
    
    private Long id;
    private String title;
    private Author author;
    private LocalDate date;
    private List<Comment> comments;

    public Book(Long id, String title, Author author, LocalDate date, List<Comment> comments) {
        this.id = id;
        this.title = title;
        this.author = author;
        this.date = date;
        this.comments = comments;
    }

    ...
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package io.github.picodotdev.blogbitix.graphql;

...

public class Magazine extends Publication {
    
    private Long id;
    private String name;
    private Long pages;

    public Magazine(Long id, String name, Long pages) {
        this.id = id;
        this.name = name;
        this.pages = pages;
    }
    
    ...
}
1
2
3
4
5
package io.github.picodotdev.blogbitix.graphql;

public class Publication {
    
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
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
package io.github.picodotdev.blogbitix.graphql;

...

public class LibraryRepository {

    ...
    private List<Book> books;
    private List<Magazine> magazines;

    public LibraryRepository() {
        this.sequence = 0l;
        ...
        this.books = new ArrayList<>();
        this.magazines = new ArrayList<>();

        ...

        this.books.addAll(
            List.of(
                new Book(nextId(), "Ojo en el cielo", a1, LocalDate.of(1957, 1, 1), this.comments),
                new Book(nextId(), "Muerte de la luz", a2, LocalDate.of(1977, 1, 1), this.comments),
                new Book(nextId(), "El nombre de la rosa", a3, LocalDate.of(1980, 1, 1), this.comments),
                new Book(nextId(), "Los tejedores de cabellos", a4, LocalDate.of(1995, 1, 1), this.comments),
                new Book(nextId(), "Ready Player One", a5, LocalDate.of(2011, 1, 1), this.comments)
            )
        );

        this.magazines.addAll(
            List.of(
                new Magazine(nextId(), "Muy interesante", 65L),
                new Magazine(nextId(), "PC Actual", 90L)
            )
        );
    }

    ...

    public List<Publication> findPublications() {
        List<Publication> publications = new ArrayList<>();
        publications.addAll(books);
        publications.addAll(magazines);
        return publications;
    }

    ...
}

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando el comando ./gradlew run. Requiere Java 9+ o Docker.