Wednesday, May 24, 2023

Spring Boot 3 Spring Data GraphQL CRUD

GraphQL is an open-source query language and runtime for APIs (Application Programming Interfaces). It was developed by Facebook and released in 2015. GraphQL provides a flexible and efficient way to define, query, and manipulate data in APIs.

At its core, GraphQL enables clients to request specific data from the server using a single API endpoint. Unlike traditional REST APIs that expose fixed endpoints with pre-defined data structures, GraphQL allows clients to specify exactly what data they need and receive it in a hierarchical structure. This reduces over-fetching and under-fetching of data, optimizing network usage and improving performance.

Key features of GraphQL include:

Strong typing: GraphQL uses a type system to define the shape of the data available in the API. This provides a clear contract between the server and clients, enabling better tooling, documentation, and validation.

Hierarchical queries: Clients can request data in a hierarchical structure, specifying nested fields and relationships. This allows clients to fetch related data in a single query, reducing the number of round trips to the server.

Efficient data fetching: GraphQL retrieves data from multiple resources in a single request, avoiding the need for multiple API calls. Clients have fine-grained control over the shape and size of the data they receive.

Mutations: GraphQL supports mutations for modifying data on the server. Clients can send requests to create, update, or delete data, using a similar syntax to queries.

Real-time updates: GraphQL supports subscriptions, allowing clients to receive real-time updates when data changes on the server. This is particularly useful for applications that require real-time features like chat or notifications.

GraphQL is language-agnostic, meaning it can be used with any programming language or framework. There are libraries and tools available for various programming languages to implement GraphQL servers and clients.

Overall, GraphQL provides a flexible and efficient approach to building APIs, empowering clients to request and receive precisely the data they need while reducing network overhead.






HSQLDB (HyperSQL Database) is an open-source, in-memory relational database management system (RDBMS). It is written in Java and provides a lightweight and fast database solution for applications. HSQLDB is often used in Java-based projects, including web applications and testing environments.


Technology

  • Spring Boot 3.0.6
  • HyperSQL
  • Maven 
  • IntelliJ IDEA

Configuration Spring Boot project  :

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.henry</groupId>
<artifactId>spring-for-graphql</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-for-graphql</name>
<description>Demo Spring for GraphQL</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-graphql</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.graphql-java/graphql-java-tools -->
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java-tools</artifactId>
<version>5.2.4</version>
</dependency>

<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.graphql</groupId>
<artifactId>spring-graphql-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>


application.yml:


# HSQLDB Configuration
spring:
datasource:
url: jdbc:hsqldb:mem:testdb
username: sa
password:
driver-class-name: org.hsqldb.jdbc.JDBCDriver

# Hibernate Configuration
jpa:
database-platform: org.hibernate.dialect.HSQLDialect
hibernate.ddl-auto: create-drop

# graphql Configuration
graphql:
graphiql:
enabled: true

logging:
level:
org:
hibernate:
SQL: DEBUG


Schema

Spring for GraphQL application, create a directory src/main/resources/graphql. Add a new file schema.graphqls to this folder with the following content:

type Query {
getAllBooks: [Book]
getAllAuthors: [Author]
getBookById(id: ID!): Book
getAuthorById(id: ID!): Author
}

type Mutation {
createAuthor(firstName: String!, lastName: String! ): Author
updateAuthor(id: ID!, firstName: String!, lastName: String!): Author
deleteAuthor(id: ID!): Boolean
createBook(title: String!, authorId: ID!): Book
updateBook(id: ID!, title: String!): Book
deleteBook(id: ID!): Boolean
}

type Book {
id: ID
title: String!
author: Author!
}

type Author {
id: ID
firstName: String!
lastName: String!
bookRecords: [Book]
}



Spring Data GraphQL Application


Model


package com.henry.model;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.Set;

@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity
@Table(name = "author")
public class Author implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", unique = true, nullable = false)
private Long id;
@Column(name = "first_name")
private String firstName;
@Column(name = "last_name")
private String lastName;

@OneToMany(fetch = FetchType.LAZY, mappedBy = "author")
private Set<Book> bookRecords;


}


package com.henry.model;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity
@Table(name = "book")
public class Book implements Serializable {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "author_id", nullable = false)
private Author author;
}


Repository



package com.henry.repository;

import com.henry.model.Author;
import org.springframework.data.jpa.repository.JpaRepository;

