Spring Data R2DBC how to query hierarchical data - java

I am new to Reactive programming. I have to develop a simple spring boot application to return a json response which contains the company details with all its sub companies and employees
Created a spring boot application (Spring Webflux + Spring data r2dbc )
Using following Database Tables to represent the Company and Sub Company and Employee relationship (it is a hierarchical relationship with Company and Sub Company where a company can have N number of sub companies, and each of these sub companies can have another N number of sub companies etc and so on)
Company
id
name
address
Company_SubCompany
id
sub_company_id (foreign key references id of above company table )
Employee
id
name
designation
company_id ( foreign key references id of above company table )
Following are the java model classes to represent the above tables
Company.java
#Data
#ToString
#AllArgsConstructor
#NoArgsConstructor
#EqualsAndHashCode
public class Company implements Serializable {
private int id;
private String name;
private String address;
#With
#Transient
private List<Company> subCompanies;
#With
#Transient
private List<Employee> employees;
}
Employee.java
#Data
#ToString
#AllArgsConstructor
#NoArgsConstructor
#EqualsAndHashCode
public class Employee implements Serializable {
#Id
private int id;
private String name;
private String designation;
}
Following repositories are created
#Repository
public interface CompanyRepository extends ReactiveCrudRepository<Company, Integer> {
#Query("select sub.sub_company_id from Company_SubCompany sub inner join Company c on sub.sub_company_id = c.id where sub.id = :id")
Flux<Integer> findSubCompnayIds(int id);
#Query("select * from Company c where id = :id")
Mono<Company> findCompanyById(Integer id);
}
#Repository
public interface EmployeeRepository extends ReactiveCrudRepository<Employee, Integer> {
#Query("select * from Employee where company_id = :id")
Flux<Employee> findEmployeeByCompanyId(Integer id);
}
In Company_SubCompany table, the super company is represented with id -1. So using this id we are now able to get the super parent company .
With the below service code i am now able to get first level of company and its employees. But i am not sure how to get all the nested sub companies and add to it
#Service
public class ComanyService {
#Autowired
CompanyRepository companyRepository;
#Autowired
EmployeeRepository employeeRepository;
public Flux<Company> findComapnyWithAllChilds() {
Flux<Integer> childCompanyIds = companyRepository.findSubCompnayIds(-1);
Flux<Company> companies = childCompanyIds.flatMap(p -> {
return Flux.zip(companyRepository.findCompanyById(p),
employeeRepository.findEmployeeByCompanyId(p).collectList(),
(t1, t2) -> t1.withEmployees(t2));
});
return companies;
}
}
I am very new to reactive, functional programing and r2dbc, so please help me to how to resolve my problem. If there is any other better approach available will use that one also. The requirement is to get the company , all its employees and sub companies (upto N leavel) .

this code can help you, this approach fill the List objects from database calls
public Flux<Company> findComapnyWithAllChilds(){
return companyRepository.findAll()
.flatMap(companies ->
Mono.just(companies)
.zipWith(employeeRepository.findEmployeeByCompanyId(companies.getId()).collectList())
.map(tupla -> tupla.getT1().withEmployees(tupla.getT2()))
.zipWith(anotherRepository.findAnotherByCompanyId(companies.getId()).collectList())
.map(tupla -> tupla.getT1().withAnother(tupla.getT2()))
);
}

Related

Spring Data JPA how to specify Join type or Fetch Mode when using get("property") chain vs Join

