BlogContacts
BlogGamesContacts

{{item}}

Simple GraphQL implementation

03.04.2020

Java, Spring, JPA, GraphQL

See GitHubSource code
Write a single-endpoint web service with the GraphQL API, using the basis of Spring webmvc and 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 databases management system we'll use PostgreSQL.
GraphQL in this application is used 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.
Simple GraphQL implementation
1. Application structure:
simple-graphql-implementation
│
├── src
│   └── main
│       ├── java
│       │   └── graphql
│       │       ├── controller
│       │       │   └── GraphQLController.java
│       │       ├── dao
│       │       │   └── GraphQLDataFetcher.java
│       │       ├── dto
│       │       │   ├── rolesdb
│       │       │   │   └── Role.java
│       │       │   ├── usersdb
│       │       │   │   └── User.java
│       │       │   └── usersnrolesdb
│       │       │       └── UserNRole.java
│       │       ├── repository
│       │       │   ├── rolesdb
│       │       │   │   └── RoleRepository.java
│       │       │   ├── usersdb
│       │       │   │   └── UserRepository.java
│       │       │   └── usersnrolesdb
│       │       │       └── UserNRoleRepository.java
│       │       ├── service
│       │       │   └── GraphQLService.java
│       │       ├── DataJpaConfig.java
│       │       └── . . .
│       ├── resources
│       │   ├── . . .
│       │   └── schema.graphql
│       └── webapp
│           ├── root
│           │   ├── . . .
│           │   └── graphiql.html
│           └── WEB-INF
│               └── web.xml
└── pom.xml
2. Main Maven dependencies:
XML
<!-- graphql -->
<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-java-spring-webmvc</artifactId>
    <version>2019-06-24T11-47-27-31ab4f9</version>
</dependency>

<!-- spring-data -->
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-jpa</artifactId>
    <version>2.1.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-entitymanager</artifactId>
    <version>5.3.8.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.3. Persistence layer configuration:
Java
package org.drakonoved.graphql;

@Configuration
public class DataJpaConfig {
    @EnableJpaRepositories(basePackages = "org.drakonoved.graphql.repository.usersdb",
            entityManagerFactoryRef = "usersdbEntityManagerFactoryBean",
            transactionManagerRef = "usersdbTransactionManager")
    public class UsersDBJpaConfig {
        @Bean
        @Qualifier("usersdbDataSource")
        public DataSource usersdbDataSource(
                @Value("${datasources.driverClass}") String driverClassName,
                @Value("${datasource.users.db.url}") String url,
                @Value("${datasource.users.db.user}") String username,
                @Value("${datasource.users.db.password}") String password) {

            DriverManagerDataSource dataSource = new DriverManagerDataSource();

            dataSource.setDriverClassName(driverClassName);
            dataSource.setUrl(url);
            dataSource.setUsername(username);
            dataSource.setPassword(password);

            return dataSource;
        }

        @Bean
        @Qualifier("usersdbEntityManagerFactoryBean")
        public LocalContainerEntityManagerFactoryBean usersdbEntityManagerFactoryBean(
                @Qualifier("usersdbDataSource") DataSource usersdbDataSource) {

            LocalContainerEntityManagerFactoryBean factoryBean =
                    new LocalContainerEntityManagerFactoryBean();
            factoryBean.setDataSource(usersdbDataSource);
            factoryBean.setPackagesToScan("org.drakonoved.graphql.dto.usersdb");
            factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
            factoryBean.setPersistenceUnitName("usersdbEntityManager");

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

            return factoryBean;
        }

        @Bean
        @Qualifier("usersdbTransactionManager")
        public PlatformTransactionManager usersdbTransactionManager(
                @Qualifier("usersdbEntityManagerFactoryBean")
                LocalContainerEntityManagerFactoryBean usersdbEntityManagerFactoryBean) {

            JpaTransactionManager transactionManager = new JpaTransactionManager();
            transactionManager.setEntityManagerFactory(usersdbEntityManagerFactoryBean.getObject());
            return transactionManager;
        }
    }
    // . . . other JPA repositories inner classes in a similar way . . .
}
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.4. 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.5. A 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;
    }
}
6. 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.7. The GraphQL schema:
JSON
schema {
  query: Query
}

# Main query type for fetching data from tables
type Query {
  # Get list of users from corresponding table
  users(id: Int, nickname: String): [User],
  # Get list of roles from corresponding table
  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.8. Request example:
JSON
{
  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.9. 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);
    }

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

    @GetMapping({"/", "/index.html"})
    public RedirectView getMainPage() {
        return new RedirectView("/graphiql.html");
    }
}
In the service layer of our application we'll initialise the GraphQL schema and specify the data fetchers for the runtime type wiring.10. GraphQL service:
Java
package org.drakonoved.graphql.service;

@Service
public class GraphQLService {
    private GraphQLDataFetcher graphQLDataFetcher;

    @Value("classpath:resources/schema.graphql")
    private Resource resource;
    private GraphQL graphQL;

    @Autowired
    public GraphQLService(GraphQLDataFetcher graphQLDataFetcher) {
        this.graphQLDataFetcher = graphQLDataFetcher;
    }

    @PostConstruct
    private void loadSchema() throws IOException {
        File file = resource.getFile();

        TypeDefinitionRegistry typeDefinitionRegistry = new SchemaParser().parse(file);

        RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring()
            .type("Query", 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);

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

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

    public ExecutionResult executeGraphQL(String query) {
        return 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.11. 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