-
-
Notifications
You must be signed in to change notification settings - Fork 114
Description
Is your feature request related to a problem? Please describe.
Let imagine we have a GraphQL service with such schema:
type Query {
userCurrent: User
}
type User {
username: String!
email: String!
orders: [Order!]! @customResolver
}
type Order {
number: String!
price: String!
}And in the database, we store users and orders in separate tables. We have such JPA models and repositories:
@Entity
public class UserJPA {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull
private String userName;
@NotNull
private String emailAddress;
// getters and setters
}@Entity
public class OrderJPA {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn
private User user;
@NotNull
private String number;
@NotNull
private BigDecimal price = BigDecimal.ZERO;
// getters and setters
}@Repository
public interface OrderRepository extends JpaRepository<OrderJPA, Long> {
List<OrderJPA> findAllByUser(UserJPA user);
}In the GraphQL server, we want to load orders only when they were requested by the client.
- this query will NOT load orders from the database:
query {
userCurrent {
username
email
}
}- but this query will load orders for the current user from the database:
query {
userCurrent {
username
orders {
number
}
}
}Now when we use graphql-java-codegen to generate a java code we will have something like this:
models:
public class User implements java.io.Serializable {
private String username;
private String email;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}public class Order implements java.io.Serializable {
private String number;
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
}api:
public interface UserCurrentQueryResolver extends graphql.kickstart.tools.GraphQLQueryResolver {
User userCurrent(graphql.schema.DataFetchingEnvironment env) throws Exception;
}public interface UserResolver extends graphql.kickstart.tools.GraphQLResolver<User> {
List<Order> orders(User user, graphql.schema.DataFetchingEnvironment env) throws Exception;
}If we want to implement UserCurrentQueryResolver, we must use mapstruct or something similar to map the original JPA entity to GraphQL POJO models. And when we start to implement UserResolver we will encounter one problem, that User POJO which generated by the graphql-java-codegen will not have UserJPA instance or user's ID field, only username.
Of course, we can do something like this:
public class UserResolverImpl implements UserResolver {
@Autowired UserRepository userRepository;
@Autowired OrderRepository orderRepository;
@Autowired OrderMapper orderMapper;
public List<Order> orders(User user, graphql.schema.DataFetchingEnvironment env) throws Exception {
UserJPA userJpa = userRepository.findByUserName(user.getUsername);
List<OrderJPA> orders = orderRepository.findByUser(userJpa)
return orderMapper.toGraphQL(orders);
}
}But this looks not correct, first of all, because we obviously already loaded UserJPA in UserCurrentQueryResolver, so we do not want to load it again, we want to pass it from UserCurrentQueryResolverImpl to UserResolverImpl.
For such cases we want to change the way how we generate User class, we want to generate the interface instead of the POJO class. And in the implementation of this interface, we will add some additional data, like UserJPA entity. So it can be something like this:
new generated User GraphQL interface:
public interface User {
String username();
String email();
}and its implementation:
public class UserImpl implements User {
private final UserJPA entity;
public UserImpl(UserJPA entity) {
this.entity = entity;
}
public UserJPA getEntity() {
return entity;
}
public String username() {
return entity.getUserName();
}
public String email() {
return entity.getEmailAddress();
}
}With such implementation, we can remove mapstruct mapping from one entity to another:
UserCurrentQueryResolver will be simplified to:
public class UserCurrentQueryResolverImpl implements UserCurrentQueryResolver {
@Autowired UserSecurityService userSecurityService;
public User userCurrent(graphql.schema.DataFetchingEnvironment env) throws Exception {
UserJPA userJpa = userSecurityService.getCurrentUser();
return new UserImpl(userJpa);
}
}public class UserResolverImpl implements UserResolver {
@Autowired OrderRepository orderRepository;
public List<Order> orders(User user, graphql.schema.DataFetchingEnvironment env) throws Exception {
List<Order> orders = orderRepository
.findByUser(((UserImpl) user).getEntity())
.stream()
.map(OrderImpl::new)
.collect(Collectors.toList());
return orders;
}
}Describe the solution you'd like
I think this can reach with adding additional option:
| Option | Data Type | Default value | Description |
|---|---|---|---|
typeInterfaces |
Set(String) | Empty | Types that must generated as interfaces should be defined here in format: TypeName or @directive. E.g.: User, @customInterface. |