I have two (Hibernate-based) Spring Data JPA domain classes, the "One" side Customer.class:
#Entity
#Table(name = "sys_customer")
#Data
public class Customer implements Serializable {
#Id
#Column(name = "cust_id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "cust_name")
private String customerName;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "customer")
private Set<Order> orders;
}
and the "Many" side Order.class:
#Entity
#Table(name = "sys_order")
#Getter
#Setter
#AllArgsConstructor
#NoArgsConstructor
public class Order implements Serializable {
#Id
#Column(name = "order_id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "order_name")
private String orderName;
#ManyToOne
#JoinColumn(name = "order_cust_id", referencedColumnName = "cust_id")
private Customer customer;
public Order( String orderName) {
this.orderName = orderName;
}
public Order(String orderName, Customer customer) {
this.orderName = orderName;
this.customer = customer;
}
}
I have OrderRepository interface which extends JpaRepository interface and JpaSpecificationExecutor interface:
public interface OrderRepository extends JpaRepository<Order, Long>, JpaSpecificationExecutor<Order> {
}
I have a OrderSpecification.class with the static method searchByCustomerName:
public class OrderSpecification {
public static Specification<Order> searchByCustomerName(String customerName) {
return new Specification<Order>() {
#Override
public Predicate toPredicate(Root<Order> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
Join<Order, Customer> join = root.join("customer");
return criteriaBuilder.like(join.get("customerName"), "%" + customerName + "%");
//return criteriaBuilder.like(root.get("customer").get("customerName"), "%" + customerName + "%");
}
};
}
}
To find the differences between get("property") chain and Join, I wrote a simple test method and comment out the above OrderSpecificatin.class code
#Test
#Transactional
public void testFindOrderByCustomerName(){
String name = "adam";
List<Order> orders = orderRepository.findAll(OrderSpecification.searchByCustomerName(name));
for(Order order: orders){
Customer customer = order.getCustomer();
log.info(new StringBuilder().append(customer.getId()).append(" ").append(customer.getCustomerName()).toString());
}
}
I found that:
get("property") chain use a cross-join(which is very bad performancing) while Join use inner-join(since ManyToOne() by default is Fetch= FetchType.EAGER)
/* get("property") chain: Hibernate: select order0_.order_id as
order_id1_1_, order0_.order_cust_id as order_cu3_1_,
order0_.order_name as order_na2_1_ from sys_order order0_ cross join
sys_customer customer1_ where order0_.order_cust_id=customer1_.cust_id
and (customer1_.cust_name like ?) Hibernate: select customer0_.cust_id
as cust_id1_0_0_, customer0_.cust_name as cust_nam2_0_0_ from
sys_customer customer0_ where customer0_.cust_id=? */
/** * "Join": * Hibernate: select order0_.order_id as order_id1_1_,
order0_.order_cust_id as order_cu3_1_, order0_.order_name as
order_na2_1_ from sys_order order0_ inner join sys_customer customer1_
on order0_.order_cust_id=customer1_.cust_id where customer1_.cust_name
like ? * Hibernate: select customer0_.cust_id as cust_id1_0_0_,
customer0_.cust_name as cust_nam2_0_0_ from sys_customer customer0_
where customer0_.cust_id=? */
My questions are:
Can I specify the Join type(inner, all three outers) or Fetch Type(LAZY, EAGER) when using get("property") chain approach to avoid cross-join?
What scenario/best practice should I use get("chain") or always stay in Join?
Does the approach OrderSpecification.class with static method obey a good OOP design pattern?
You can't specify the join type for paths. It will use INNER join semantics by default and that is mandated by the JPA specification. If you want a different join type, you will have to create joins explicitly. The fact that using get renders as cross joins is a limitation of the old query model of Hibernate, but Hibernate 6.0 will fix this. The semantics are the same though and the query planner of your database should be able to treat both queries the same way. Maybe you just need to update your database version?
There is no "best practice" i.e. this really depends on your needs. Explicit joins are just that, explicit. So multiple calls to join will create multiple joins in SQL.
As for the OOP question, I think this is fine, yes.

How to make a paginated repository call from column entity in Spring Boot JPA