public interface AuthorRepository extends JpaRepository<Author, Long> {

}

package com.henry.repository;

import com.henry.model.Book;
import org.springframework.data.jpa.repository.JpaRepository;

public interface BookRepository extends JpaRepository<Book,Long> {
}


Resolver


package com.henry.resolver;

import com.coxautodev.graphql.tools.GraphQLMutationResolver;
import com.coxautodev.graphql.tools.GraphQLQueryResolver;
import com.henry.model.Author;
import com.henry.model.Book;
import com.henry.repository.AuthorRepository;
import com.henry.repository.BookRepository;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class QueryAndMutationResolver implements GraphQLQueryResolver, GraphQLMutationResolver {

private final BookRepository bookRepository;
private final AuthorRepository authorRepository;


public QueryAndMutationResolver(BookRepository bookRepository, AuthorRepository authorRepository) {
this.bookRepository = bookRepository;
this.authorRepository = authorRepository;
}

public List<Book> getAllBooks() {
return bookRepository.findAll();
}

public Book getBookById(Long id) {
return bookRepository.findById(id).orElse(null);
}

public Iterable<Author> getAllAuthors() {
return authorRepository.findAll();
}

public Author getAuthorById(Long id) {
return authorRepository.findById(id).orElseGet(null);
}

public Author createAuthor(String firstName, String lastName ) {
Author author = new Author();
author.setFirstName(firstName);
author.setLastName(lastName);
return authorRepository.saveAndFlush(author);
}

public Author updateAuthor(Long id, String firstName, String lastName) {
Author author = authorRepository.findById(id).orElse(null);
if (author != null) {
author.setFirstName(firstName);
author.setLastName(lastName);
return authorRepository.saveAndFlush(author);
}
return null;
}

public Boolean deleteAuthor(Long id) {
authorRepository.deleteById(id);
return true;
}

public Book createBook(String title, Long author_id) {

Author author = authorRepository.findById(author_id).
orElseGet(null);

Book book = new Book();
book.setTitle(title);
book.setAuthor(author);
return bookRepository.saveAndFlush(book);
}

public Book updateBook(Long id, String title) {
Book book = bookRepository.findById(id).orElse(null);
if (book != null) {
book.setTitle(title);
return bookRepository.saveAndFlush(book);
}
return null;
}

public Boolean deleteBook(Long id) {
bookRepository.deleteById(id);
return true;
}
}




Controller


package com.henry.controller;

import com.henry.model.Author;
import com.henry.resolver.QueryAndMutationResolver;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.data.method.annotation.SchemaMapping;
import org.springframework.stereotype.Controller;

@Controller
public class AuthorController {
private final QueryAndMutationResolver queryAndMutationResolver;

public AuthorController(QueryAndMutationResolver queryAndMutationResolver) {
this.queryAndMutationResolver = queryAndMutationResolver;
}

@QueryMapping
public Author getAuthorById(@Argument Long id) {
return queryAndMutationResolver.getAuthorById(id);
}

@QueryMapping
public Iterable<Author> getAllAuthors(){
return queryAndMutationResolver.getAllAuthors();
}

@SchemaMapping(typeName = "Mutation", field = "createAuthor")
public Author createAuthor(@Argument String firstName, @Argument String lastName){
return queryAndMutationResolver.createAuthor(firstName, lastName);
}

@SchemaMapping(typeName = "Mutation", field = "updateAuthor")
public Author updateAuthor(@Argument Long id, @Argument String firstName, @Argument String lastName) {
return queryAndMutationResolver.updateAuthor(id,firstName, lastName);
}

@SchemaMapping(typeName = "Mutation", field = "deleteAuthor")
public Boolean deleteAuthor(@Argument Long id) {
return queryAndMutationResolver.deleteAuthor(id);
}
}


package com.henry.controller;

import com.henry.model.Book;
import com.henry.resolver.QueryAndMutationResolver;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.data.method.annotation.SchemaMapping;
import org.springframework.stereotype.Controller;

import java.util.List;

