question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

[Feature Request] Add ability to generate particular types as Java interface, not POJO class

See original GitHub issue

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.

  1. this query will NOT load orders from the database:
query {
  userCurrent {
    username
    email
  }
}
  1. 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:closed
  • Created 3 years ago
  • Reactions:3
  • Comments:7 (5 by maintainers)

github_iconTop GitHub Comments

2reactions
donbeavecommented, Mar 21, 2021

I think I can make a PR.

0reactions
kobylynskyicommented, Mar 26, 2021

@donbeave, I’ve merged your changes to develop branch. This feature will soon be released as part of 5.0.0. Thanks!

Read more comments on GitHub >

github_iconTop Results From Across the Web

POJO Data Binding Interface (Java Application Developer's ...
The data binding feature of the Java Client API enables your data to flow seamlessly between application-level Java objects and JSON documents stored...
Read more >
java - Method accepting two different types as parameter
I am writing a method that should accept as its parameter an object of one of two types which do not share ...
Read more >
POJO Class in Java (Plain Old Java Object with Example)
Objective. In the last tutorial, we discussed Final Keyword in Java. Here, in this Java POJO tutorial, we are going to study the...
Read more >
Add options to generate non-null attributes on Records, Pojos ...
An option is to generate false negatives only on records and/or POJOs (i.e. only nullable types are T? , the true positives, but...
Read more >
Guide to Java Reflection - Baeldung
In this tutorial, we will explore Java reflection, which allows us to inspect and/or modify runtime attributes of classes, interfaces, ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found