I have this entity model class (Book) where an author can have written multiple books.
#Entity
#Table(name="book")
public class Book {
#Id
#GeneratedValue
private Long id;
#Column(name="book_name")
private String bookName;
#Column(name="author_id")
private Long authorId;
//Setters and getters
}
In my Spring Boot project, I don't want to have an author table since there is a third part service that defines authors and their ids, how could I make a paginated repository call for all authorIds and their books?
I would want to have an endpoint that takes in (page, size) and returns a paginated list of a AuthorDTO like so:
public abstract class AuthorDTO implements Serializable {
public abstract Long authorId();
public abstract List<Book> books();
}
[
{
"authorId": 123,
"books": [...]
},
...
]
My first thought is to create a repository call not sure how we can get a page of a custom object. This is not valid below, but I would like to do something like the following.
Page<AuthorDTO> findAllBooksGroupedByAuthorId(Pageable pageable);
Your code seems to suggest you are trying to show the foreign key relationship in the class as an id.
JPA doesn't really do that.
JPA = "Java Persistence Language" i.e. you represent the relations between Java classes that mirror the database.
So in the database you may have a foreign key like 'author_id' in the book table, but in JPA/Java side it will be an "Author" class and not just a long/int.
I hope the below helps. Iv'e just slapped it on the main() of my code so it may not be perfect but I have left some comments as well.
Once you have a Page<Book> you may then want to map it to the DTO in java.
As the query is "get books by author id" we can assume that they all have the same author ID...so there is no real need to try get this projection in the database.
EDIT: Is it not at all possible to have a reference to the author from the 3rd party?
I.e. I don't know how you are populating "Book"...but could you not as you get "Book" from the 3rd party see if you have an Author entity with the books 'author_id' and not persist a new "Author" with that ID if it doesn't already exist?
In this case you can then do an AuthorRepo and simply query like:
Page<Author> findAllBy(Pageable page)
==========================================================================
Seeming as you are fetching a Page of Books by an author Id...you should really have a JPA relationship to show that:
#Entity
private class Book{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#Column(name = "book_name")
private String name;
//Technically this could be Many:Many as a book could have 2 authors? If so....#ManyToMany
//For simplicity (and what you seem to want) Many Books have ONE author.
#ManyToOne(fetch = FetchType.LAZY)
private Author author;
}
#Entity
private class Author{
//ID here - omitted for clarity
#Column(name = "authors_name")
String name;
//The Author has many books.
// Mapped by shows the bi-direction relationship. You can then do 'Author.getAuthorsBooks()'
//Lazy means it wont fetch all the books from database/(hibernate wont) when you do AuthorRepo.get()
//and will only do the `JOIN ON Books where` if you do Author.getAuthorsBooks()
#OneToMany(fetch = FetchType.LAZY,mappedBy = "author")
private Set<Book> authorsBooks = new HashSet<>();
}
private interface AuthorRepo extends JpaRepository<Author,Long>{
//Note the JPA syntax.
Page<Book> findAll(Pageable pageable);
}
EDIT:
I have only written this in an empty file...so it may need tweaking or has typos etc
If you can NOT have a separate entity for some reason for Author, having to keep your entity as it currently is...I'd do 2 queries.
I feel you can either do this in various ways.
If you MUST stick with spring's Pageable:
Get the page request in the controller and make it in to a new PageRequest.of(pagenum,size)
and feed it in to do the Page query below
List<Long> getPageOfUniqueAuthorIds(Pageable pageable);
This will give a page of author Ids.
Then you want to use that List of Longs (aithorIds) to do the second query.
List<AuthorDTOProjection> getBooksAndAuthorIdsWithAuthorsIdsIn(List<Long> authorIds);
#Entity
#Table(name="book")
public class Book {
#Id
#GeneratedValue
private Long id;
#Column(name="book_name")
private String bookName;
#Column(name="author_id")
private Long authorId;
//Setters and getters
}
private interface BookRepo extends JpaRepository<Book,Long> {
//The countQuery is required by Spring Paging.
//Hibernate will need to use the count query when doing paging on a native query.
#Query(nativeQuery = true,
value = "SELECT DISTINCT(author_id) FROM book b ",
countQuery = "SELECT count(*) \n" +
"FROM (SELECT DISTINCT(author_id) FROM book b) authorIds ")
List<Long> getPageOfUniqueAuthorIds(Pageable pageable);
//This is not paged. You want all books with the author IDs from the page query above.
List<Book> findAllByAuthorIdIn(List<Long> authorIds);
}
You will then have to map the Entity to the DTO in your service layer.
#Autowired
BookRepo bookRepo;
//This would be from the controller method...not declared here...
Pageable pageableFromController = PageRequest.of(0,10);
List<Long> pageOfUniqueAuthorIds = bookRepo.getPageOfUniqueAuthorIds(pageableFromController);
//Get All the books with Author Ids.
List<Book> books = bookRepo.findAllByAuthorIdIn(pageOfUniqueAuthorIds);
//Your abstract AuthorDTO.
abstract class AuthorDTO implements Serializable {
public abstract Long authorId();
public abstract List<Book> books();
}
//Your Author DTO needs to be implemented so I made a "View".
#AllArgsConstructor
class AuthorView extends AuthorDTO{
private long authorId;
private List<Book> books;
#Override
public Long authorId() {
return authorId;
}
#Override
public List<Book> books() {
return books;
}
}
//Get a List of the authorIds in the List<Books>. Could also use the original Page<Long> authorIds...
//As an author without a book is not possible in your database.
final List<Long> authorIdsInBooks = books.stream().map(it -> it.authorId).distinct().collect(Collectors.toList());
//Map the Ids of authors to an Impl of your abstract DTO. Personally I don't see why the AuthorDTO is abstract.
//I'd have expected just an abstract DTO class called "DTO" or something and then AuthorDTO impl that.
//But as the way you have it this will work. I guess you may want more impl of the AuthorDTO so maybe leave the AuthorDTO as abstract.
//This can be returned to client.
final List<AuthorView> authorViews = authorIdsInBooks.stream()
.map(authorId -> new AuthorView(
authorId,
books.stream().filter(it -> it.authorId.equals(authorId)).collect(Collectors.toList()))
)
.collect(Collectors.toList());