@Controller
public class BookController {

private final QueryAndMutationResolver queryAndMutationResolver;

public BookController(QueryAndMutationResolver queryAndMutationResolver) {
this.queryAndMutationResolver = queryAndMutationResolver;
}


@QueryMapping
public Book getBookById(@Argument Long id) {
return queryAndMutationResolver.getBookById(id);
}

@QueryMapping
public List<Book> getAllBooks(){
return queryAndMutationResolver.getAllBooks();
}

@SchemaMapping(typeName = "Mutation", field = "createBook")
public Book createBook(@Argument String title, @Argument Long authorId){
return queryAndMutationResolver.createBook(title, authorId);
}

@SchemaMapping(typeName = "Mutation", field = "updateBook")
public Book updateBook(@Argument Long id, @Argument String title) {
return queryAndMutationResolver.updateBook(id,title);
}

@SchemaMapping(typeName = "Mutation", field = "deleteBook")
public Boolean deleteBook(@Argument Long id) {
return queryAndMutationResolver.deleteBook(id);
}
}


Run & Test

Run Spring Boot application with command: mvn spring-boot:run. by console, IntelliJ etc.

Boot the application

Start your Spring application. Navigate to http://localhost:8080/graphiql.


Run the queries

Type in the queries and click the play button at the top of the window.












Or Postman

POST
http://localhost:8080/graphql

















Author queries

mutation { createAuthor( firstName: "The Author Title", lastName: "test" ) { id firstName lastName } } mutation { updateAuthor( id: 3, firstName: "Test1", lastName: "test123" ) { id firstName lastName } } mutation { deleteAuthor( id: 3 ) } query { getAuthorById(id: 1) { firstName lastName bookRecords { id } } } query { getAllAuthors { id firstName lastName bookRecords { id } } }


Book queries

mutation {
  createBook(
    title: "The Book Title", 
    authorId: 1
   ) 
  {
    id
    title
    author {
      id
      firstName
      lastName
    }
  }
}

mutation {
  updateBook( 
    id: 2,
    title: "Test1"
   ) 
  {
    id
    title
    author {
      id
      firstName
      lastName
    }
  }
}

mutation {
  deleteBook( 
    id: 3
   ) 
}

query  {
  getBookById(id: 1) {
    title
    author {
      id
      firstName
      lastName
    }
  }
}

query {
  getAllBooks
  {
    id
    title
    author {
      id
      firstName
      lastName
    }
  }
}



Source Code


Here on GitHub.











Tuesday, May 9, 2023

Spring Boot 3 Spring Data Elasticsearch

 

Elasticsearch: 

By official documentation Elasticsearch is a distributed, free and open search and analytics engine for all types of data, including textual, numerical, geospatial, structured, and unstructured. Elasticsearch is built on Apache Lucene and was first released in 2010 by Elasticsearch N.V. (now known as Elastic).



Query DSL ?

Query DSL stands for Query Domain-Specific Language, which is a powerful feature of Elasticsearch that enables users to specify search queries in a flexible and intuitive way. It's a JSON-based language that allows you to define complex search queries with a simple and concise syntax.

Query DSL allows you to search for specific documents in your Elasticsearch index based on a wide range of criteria, such as keyword match, ranges of values, geographic locations, and more. You can also use Query DSL to apply filters to your search results and to specify sorting and aggregations.

Technology


  • Spring Boot 3.0.6
  • Docker
  • Maven 
  • Elasticsearch 7.17.5
  • IntelliJ IDEA

Configuration Spring Boot project  :


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.henry</groupId>
<artifactId>SpringDataElasticsearchQueryDsl</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>SpringDataElasticsearchQueryDsl</name>
<description>Demo Spring boot + elasticsearch + querydsl</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>jakarta.json</groupId>
<artifactId>jakarta.json-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>


application.yml:


server:
port: 9000
servlet:
context-path: /

elasticsearch:
host: localhost
port: 9200




Spring Data ElasticSearch Application

Configuration


package com.henry.configuration;

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ElasticsearchConfig {

@Value("${elasticsearch.host}")
private String EsHost;

@Value("${elasticsearch.port}")
private int EsPort;

@Bean
public ElasticsearchTransport client() {

// Create the low-level client
RestClient restClient = RestClient.builder(
new HttpHost(EsHost, EsPort)).build();

// Create the transport with a Jackson mapper
ElasticsearchTransport transport = new RestClientTransport(
restClient, new JacksonJsonpMapper());

return transport;
}

// And create the API client
@Bean
public ElasticsearchClient elasticsearchClient() {
return new ElasticsearchClient(client());
}
}


Model


package com.henry.model;

import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;
import org.springframework.data.elasticsearch.annotations.Document;

