BlogContacts
BlogGamesContacts

{{item}}

Simple GraphQL implementation

03.04.2020

Java, Spring, JPA, H2, GraphQL

DemoGitHub
Example of a single-endpoint web service with the GraphQL API and three in-memory H2 databases using spring-webmvc and spring-data-jpa.For this implementation of GraphQL API we'll take two tables: users and roles with many-to-many relationships between them. So that each user may have several roles, and each role may belong to several users. As a mapping for these associations we'll use the third table: users_n_roles consisting of two columns: user_id and role_id, which form a composite primary key for this table.For the purity of the experiment all these tables are located in separate databases. So we have three databases and three tables in them, and no inner join or left join are available.
Persistence layer for this application is organized using spring-data-jpa and hibernate-entitymanager. As a database management system we'll use H2 Database Engine.
This application uses the GraphQL Java implementation integrated with spring-webmvc as a single-endpoint REST controller, a manager for a service layer and a data fetcher from a persistence layer. For testing the API from client browser we'll include in this application a GraphiQL developer tool.
Example of a query with a response looks like this:
Simple GraphQL implementation
1. Maven dependencies:
XML
<!-- graphql -->
<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-java-spring-webmvc</artifactId>
    <version>2020-07-12T23-24-35-c6606f6</version>
</dependency>

<!-- spring-data -->
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-jpa</artifactId>
    <version>2.1.8.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-entitymanager</artifactId>
    <version>5.4.3.Final</version>
</dependency>
Let's begin a construction of our application with a persistence layer. For each database we should enable JPA repositories and specify the base packages for the corresponding interfaces. Also for each database we should specify the entity manager factory and the base packages for corresponding entities, as well as the transaction manager and the data source, of course.To do this, we'll include in our application one configuration class for data JPA and three inner classes for each database.2. Persistence layer configuration:
Java
package org.drakonoved.graphql;

@Configuration
@PropertySource(value = "classpath:resources/application.properties", encoding = "UTF-8")
public class DataJpaConfig {
    private final String basePackage = "org.drakonoved.graphql";

    @EnableJpaRepositories(
            basePackages = basePackage + ".repository.usersdb",
            entityManagerFactoryRef = "usersdbEntityManagerFactory",
            transactionManagerRef = "usersdbTransactionManager")
    public class UsersDBJpaConfig {
        @Bean("usersdbEntityManagerFactory")
        public LocalContainerEntityManagerFactoryBean usersDBEntityManagerFactoryBean(
                @Value("${datasource.usersdb.script}") String script) {
            return createEntityManagerFactoryBean(
                    script, "usersdb", basePackage + ".dto.usersdb");
        }

        @Bean("usersdbTransactionManager")
        public PlatformTransactionManager usersDBTransactionManager(
                @Qualifier("usersdbEntityManagerFactory")
                        LocalContainerEntityManagerFactoryBean factoryBean) {
            return new JpaTransactionManager(factoryBean.getNativeEntityManagerFactory());
        }
    }

    @EnableJpaRepositories(
            basePackages = basePackage + ".repository.rolesdb",
            entityManagerFactoryRef = "rolesdbEntityManagerFactory",
            transactionManagerRef = "rolesdbTransactionManager")
    public class RolesDBJpaConfig {
        @Bean("rolesdbEntityManagerFactory")
        public LocalContainerEntityManagerFactoryBean rolesDBEntityManagerFactoryBean(
                @Value("${datasource.rolesdb.script}") String script) {
            return createEntityManagerFactoryBean(
                    script, "rolesdb", basePackage + ".dto.rolesdb");
        }

        @Bean("rolesdbTransactionManager")
        public PlatformTransactionManager rolesDBTransactionManager(
                @Qualifier("rolesdbEntityManagerFactory")
                        LocalContainerEntityManagerFactoryBean factoryBean) {
            return new JpaTransactionManager(factoryBean.getNativeEntityManagerFactory());
        }
    }

    @EnableJpaRepositories(
            basePackages = basePackage + ".repository.usersnrolesdb",
            entityManagerFactoryRef = "usersnrolesdbEntityManagerFactory",
            transactionManagerRef = "usersnrolesdbTransactionManager")
    public class UsersNRolesDBJpaConfig {
        @Bean("usersnrolesdbEntityManagerFactory")
        public LocalContainerEntityManagerFactoryBean usersNRolesDBEntityManagerFactoryBean(
                @Value("${datasource.usersnrolesdb.script}") String script) {
            return createEntityManagerFactoryBean(
                    script, "usersnrolesdb", basePackage + ".dto.usersnrolesdb");
        }

        @Bean("usersnrolesdbTransactionManager")
        public PlatformTransactionManager usersNRolesDBTransactionManager(
                @Qualifier("usersnrolesdbEntityManagerFactory")
                        LocalContainerEntityManagerFactoryBean factoryBean) {
            return new JpaTransactionManager(factoryBean.getNativeEntityManagerFactory());
        }
    }

