Spring Data Jpa One method for multiple findBy Queries - java

I have this JobPosting class and a repository interface for this entity. I wanna be able to search for all combinations of company, skills, title and city fields of JobPosting class. The only way I know is creating a different method in repository interface . For instance for searching by city and title i need to create a method named findByCityIdAndTitleContaining(Long id,String title). For searching by skills and title i need to create a method named LfindBySkillsIdAndTitleContaining(Long id,String title). The problem is if i create different method for each possibility there will be too many methods. Is there any better way ?
Entity (I didn't include get set methods here):
#Entity
public class JobPosting
{
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
#ManyToOne
private Company company;
private Date publishedAt;
private String title,description;
private boolean isActive;
#ManyToOne
private City city;
#ManyToMany
#JoinTable(name="job_posting_skill",joinColumns=#JoinColumn(name="job_posting_id"),inverseJoinColumns=#JoinColumn(name="skill_id"))
private Set<Skill> skills;
}
Repository:
package app.repository;
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import app.entity.JobPosting;
public interface JobPostingRepository extends JpaRepository<JobPosting,Long>
{
Page<JobPosting> findAll(Pageable pageable);
List<JobPosting> findByCompanyId(long companyId);
List<JobPosting> findByTitleContaining(String title);
List<JobPosting> findBySkillsId(Long id);
List<JobPosting> findByCityId(Long id);
}

Spring Data provides JpaSpecificationExecutor which can fit your requirements.
https://docs.spring.io/spring-data/jpa/docs/current/api/org/springframework/data/jpa/repository/JpaSpecificationExecutor.html
List<T> findAll(Specification<T> spec)
Returns all entities matching the given Specification.
A Specification has a predicate method:
interface Specification<T> {
Predicate toPredicate(Root<T> root, CriteriaQuery query, CriteriaBuilder cb);
}
It has a CriteriaBuilder, so in theory you will still need to define what exactly you need to match for, however you wont have to create multiple findByXYZ.
To be able to use findAll(Specification), your repository needs to extend org.springframework.data.jpa.repository.JpaSpecificationExecutor<T>:
Example of usage:
https://www.baeldung.com/spring-data-criteria-queries

Related

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());

How to select only certain fields with Quarkus Panache?

Quarkus simplifies Hibernate ORM mappings with Panache.
Here is an example of my entity and PanacheRepository:
#Entity
public class Person {
#Id #GeneratedValue private Long id;
private String firstName;
private String lastName;
private LocalDate birth;
private Status status;
}
#ApplicationScoped
public class PersonRepository implements PanacheRepository<Person> {
// example
public Person findByName(String name){
return find("name", name).firstResult();
}
// ! and this is what I tried, but it's not possible to do it this way
// all the methods return Person or something of type Person like List<Person>
// so basically this won't even compile
public List<String> findAllLastNames() {
return this.find("select p.lastName from Person p").list();
}
}
All the guides explain how to write different queries, but is not clear how to select only certain attributes.
If I don't need the whole Person object, but rather the lastName of all persons in my DB?
Is it possible to select only certain attributes with Quarkus Panache?
This is currently not possible, you can subscribe to this issue regarding projection for Hibernate with Panache: https://github.com/quarkusio/quarkus/issues/6261
Don't hesistate to vote for it (+1 reaction) and provides feedback.
As #aksappy said, this feature was added and it's documentation is available here.

Deleting in relationships in Java SpringBoot

