I am new to Graphql and looking into creating a proof of concept to see how it works. I am using Spring Boot (2.2.2.RELEASE) and bringing in the graphql-spring-boot-starter.
<dependency>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphql-spring-boot-starter</artifactId>
<version>6.0.1</version>
</dependency>
I have setup my graphql schema as a file on the classpath with the following configuration:
type Order {
orderNumber: ID!
customers: [Customer]
items: [Item]
}
type Customer {
customerNumber: ID!
fullName: String
postalAddresses: [PostalAddress]
}
type PostalAddress {
line1: String
line2: String
city: String
stateCode: String
postalCode: String
postalCodeExtension: String
countryCode: String
}
type Item {
itemId: ID!
fullDescription: String
}
type Query {
findOrdersByCustomerNumber(customerNumber: String): [Order]
}
I have created a root Query class:
#Component
public class Query implements GraphQLQueryResolver {
private OrderService orderService;
#Autowired
public Query(OrderService orderService) {
this.orderService = orderService;
}
public List<Order> findOrdersByCustomerNumber(String customerNumber) {
return this.orderService.findOrdersByCustomerNumber(customerNumber);
}
}
And here is my Order resolver:
#Component
public class OrderResolver implements GraphQLResolver<Order> {
private ItemRepository itemRepository;
private CustomerRepository customerRepository;
#Autowired
public OrderResolver(CustomerRepository customerRepository, ItemRepository itemRepository) {
this.customerRepository = customerRepository;
this.itemRepository = itemRepository;
}
public List<Item> item(Order order) {
return itemRepository.findItemsByOrderNumber(order.getOrderNumber());
}
public List<Customer> customers(Order order) {
return customerRepository.findCustomersByOrderNumber(order.getOrderNumber());
}
}
Everything seems to work fine and I can send a graphql request and get a response. It was actually really easy to implement and impressed with how quickly this library allowed me to do that.
However, here is my issue. It's the dreaded n+1 SQL issue.
So, when I have customer with 162 orders.
1 = Driver SQL (get all the orders for the customer number)
162 = Customer SQL (One query is fired off for each order and it's same customer for each select)
162 = Postal Address SQL (One query is fired off for each customer...note, not included in the code snippets)
162 = Item SQL (assume one item per order).
So, that totals 487 SQL queries. And as a result, this has performance implications. I am using straight JDBC to query the database (no JPA or ORM's at the moment).
My questions is how can I get hold of the GraphQL request in the root Query resolver so that can manipulate the SQL for the dependent objects in the graph? When doing some research, I see that in the Node/Javacript world, there a dataloader utility that can help address this issue (https://github.com/graphql/dataloader)
So, I am unclear on how to solve this with this java implementation. If anyone has any suggestions or sample code, that would be really helpful to see if this POC has any merit.
Related
I want to run some native queries and expose the results through endpoints, but I want to do this without having to create all the entities. I just want the data obtained from the database to be exposed as it comes.
I found some suggestions at: Create spring repository without entity
However, I was not able to make them work. I'm very new to Spring.
I tried Maciej Kowalski's solution like this:
Interface:
public interface CustomNativeRepository {
Object runNativeQuery();
}
Implementation:
#Repository
public class CustomNativeRepositoryImpl implements CustomNativeRepository {
#Autowired
private EntityManager entityManager;
#Override
public Object runNativeQuery() {
return entityManager.createNativeQuery(
"""
SELECT 1 as col1, 2 as col2, 3 as col3
UNION ALL SELECT 4, 5, 6
UNION ALL SELECT 7, 8, 9
"""
)
.getResultList();
}
}
However, no endpoints were exposed, as happens when you extend CrudRepository. Should I have done something else with CustomNativeRepositoryImpl? I don't know how to proceed.
I also tried Gagarwa's solution:
RootEntity:
#Entity
public class RootEntity {
#Id
private Integer id;
}
RootEntityRepository:
#Repository
public interface RootEntityRepository extends JpaRepository<RootEntity, Integer> {
#Query(value = """
SELECT 1 as col1, 2 as col2, 3 as col3
UNION ALL SELECT 4, 5, 6
UNION ALL SELECT 7, 8, 9""",
nativeQuery = true)
public Collection<Object> findFromCustomQuery();
}
The endpoint http://localhost:8080/rootEntities was exposed, but when I accessed it, I got the exception: "Relation root_entity does not exist". So, I created the table in the database:
create table root_entity(
id SERIAL PRIMARY KEY
)
After that, the endpoint worked, and returned an empty array (the table root_entity is empty in the database).
I tried to access the endpoint: http://localhost:8080/rootEntities/search/findFromCustomQuery, but I got an exception (Couldn't find PersistentEntity for type class).
Again, I was not able to make it work.
After trying a lot, I made some progress doing the following:
#RestController
public class CustomQueryController {
#Autowired
private EntityManager entityManager;
#GetMapping("/myEndpoint")
#ResponseBody
public Object runNativeQuery() {
return ResponseEntity
.ok()
.body(
entityManager.createNativeQuery(
"""
SELECT 1 as col1, 2 as col2, 3 as col3
UNION ALL SELECT 4, 5, 6
UNION ALL SELECT 7, 8, 9
"""
).getResultList()
);
}
}
With the code above, I can access http://localhost:8080/myEndpoint and see the result of the query.
However, the endpoint didn't appear in the endpoints listing that is showed in http://localhost:8080/. I had to type it manually in the browser. I would like the endpoint to be exposed in order to see it in Swagger.
Also, I have a feeling that there must be a better way to do this. And I want to learn.
I would like help to:
Get a solution that works and exposes the endpoint.
Understand what I did wrong and how to implement Kowalski's and Gagarwa's solutions.
Being able to expose the endpoint for the last solution (CustomQueryController).
Thanks in advance!
try changing your CustomQueryController to implement RepresentationModelProcessor
public class CustomQueryController implements RepresentationModelProcessor<RepresentationModel<RepositoryLinksResource>> {
and implementing the process method with:
#Override
public RepresentationModel<RepositoryLinksResource> process(RepresentationModel<RepositoryLinksResource> model) {
if (model instanceof RepositoryLinksResource) {
model.add(Link.of( "http://localhost:8080" + "/myEndpoint", "myEndpoint"));
}
return model;
}
https://docs.spring.io/spring-data/rest/docs/current/reference/html/#customizing-sdr.customizing-json-output.representation-model-processor
I tried the first example that you have put here and it worked for me. But there is a bit of change. I have used PersistenceContext.
To return a Link as response I have used Link of WebMvcLinkBuilder.
Solution
In the below example I have used two tables Employee and Address in PostgresSQL . Both have area_code in common.
Interface
public interface CustomNativeRepository {
List<Object> runNativeQuery(Integer name);
}
Repository
#Repository
public class CustomNativeRepositoryImpl implements CustomNativeRepository {
Logger logger = LoggerFactory.getLogger(this.getClass());
#PersistenceContext
private EntityManager entityManager;
#Override
public List<Object> runNativeQuery(Integer areaCode) {
Query query = entityManager.createNativeQuery(
"Select e.first_name as name from employees e where e.area_code = ? "
+ "union all " +
"Select a.address as address from address a where a.area_code = ?");
query.setParameter(1, areaCode);
query.setParameter(2, areaCode);
List<Object> response = query.getResultList();
logger.info("Response from database: {}", response);
return response;
}
}
RestEndpoint Layer
#GetMapping(path ="/employee/{areaCode}")
public ResponseEntity<?> getEmployeeByCode(#PathVariable(value = "areaCode") Integer areaCode) throws NoSuchMethodException {
List<Object> response = customCustomerRepository.runNativeQuery(areaCode);
Link link = WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder.methodOn(EmployeeController.class).getEmployeeByCode(areaCode)).withSelfRel();
return ResponseEntity.ok().body(CollectionModel.of(response, link));
}
Few examples which may help. link1 link2
Note: I have not created any Entity classes in my code base.
Is there a possibility to run some custom SQL query generated via 3rd party tools like e.g. jOOQ and still benefit from Spring Data JDBC mapping features i.e. #Column annotations? I don't want to use #Query annotation.
class Model { #Id private Long id; #Column("column") private String prop; }
class MyRepo {
public Model runQuery() {
val queryString = lib.generateSQL()
// run query as if it has been generated by Spring Data JDBC and map
// results to Model automatically
}
}
Perhaps JdbcTemplate can solve your problem? Specifically, one of the queryForObject() methods may be of interest since your are requesting a single object:
class MyRepo {
private JdbcTemplate jdbcTemplate;
#Autowired
MyRepo(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public Model runQuery() {
val query = lib.generateQuery();
return jdbcTemplate.queryForObject(query, Model.class);
}
}
More information and other use cases can be found in the Spring Guide Accessing Relational Data using JDBC with Spring
Please, look at the two code examples bellow which I'm going to use in my Spring Boot project. They both do merely the same thing - add a new object into users table, represented by User entity with username defined as #Id and a unique constraint imposed on email column (there are some other columns as well, but they are not shown here for brevity). Note: I can't simply use save() method from CrudRepository, because it merges existing record with new object if they both have the same username value. Instead, I need to insert a new object with appropriate exception thrown for duplicate data persistence.
My question is about which option should be given a favor. With EntityManager, I don't need to construct SQL statement. Apart from that obvious observation, are there any advantages which one method may offer over the other (especially, in the matter of performance and resources consumption)?
Also, when I read latest books and tutorials about data persistence in Spring Boot, they mainly focus on Spring Data JPA. For example, the 5th edition of "Spring in Action" has no word about Hibernate's EntityMnager. Does it mean that dealing with Hibernate directly can be regarded as kind of "old school" and should generally be avoided in modern projects?
Option #1: Hibernate's EntityManager
#RestController
#RequestMapping(path = "/auth/register", produces = "application/json")
#Transactional
public class RegistrationController {
#PersistenceContext
EntityManager entityManager;
#PostMapping
#ResponseStatus(HttpStatus.CREATED)
public Map<String, String> registerNewUser(#RequestBody #Valid User newUser) {
try {
entityManager.persist(newUser);
entityManager.flush();
} catch (PersistenceException ex) {
// parse exception to find out which constraints have been
// broken - either it's duplicate username, email or both
String message = parseExceptionForConstraintNames(ex);
throw new ResponseStatusException(HttpStatus.CONFLICT, messsage);
}
return Collections.singletonMap("message", "Success...");
}
}
Option #2: custom #Query from CrudRepository
#RestController
#RequestMapping(path = "/auth/register", produces = "application/json")
public class RegistrationController {
private final UsersRepository usersRepository;
#Autowired
public RegistrationController(UsersRepository usersRepository) {
this.usersRepository = usersRepository;
}
#PostMapping
#ResponseStatus(HttpStatus.CREATED)
public Map<String, String> registerNewUser(#RequestBody #Valid User newUser) {
try {
usersRepository.insert(newUser);
} catch (DataIntegrityViolationException ex) {
// parse exception to find out which constraints have been
// broken - either it's duplicate username, email or both
String message = parseExceptionForConstraintNames(ex);
throw new ResponseStatusException(HttpStatus.CONFLICT, message);
}
return Collections.singletonMap("message", "Success...");
}
}
public interface UsersRepository extends CrudRepository<User, String> {
#Modifying
#Transactional
#Query(nativeQuery = true, value = "INSERT INTO users (username, email) " +
"VALUES (:#{#user.username}, :#{#user.email})")
void insert(#Param("user") User newUser);
}
See this answer for Using JPA repository vs Entity Manager.
Best practice is to not use Repository directly. use Service layer between controller and repository where you can implement the logic for duplicate entries by checking if the record already exist in DB using findByUsername(String username); throw exception if it already exist else save() the object in DB
With the given requirements, the username filed in the entity never qualifies for the #Id.
Why can't u add an explicit id field with some sequence generator for the id filed and just keep the username marked with unique constraint only.
I'm trying simply to save entity into solr using spring data and get its autogenerated id. I see that id is generated but it was not returned back to me. Code is trivial
entity:
#SolrDocument(solrCoreName = "bank")
#Canonical
class Shop {
#Id
#Field
String id
#Field
String name
}
repository:
#Repository
interface ShopRepository extends SolrCrudRepository<Shop, String>{
}
handler:
#Autowired
ShopRepository repository
void save() {
Shop shop = new Shop()
shop.name = 'shop1'
log.info("before {}", shop)
Shop savedShop = repository.save(shop)
log.info("after {}", savedShop)
}
dependencies:
dependencies {
compile lib.groovy_all
compile 'org.springframework.boot:spring-boot-starter-data-solr:1.5.10.RELEASE'
}
and result is:
before com.entity.Shop(null, shop1)
after com.entity.Shop(null, shop1)
however via solr's admin console I see generated id:
{ "responseHeader":{
"status":0,
"QTime":0,
"params":{
"q":"*:*",
"_":"1527472154657"}}, "response":{"numFound":3,"start":0,"docs":[
{
"name":["shop1"],
"id":"4db1eb1d-718b-4a38-b960-6d52f9b6240c",
"_version_":1601670593291223040,
"name_str":["shop1"]},
{
"name":["shop1"],
"id":"6ad52214-0f23-498d-82b8-82f360ef22f1",
"_version_":1601670855078707200,
"name_str":["shop1"]},
{
"name":["shop1"],
"id":"b45b5773-f2b9-4474-b177-92c98810978b",
"_version_":1601670887722975232,
"name_str":["shop1"]}] }}
and repository.findAll() also returns correct result with mapped id. Is it a feature or bug?
The flow is working as expected (no ID available in the returned object):
During the Save operation
Original object is converted in something can be digested by Solr (id is null)
The update request (with the object with null id) is sent to Solr
Solr process the "create" and generate (internally) the ID
Solr response is OK/KO (with few other data...but no ID here)
So...the final object is exactly the same of the original object (id null).
A quick "workaround" can be implemented as:
#Repository
public interface PlaceRepo extends SolrCrudRepository<PlaceModel, String> {
default PlaceModel create(PlaceModel model, Duration commit) {
model.setId(IDGenerator.generateID());
return this.save(model, commit);
}
default PlaceModel create(PlaceModel model) {
return this.create(model, Duration.ZERO);
}
}
You are moving the ID generation logic to the Java layer.
The Id can be generated as
public static String generateID() {
return UUID.randomUUID().toString();
}
Normally I use annotiations:#Query("SELECT c FROM Country c") with JpaRepositoryor predefined methods like findAll
but in my case I want to generate dynamic query.
String baseQuery =SELECT c FROM Country c`
if(age!=null)
baseQuery+="WHERE c.age=20"
I need to perform same query from code level like this:
Query q1 = em.createQuery("SELECT c FROM Country c");
but I dont use EntityManager in spring boot
How can I generate query from code level?
If you would like to create dynamic queries from code you can take advantage of Spring's JdbcTemplate. Using spring boot it is as simple as injecting JdbcOperations bean to your repository class (assuming you have provided spring-boot-starter-jdbc module to your project).
But remember! This solution uses SQL, not JPQL. That's why you have to use proper tables and columns names in queries and properly map result to objects (i.e. using RowMapper)
This simple example worked fine for me (with different entity, but in same manner - I've adapted it to your example):
#Repository
public class CountryRepository {
#Autowired
private JdbcOperations jdbcOperations;
private static String BASIC_QUERY = "SELECT * FROM COUNTRY";
public List<Country> selectCoutry(Long age){
String query = BASIC_QUERY;
if (age != null){
query += " WHERE AGE = ";
query += age.toString();
}
//let's pretend that Country has constructor Conutry(String name, int age)
return jdbcOperations.query(query, (rs, rowNum) ->
{ return new Country(rs.getString("NAME"), rs.getInt("AGE");}
);
};
}
Then in service or whatever you inject CountryRepository and call method.
Since you're using Spring Boot, you can use Spring Data to create queries in your repository:
#Repository
public interface CountryRepository extends JpaRepository<Country, Long> {
}
Not a 100% on syntax, but should be something similar.
Now you can autowire this class:
#Autowired
public CountryRepository countryRepo;
And all basic methods are already available to you like:
countryRepo.findOne(id);
countryRepo.find();
If you want to make more advanced queries, you can use Spring Data e.g.:
#Repository
public interface CountryRepository extends JpaRepository<Country, Long> {
public Country findByNameAndContinent(String name, String continent);
}
This is just an example (a stupid one) of course and assumes your Country class has the field names 'name' and 'continent' and both are strings. More is available here:
http://docs.spring.io/spring-data/jpa/docs/current/reference/html/
Section 5.3 more specifically.
PS: Make sure your Country class has the #Entity annotation