    //////// //////// //////// //////// //////// //////// //////// ////////

    private LocalContainerEntityManagerFactoryBean createEntityManagerFactoryBean(
            String script, String dbname, String packagesToScan) {
        var factoryBean = new LocalContainerEntityManagerFactoryBean();
        factoryBean.setDataSource(new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .setName(dbname)
                .addScript(script)
                .build());
        factoryBean.setPersistenceUnitName(dbname + "EntityManagerFactory");
        factoryBean.setPackagesToScan(packagesToScan);
        factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());

        var properties = new HashMap<String, Object>();
        properties.put("hibernate.hbm2ddl.auto", "none");
        properties.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
        factoryBean.setJpaPropertyMap(properties);

        return factoryBean;
    }
}
Consider entities for JPA repositories. We'll use them as data transfer objects in GraphQL schema. SQL tables users and roles have no information about each other, but in web service response each user may have a list of his roles, and vice versa. So we'll extend these entities with transient fields.3. Entities and DTO:
Java
package org.drakonoved.graphql.dto.usersdb;

@Entity
@Table(name = "users")
public class User {
    @Id
    private Integer id;
    private String nickname;
    @Column(name = "first_name")
    private String firstName;
    @Column(name = "second_name")
    private String secondName;
    @Column(name = "third_name")
    private String thirdName;
    @Transient
    private ArrayList<Role> roles;

    // getters, setters and constructor . . .

    public void addRole(Role role) {
        if (role == null) return;
        if (this.roles == null) {
            this.roles = new ArrayList<>();
        }
        roles.add(role);
    }
}
Java
package org.drakonoved.graphql.dto.rolesdb;

@Entity
@Table(name = "roles")
public class Role {
    @Id
    private Integer id;
    private String name;
    private String description;
    @Transient
    private ArrayList<User> users;

    // getters, setters and constructor . . .

    public void addUser(User user) {
        if (user == null) return;
        if (this.users == null) {
            this.users = new ArrayList<>();
        }
        users.add(user);
    }
}
Third table users_n_roles we'll use as a map for the relations between the two previous tables. We won't use it as a data transfer object, but only for fetching data. There we'll need an additional IdClass for the composite primary key.4. Mapping entity with the composite primary key:
Java
package org.drakonoved.graphql.dto.usersnrolesdb;

@Entity
@IdClass(UserNRole.InnerIdClass.class)
@Table(name = "users_n_roles")
public class UserNRole {
    @Id
    @Column(name = "user_id")
    private Integer userId;
    @Id
    @Column(name = "role_id")
    private Integer roleId;

    // getters, setters and constructor . . .

    public static class InnerIdClass implements Serializable {
        private Integer userId;
        private Integer roleId;
    }
}
5. JPA repositories interfaces:
Java
package org.drakonoved.graphql.repository.rolesdb;

@Repository
public interface RoleRepository extends JpaRepository<Role, Integer> {
    Role getById(Integer id);
    Role getByIdAndName(Integer id, String name);
    List<Role> findByName(String name);
}
Java
package org.drakonoved.graphql.repository.usersdb;

@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
    User getById(Integer id);
    User getByIdAndNickname(Integer id, String nickname);
    List<User> findByNickname(String nickname);
}
Java
package org.drakonoved.graphql.repository.usersnrolesdb;

@Repository
public interface UserNRoleRepository extends JpaRepository<UserNRole, Integer> {
    List<UserNRole> findByUserId(Integer userId);
    List<UserNRole> findByRoleId(Integer roleId);
    UserNRole getByUserIdAndRoleId(Integer userId, Integer roleId);
}
The GraphQL schema contains two query types: users and roles, which return corresponding data types. Schema accepts incoming parameters, but they are not required in this application.6. GraphQL schema:
schema {
  query: Root
}

# Query types for fetching data from tables
type Root {
  # Get list of Users with their Roles
  users(id: Int, nickname: String): [User],
  # Get list of Roles with their Users
  roles(id: Int, name: String): [Role]
}

type User {
  id: ID!,
  nickname: String!,
  firstName: String!,
  secondName: String!,
  thirdName: String!,
  roles(id: Int, name: String): [Role]
}

type Role {
  id: ID!,
  name: String!,
  description: String!,
  users(id: Int, nickname: String): [User]
}
Note: the GraphQL schema and request format are not a widely known json format, but similar.7. Request example:
{
  users(id: 3) {
    id
    nickname
    firstName
    secondName
    thirdName
    roles(id: 1) {
      id
      name
      description
    }
  }
}
Main REST controller for this application will consist of two endpoints: for a simple GraphQL string queries and for a inner GraphiQL tool queries. As a response both of these endpoints return standard JSON objects.8. GraphQL REST controller:
Java
package org.drakonoved.graphql.controller;

