[Feature Request] Add ability to generate particular types as Java interface, not POJO class
See original GitHub issueIs 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 . |
Issue Analytics
- State:
- Created 3 years ago
- Reactions:3
- Comments:7 (5 by maintainers)
I think I can make a PR.
@donbeave, I’ve merged your changes to develop branch. This feature will soon be released as part of 5.0.0. Thanks!