@Document(indexName = "users")
public class
User {

@Id
private Long id;
private String name;
private String lastName;

@Transient
private String _class;

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getLastName() {
return lastName;
}

public void setLastName(String lastName) {
this.lastName = lastName;
}

public String get_class() {
return _class;
}

public void set_class(String _class) {
this._class = _class;
}
}



Repository


package com.henry.repository;

import com.henry.model.User;
import org.springframework.data.repository.CrudRepository;

public interface UserRepository extends CrudRepository<User,Long> {

Iterable<User> findByName(String name);
}


Service


package com.henry.service;

import com.henry.model.User;

public sealed interface UserService permits UserServiceImpl {

User save(User user);

User update(Long id, User user);

void delete(Long id);

User findOne(Long id);

Iterable<User> findAll();

Iterable<User> findByName(String name);

}


ServiceImpl


package com.henry.service;

import com.henry.model.User;
import com.henry.repository.UserRepository;
import org.springframework.stereotype.Service;

@Service
public final class UserServiceImpl implements UserService {

private final UserRepository userRepository;

public UserServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}

@Override
public User save(User user) {
return userRepository.save(user);
}

@Override
public User update(Long id, User user) {
var obj = userRepository.findById(id);
obj.get().setName(user.getName());
obj.get().setLastName(user.getLastName());
return userRepository.save(obj.get());
}

@Override
public void delete(Long id) {
userRepository.deleteById(id);
}

@Override
public User findOne(Long id) {
return userRepository.findById(id).get();
}

@Override
public Iterable<User> findAll() {
return userRepository.findAll();
}

@Override
public Iterable<User> findByName(String name) {
return userRepository.findByName(name);
}
}


Controller


package com.henry.controller;

import com.henry.model.User;
import com.henry.service.UserService;
import org.springframework.web.bind.annotation.*;


@RestController
@RequestMapping("/users")
public class UserController {

private final UserService userService;

public UserController(UserService userService) {
this.userService = userService;
}

@PostMapping("/save")
public User add(@RequestBody User user) {
return userService.save(user);
}

@PutMapping("/update/{id}")
public User update(@PathVariable Long id, @RequestBody User user) {
return userService.update(id, user);
}

@GetMapping("/findOne/{id}")
public User findOne(@PathVariable Long id) {
return userService.findOne(id);
}

@GetMapping("/all")
public Iterable<User> findAll() {
return userService.findAll();
}

@GetMapping("/findByName/{name}")
public Iterable<User> findByName(@PathVariable String name) {
return userService.findByName(name);
}

@DeleteMapping("/delete/{id}")
public void delete(@PathVariable Long id) {
userService.delete(id);
}

}


Resource


package com.henry.resource;

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.Hit;
import com.henry.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

@Component
public class SearchQueryBuilder {

@Autowired
private ElasticsearchClient client;

public List<User> getByName(String text) throws IOException {
var list = new ArrayList<User>();
SearchResponse<User> search = client.search(s -> s
.index("users")
.query(q -> q
.term(t -> t
.field("name")
.value(v -> v.stringValue(text))
)),
User.class);

for (Hit<User> hit: search.hits().hits()) {
list.add(hit.source());
}

return list;
}

}

package com.henry.resource;

import com.henry.model.User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.util.List;

@RestController
@RequestMapping("/user-resource")
public class UserResource {

private final SearchQueryBuilder searchQueryBuilder;

public UserResource(SearchQueryBuilder searchQueryBuilder) {
this.searchQueryBuilder = searchQueryBuilder;
}

@GetMapping("/{name}")
public List<User> getByName(@PathVariable String name) throws IOException {
return searchQueryBuilder.getByName(name);
}
}

Downloading and installing Elasticsearch

docker run -d --name elasticsearch -p 9200:9200 -e "discovery.type=single-node" elasticsearch:7.17.5


Run & Test


Run Spring Boot application with command: mvn spring-boot:run. by console, IntelliJ etc.

POST
http://localhost:9000/users/save
















GET
http://localhost:9000/user-resource/henry

















GET
http://localhost:9200/_search?q=Henry
















GET
http://localhost:9000/users/findByName/henry


















Source Code


Here on GitHub.



References.

https://www.elastic.co/what-is/elasticsearch
https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/connecting.html








Multiple Data Sources in Spring Boot 3 with Java 21

  In this blog post, we'll explore the configuration and setup for a Spring Boot 3 application with Java 21 that uses multiple data sour...