@RestController
public class GraphQLController {
    private GraphQLService graphQLService;

    @Autowired
    public GraphQLController(GraphQLService graphQLService) {
        this.graphQLService = graphQLService;
    }

    @PostMapping("/graphql")
    public ResponseEntity<Object> graphql(@RequestBody String query) {
        ExecutionResult result = graphQLService.executeGraphQL(query);
        return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON_UTF8).body(result);
    }

    @PostMapping("/graphiql-inner-query")
    public ResponseEntity<Object> graphiql(@RequestBody Map<String, String> requestBody) {
        ExecutionResult result = graphQLService.executeGraphQL(requestBody.get("query"));
        return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON_UTF8).body(result);
    }
}
In the service layer of our application we'll initialise the GraphQL schema and specify the data fetchers for the runtime type wiring.9. GraphQL service:
Java
package org.drakonoved.graphql.service;

@Service
public class GraphQLService {
    private final GraphQL graphQL;

    @Autowired
    public GraphQLService(@Value("classpath:resources/schema.graphql") Resource schema,
                          GraphQLDataFetcher graphQLDataFetcher) throws IOException {

        TypeDefinitionRegistry typeDefinitionRegistry =
                new SchemaParser().parse(schema.getFile());

        RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring()
                .type("Root", typeWiring -> typeWiring
                        .dataFetcher("users", graphQLDataFetcher.getUsers())
                        .dataFetcher("roles", graphQLDataFetcher.getRoles()))
                .type("User", typeWiring -> typeWiring
                        .dataFetcher("roles", graphQLDataFetcher.getUserRoles()))
                .type("Role", typeWiring -> typeWiring
                        .dataFetcher("users", graphQLDataFetcher.getRoleUsers()))
                .build();

        GraphQLSchema graphQLSchema =
                new SchemaGenerator().makeExecutableSchema(typeDefinitionRegistry, runtimeWiring);

        this.graphQL = GraphQL.newGraphQL(graphQLSchema).build();
    }

    //////// //////// //////// //////// //////// //////// //////// ////////

    public ExecutionResult executeGraphQL(String query) {
        return this.graphQL.execute(query);
    }
}
GraphQL data fetchers describe how the data should be fetched from a persistence layer, according to those fields which client requested. If the client asks for the list of the users and doesn't specify the list of the roles for each user, then the call of the data fetcher getUserRoles won't be made and JPA repository won't be invoked.Incoming parameters are accepted, but not required.10. GraphQL data fetchers:
Java
package org.drakonoved.graphql.dao;

@Component
public class GraphQLDataFetcher {
    private UserRepository userRepository;
    private RoleRepository roleRepository;
    private UserNRoleRepository userNRoleRepository;

    @Autowired
    public GraphQLDataFetcher(UserRepository userRepository,
                              RoleRepository roleRepository,
                              UserNRoleRepository userNRoleRepository) {
        this.userRepository = userRepository;
        this.roleRepository = roleRepository;
        this.userNRoleRepository = userNRoleRepository;
    }

    public DataFetcher<List> getUsers() {
        return environment -> {
            Integer id = environment.getArgument("id");
            String nickname = environment.getArgument("nickname");

            if (id != null && nickname != null) {
                return Collections.singletonList(userRepository.getByIdAndNickname(id, nickname));
            }

            if (id != null) {
                return Collections.singletonList(userRepository.getById(id));
            }

            if (nickname != null) {
                return userRepository.findByNickname(nickname);
            }

            return userRepository.findAll();
        };
    }

    public DataFetcher<List> getUserRoles() {
        return environment -> {
            User user = environment.getSource();

            Integer roleId = environment.getArgument("id");
            String roleName = environment.getArgument("name");

            if (roleId != null) {
                UserNRole userNRole = userNRoleRepository.getByUserIdAndRoleId(user.getId(), roleId);
                if (userNRole == null) {
                    return null;
                }
                Role role;
                if (roleName != null) {
                    role = roleRepository.getByIdAndName(roleId, roleName);
                } else {
                    role = roleRepository.getById(roleId);
                }
                user.addRole(role);
                return user.getRoles();
            }

            List<UserNRole> usersNRoles = userNRoleRepository.findByUserId(user.getId());

            if (roleName != null) {
                usersNRoles.forEach(userNRole ->
                        user.addRole(roleRepository.getByIdAndName(userNRole.getRoleId(), roleName)));
            } else {
                usersNRoles.forEach(userNRole ->
                        user.addRole(roleRepository.getById(userNRole.getRoleId())));
            }

            return user.getRoles();
        };
    }

    // . . . other GraphQL data fetchers in a similar way . . .
}
That is all folks. See the entire code of the application.
Privacy policy
Back to Top