Is there a way to replicate JPARepository methods for same structure tables?

Bit of context : I am using five tables with same structure, as follows :
#Entity
#Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Book {
#Column(name = "BOOK_ID")
private String bookId;
#Column(name = "CUSTOMER_ID")
private String customerId;
#Column(name = "AUTHOR")
private String author;
#Column(name = "YEAR")
private Integer year;
#Id
#Column(name = "ID")
private Integer id;
}
Then all five tables look like this :
#Data
#Entity
#Table(name = "FANTASTIC_BOOK")
public class FantasticBook extends Book {
}
I would like to get the correct bookId regarding the table it is in. The 5 tables are mandatory.
My repository looks like this :
#Repository
public interface BookRepository extends JpaRepository<Book, Integer> {
List<? extends Book> findByBookId(String bookId);
}
I thought the wildcard would help by saying to Jpa "hey, the request comes from Fantastic / Horror Book so you should use the method in their respective table".
But it's not. If I run this, the result is :
[FantasticBook(), FantasticBook(), FantasticBook(), FantasticBook(), FantasticBook(), HorrorBook(), HorrorBook(), HorrorBook(), HorrorBook(), HorrorBook()]
But my call looks like this :
MedievalBookList = (List<MedievalBook>) BookRepository.findByBookId(bookId);
My interpretation here is that, I have this "BookId" in all 5 tables to test Data. It means that, with the wildcard, it actually looks in every child of the Book.class , which is something I don't want.
Is there a way to bypass this ?
Also, a solution I had was to make the repo like this :
#Repository
public interface BookRepository extends JpaRepository<Book, Integer> {
List<MedivalBook> findByBookId(String bookId);
}
But if I do this, I can't make the 5 same queries.
Any of you got a solution ?
That's the way how JPA Inheritance works.
If you query for a Book you get all Books incl. the Subclasses and if you query for MedivalBook you only get the MedivalBooks.
On solution could be to use a custom query like:
#Repository
public interface BookRepository extends JpaRepository<Book, Integer> {
#Query("SELECT b FROM Book b WHERE TYPE(b) = :type and b.bookId = :bookId")
List<Book> findByBookId(Class type, String bookId);
}
The call would then be:
findByBookId(MedivalBook.class, "The ID");

Access elements of Join Tables in spring through Service