Good Day developers , i'm hardly striving with this problem on my App which use SpringBoot framework.Basically can't put two and two together about how deleting one of the items in the relation ship once its parent is delete. Here my explanation:
First both entities with its respective relation to each other:
Product(Children)
#Entity
public class Product {
#Id
#GeneratedValue(strategy = GenerationType.AUTO,generator = "native")
#GenericGenerator(name="native",strategy="native")
private Long id;
#OneToMany(mappedBy = "products",fetch= FetchType.EAGER,cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Category> categorySet= new HashSet<>();
CONSTRUCTOR FOR PRODUCTS ENTITY
-------------------------------------GETTERS AND SETTERS---------------------------------
Being this the Product entity under the premise of one product being able to clasify to several categories hence its relation OnetoMany.Then:
Categories(Parent)
#Entity
public class Category {
#Id
#GeneratedValue(strategy = GenerationType.AUTO,generator = "native")
#GenericGenerator(name="native",strategy="native")
private Long id;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name="product_id")
private Product products;
CONSTRUCTOR FOR CATEGORY ENTITY
---------------------------GETTERS AND SETTERS-----------------------------
Following the former concept but withan inverse logic applied Category reltion toward products, and works perect on my database.
on repositories lets say i set this
Category Repository
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import java.util.Collection;
#RepositoryRestResource
public interface CategoryRepository extends JpaRepository <Category,Long> {
}
Product Repository
package com.miniAmazon;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.*;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
#RepositoryRestResource
public interface ProductRepository extends CrudRepository<Product,Long> {
Product findByProductName (String productName);
}
Then trying to set the command to delete products or categories from my Jpa and Crud Reps, using Junit Test on the Category Entity, like this:
Category Entity
#Test
public static void whenDeletingCategories_thenProductsShouldAlsoBeDeleted() {
ProductRepository.deleteAll();
assert(CategoryRepository.count()).isEqualTo(0);
assert(ProductRepository.count()).isEqualTo(0);
}
#Test
public static void whenDeletingProducts_thenCategoriesShouldAlsoBeDeleted() {
CategoryRepository.deleteAll();
assert(CategoryRepository.count()).isEqualTo(0);
assert(ProductRepository.count()).isEqualTo(2);
}
Throws me an error saying that "Non-static method 'deleteAll()/count()' cannot be referenced from a static context".
Any idea about why this is happening .Any advice ?.Thanks in advance!!!!.Have a good day!!!
Try using instantiated beans CategoryRepository and ProductRepository instead of the interfaces.
You are try to use Non-static method of interface but deleteAll() or count() are not static method. Try to create a repository object then autowired it to call deleteAll() / count() method.
#Autowired
private CategoryRepository categoryRepository;
And use categoryRepository to call call deleteAll() / count() method
categoryRepository.deleteAll();
assert(categoryRepository.count()).isEqualTo(0);

Query DSL advance sorting with spring pageable

) I have some problem with sorting and spring pageable when I use QueryDSL. I need quite advanced sorting, not just by the fields of the object stored in the database in the same table.
This is my model in approx:
#Getter
#Setter
#Entity
public class Book {
#Id
#GeneratedValue
private long id;
#OneToMany
private Set<Category> cats = new HashSet<>()
}
#Getter
#Setter
#Entity
public class Category{
private long id;
private Name name;
}
public enum Name{
WINTER,
SUN,
SUMMER
}
Now, this what I want to do is sort (desc and asc) Book (I have many Books) by Category id only if Category has name SUN. And i want to pass Qsort as Sort interface to PageRequest sping class.
I have no idea how I can achieve it. I try many way but none of these are even close to resolve this problem (for example some subquery with invoke any() on collection and use Qsort class). I affraid I made a mistake using Query DSL rather than Criteria Api. Can someone direct me? I will be very grateful.
Best Regards

Spring Data use parameters in select clause of #Query

Goal
I am trying to define a generic query that allows me to list the possible (distinct) values of a property, possibly nested, of an entity. The goal is to have a drop down selection for the end user to choose from when filtering down the list of entities.
Setup
#Entity
public class Customer {
#Id #GeneratedValue Long id;
#NotNull String name;
#Embedded #NotNull Address address;
...
}
#Embeddable
public class Address {
String country;
String city;
String postalCode;
String street;
String number;
...
}
public interface CustomRepository {
#Query("select distinct ?1 from #{#entityName}")
List<String> findAllValues(String value);
#Query("select distinct ?1.?2 from #{#entityName} where ?1 IS NOT NULL")
List<String> findAllSubValues(String path, String value);
}
public interface RepositoryCustomer extends
CrudRepository<Customer, Long>,
JpaSpecificationExecutor<Customer>,
CustomRepository {}
Usage
The query could then be used as follows to show a selection box for filtering down the customers list based on their address country:
public class SelectionComponent {
#Autowired RepositoryCustomer repo;
ComboBox<String> select = new ComboBox<String>();
#PostConstruct
void onPostConstruct() {
select.setItems(repo.findAllSubValues("address", "country"));
}
}
Problem
Compiling the above setup results in follow exception:
org.hibernate.QueryException: Parameters are only supported in SELECT clauses when used as part of a INSERT INTO DML statement
Question
It seems this is not supported. Any alternative suggestions?
To not leave this unanswered. My solution in general has been to avoid trying to express relational or complex queries when it comes to JPA and Spring data.
I have come to prefer creating specific (single purpose) database views for such needs and have a very simple query in my "business layer". In some sense, this creates duplication or denormalization in the database layer, but greatly reduces the complexity required from overlay frameworks such as JPA and Spring data.
In this particular case, I would have a customer country database view that I would map to a JPA entity.

Categories

Resources