I am trying to learn it and I am facing some conceptual issues.
Let's imagine to have the entities of Employee, Project and Department where:
Employee - Department is a ManyToOne relationship
Employee - Project is a ManyToMany relationship
Right now I have written this:
Employee.java
#Entity
public class Employee {
#Id #GeneratedValue
private Long id;
private String name;
#ManyToOne
#JoinColumn(name="department_id")
private String department;
#ManyToMany
#JoinTable(name="project_employee",
joinColumns=#JoinColumn(name="project_id", referencedColumnName="id"),
inverseJoinColumns=#JoinColumn(name="employee_id", referencedColumnName="id"))
private ArrayList<Project> projects;
//constructors, getters, setters...
}
Project.java
#Entity
public class Project {
#Id #GeneratedValue
private Long id;
private String client;
#ManyToMany(mappedBy="project")
private ArrayList<Employee> employees;
//constructors, getters, setters...
}
Department.java
#Entity
public class Department {
#Id #GeneratedValue
private Long id;
private String name;
#OneToMany(mappedBy="department")
private ArrayList<Employee> employees;
//constructors, getters, setters...
}
EmployeeService.java
#Service
public class EmployeeService {
#Autowired
private EmployeeRepository employeeRep;
#Autowired
private ProjectRepository projectRep;
#Autowired
private DepartmentRepository deptRep;
public List<Project> getProjectsByEmployee(Long emplId){
}
}
The first solution that came in my mind to implement that methods was:
Get the specific employee object through the employeeRep and then loop over its list project retrieving ids and using them to retrieve project info with projectRep.
But in this case it seems I am not exploiting the table relation I have between Project and Employee, so what's the point on having it then?
But in that case it seems to me that I am not exploiting the table
relation I have between Project and Employee, so what's the point on
having it then?
In fact you exploit that ManyToMany table when you access to the Project relationship from an Employee entity (JPA performs a query for you at that time) but you don't exploit it efficiently. I will explain.
The first solution that came in my mind to implement that methods was:
Get the specific employee object through the employeeRep and then loop
over its list project retrieving ids and using them to retrieve
project info with projectRep.
That is bad idea because you don't want to perform a lot of queries while a single one rather simple could achieve the same thing.
Similarly defining a repository by entity is not mandatory :
#Autowired
private EmployeeRepository employeeRep;
#Autowired
private ProjectRepository projectRep;
#Autowired
private DepartmentRepository deptRep;
if according to your requirements, your queries rely on a pivot table (for example the employee table), just declare :
#Autowired
private EmployeeRepository employeeRep;
And perform the join from employee to project to get the list of projects for a specific employee.
In JPQL you could use fetch join on projects to get projects associated to the matching employee such as :
public List<Project> getProjectsByEmployee(Long empId){
String jpql = "SELECT emp FROM Employee e" +
"INNER JOIN FETCH e.projects" +
"WHERE emp.id=:empId";
TypedQuery<Employee> query = em.createQuery(jpql, Employee.class)
.setParameter("empId", empId);
Employee emp = query.getSingleResult();
return emp.getProjects();
}

Spring data JPA: How to use abstract entity

I have a few tables named MEMBER, PROVIDER, EMPLOYER etc and they have few common columns like ID, ADDRESS etc. I'm using hibernate to fetch data from these tables using Spring Data JPA. Since I have many similar tables, I created a super entity class and wrote a common method to fetch the data. Here is my super class.
#Entity
#Getter
#Setter
#NoArgsConstructor
#ToString
public abstract class Recipient implements Serializable {
#Id
#Column(name = "RECIPIENT_ID", unique = true, nullable = false)
private Long id;
}
Here is a sample entity which extends this super class.
#Entity
#Getter
#Setter
#NoArgsConstructor
#ToString
#Table(name = "MEMBER")
public class Member extends Recipient {
#Column(name = "Member_NAME")
private String membername;
}
Here is the Service
public class RecipientServiceImpl extends AbstractService<Recipient>
implements RecipientService {
#Override
public Recipient findByIdAndType(long id, String type) {
Recipient recipient = null;
switch (RecipientType.get(type)) {
case MEMBER:
recipient = recipientRepository.findMemberById(id);
case PROVIDER:
recipient = recipientRepository.findProviderById(id);
case EMPLOYER:
recipient = recipientRepository.findEmployerById(id);
}
}
And here is the repository
public interface RecipientRepository
extends JpaRepository<Recipient, Long>, JpaSpecificationExecutor<Recipient> {
public Member findMemberById(long id);
public Provider findProviderById(long id);
public Employer findEmployerById(long id);
}
Obviously it didn't work since there weren't any tables mapped in the Recipient abstract entity. Is there a way to implement what I was hoping for as a generic fetching method instead of having to create repositories for each entities ?

Categories

Resources