I have a Spring-MVC online-store project where I use Spring Boot and Hibernate. I decided to use the Specification for filtering. Thus, I wrote a method for the specification using JOINS. Please, tell me how you can write the same method without JOIN.
TeaSpecification class:
public static Specification<Tea> getTeasByFilter(Long colorId, Long typeId, Long countryId) {
return (root, query, criteriaBuilder) -> {
Join<Object, Object> colorJoin = root.join(Tea_.TEA_COLOR);
Join<Object, Object> typeJoin = root.join(Tea_.TEA_TYPE);
Join<Object, Object> countryJoin = root.join(Tea_.COUNTRIES);
Predicate countryPredicate = criteriaBuilder.equal(countryJoin.get(Countries_.ID), countryId);
Predicate colorPredicate = criteriaBuilder.equal(colorJoin.get(TeaColor_.ID), colorId);
Predicate typePredicate = criteriaBuilder.equal(typeJoin.get(TeaColor_.ID), typeId);
return criteriaBuilder.and(colorPredicate, typePredicate, countryPredicate);
};
}
Drink class(Tea extends Drink):
#Inheritance(strategy = InheritanceType.JOINED)
public class Drink {
// Fields
//
private #Id
#GeneratedValue
Long id;
private String name;
private BigDecimal price;
private String about;
#Column(name = "is_deleted")
private boolean isDeleted;
// Relationships
//
#ManyToOne
#JoinColumn(name = "packaging_id")
private Packaging packaging;
#ManyToOne
#JoinColumn(name = "manufacturer_id")
private Manufacturer manufacturer;
#ManyToOne
#JoinColumn(name = "country_id")
private Countries countries;
}
public class Tea extends Drink {
// Relationships
//
#ManyToOne
#JoinColumn(name = "type_id")
private TeaType teaType;
#ManyToOne
#JoinColumn(name = "color_id")
private TeaColor teaColor;
}
public class TeaSpecification {
public static Specification<Tea> getTeasByFilter(Long colorId, Long typeId, Long countryId) {
return (root, query, criteriaBuilder) -> {
Predicate colorPredicate = criteriaBuilder
.equal(root.get(Tea_.teaColor).get(TeaColor_.id), colorId);
Predicate typePredicate = criteriaBuilder
.equal(root.get(Tea_.teaType).get(TeaType_.id), typeId);
Predicate countryPredicate = criteriaBuilder
.equal(root.get(Tea_.countries).get(Countries_.id), countryId);
return criteriaBuilder.and(colorPredicate, typePredicate, countryPredicate);
};
}
}
Related
The following example filters "Entity2" according to the given criteria. However, the List of the #OneToMany relationship remains unfiltered (all entities are included, regardless of whether they match the filter). How to expand the filter so that these are also filtered? In this example entity2.entities1 may only include an Entity1 if the "value" is greater or equals the given one.
(postgres) database structure:
table1
-------------------
id character
id_table2 character
value integer
table2
--------------
id character
name character
deletedat timestamp without time zone
entity model:
#Entity
#Table(name = "table1")
public class Entity1 {
#Id
private String id;
#Column(name = "value")
private Integer value;
// ...
}
#Entity
#Table(name = "table2")
#Where(clause = "deletedat is null")
public class Entity2 {
#Id
private String id;
#Column(name = "name")
private String name;
#OneToMany
#JoinColumn(name = "id_table1", updatable = false)
private List<Entity1> entities1;
// ...
}
call and specification:
public Page<Entity2> findByFilter(SomeFilter filter) {
Pageable pageable = //...
return entity2Repository.findAll(createSpecification(filter), pageable);
}
private Specification<Entity2> createSpecification(SomeFilter filter) {
return (root, query, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
predicates.add(criteriaBuilder.equal(root.get("name"), filter.getName()));
root.join("entities1").on(criteriaBuilder.greaterThanOrEqualTo(join.get("value"), filter.getMinValue()));
query.distinct(true);
return criteriaBuilder.and(predicates.stream().toArray(Predicate[]::new));
};
}
I already tired solutions from this questions but without success:
JPA Criteria Join OneToMany table where clause does not work
JPA Criteria Builder OneToMany Restrictions
https://stackoverflow.com/a/48441456/3615209
First of all, it's better to define the relationship in both classes.
Therefore, from Entity1 to Entity2 there is a #ManyToOne relationship and from Entity2 to Entity1 there is a #OneToMany relationship.
#Entity
#Table(name = "table1")
public class Entity1 {
#Id
private String id;
#Column(name = "value")
private Integer value;
#ManyToOne
#JoinColumn(name = "id_table2", referencedColumnName = "id", updatable = false)
private Entity2 entity2;
// ...
}
#Entity
#Table(name = "table2")
#Where(clause = "deletedat is null")
public class Entity2 {
#Id
private String id;
#Column(name = "name")
private String name;
#OneToMany(mappedBy = "entity2")
private List<Entity1> entities1;
// ...
}
In the end, you have to return a list of entity1, therefore you can reverse the selection and you can start from entity1. In this way, you can use fetch to avoid extra queries and you can navigate with get.
private Specification<Entity1> createSpecification(SomeFilter filter) {
return (root, query, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
root.fetch("entity2");
predicates.add(criteriaBuilder.equal(root.get("entity2").get("name"),
filter.getName()));
predicates.add(criteriaBuilder.greaterThanOrEqualTo(
root.get("value"),
filter.getMinValue()));
query.distinct(true);
return criteriaBuilder.and(predicates.stream().toArray(Predicate[]::new));
};
}
It is my first experience with JPA-Specification.
I tried to implement a sample project with same requirements of my real project.
Here Are my Entities: Movie and Actor
#Entity
#Table(name = "MOVIE")
public class Movie {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "ID")
private Long id;
#Column(name = "TITLE")
#Basic
private String title;
#Column(name = "GENRE")
#Basic
private String genre;
#Column(name = "RATING")
#Basic
private double rating;
#Column(name = "WATCH_TIME")
#Basic
private double watchTime;
#Column(name = "RELEASE_YEAR")
#Basic
private int releaseYear;
}
#Entity
#Table(name = "ACTOR")
public class Actor {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "ID")
private Long id;
#Column(name = "NAME")
#Basic
private String name;
#Column(name = "FAMILY")
#Basic
private String family;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name = "MOVIE_ID")
#Fetch(FetchMode.JOIN)
private Movie movie;
#Basic
#Column(name = "DATE_TIME")
private Timestamp dateTime;
}
Also I have their repositories which extends JpaSpecificationExecutor<>
And my ActorSpecification is as below:
public class ActorSpecification implements Specification<Actor> {
private List<SearchCriteria> list;
public ActorSpecification() {
this.list = new ArrayList<>();
}
public void add(SearchCriteria criteria) {
list.add(criteria);
}
#Override
public Predicate toPredicate(Root<Actor> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
//create a new predicate list
List<Predicate> predicates = new ArrayList<>();
//add criteria to predicates
for (SearchCriteria criteria : list) {
Join<Actor, Movie> join = root.join(Actor_.MOVIE);
query.orderBy(builder.desc(root.get(Actor_.DATE_TIME)));
if (criteria.getOperation().equals(SearchOperation.IN_MOVIE_ID)) {
predicates.add(join.get(Movie_.ID).in(Arrays.asList(72, 74, 76, 78)));
} else if (criteria.getOperation().equals(SearchOperation.IN_MOVIE_WATCHTIME)) {
predicates.add(join.get(Movie_.WATCH_TIME).in(Arrays.asList(135, 126)));
}
}
return builder.and(predicates.toArray(new Predicate[0]));
}
}
And this is the way I use to filter my data and fetch data:
ActorSpecification actorsInMovieIdList = new ActorSpecification();
actorsInMovieIdList.add(new SearchCriteria("MovieId", "72, 74, 76, 78", SearchOperation.IN_MOVIE_ID));
List<Actor> actorsMovieIdList = actorRepository.findAll(actorsInMovieIdList);
ActorSpecification actorsInMovieWatchTime = new ActorSpecification();
actorsInMovieWatchTime.add(new SearchCriteria("MovieWatchTime", "135 ,126", SearchOperation.IN_MOVIE_WATCHTIME));
List<Actor> actorsMoviesWatchTime = actorRepository.findAll(actorsInMovieIdList.and(actorsInMovieWatchTime));
AND NOW MY REQUIREMENT:
As we have many Actor in each Movie, so the join result will return list of Actors of each movie that matches our conditions for filtering movies.
Now I need to just return the Actor of that movie which has the greatest DATE_TIME
,is there any way for doing it with JpaSpecification or I need to implement a filter method myself.
If I want to tell you about my real project in order to make it more clear.
I have STOCK and DAILY_PRICE Tables and of course any Stock has many Daily_Price, So I just want to fetch the last Daily_price joining my Stock record.
Can anyone help me in this issue??
Any help will be appreciated!!
I can't find a clean and simple way to do pagination when using a many-to-many relationship with an extra column.
My model looks like that:
I have a user and a product model. Each user can consume n products. Each consumption will be stored in an extra table, because I want to store extra information such as date etc. I have implemented the model as follows and it works, but I want to get the consumptions of an user as a Pageable rather than retrieving the whole set. What would be the best way to implement that ?
#Entity
public class User {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#OneToMany(
mappedBy = "user",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<Consumption> consumptionList = new ArrayList<>(); // never set this attribute
public List<Consumption> getConsumptionList() {
return consumptionList;
}
public void addConsumption(Product product) {
Consumption consumption = new Consumption(this, product);
consumptionList.add(consumption);
product.getConsumptionList().add(consumption);
}
public void removeConsumption(Consumption consumption) {
consumption.getProduct().getConsumptionList().remove(consumption);
consumptionList.remove(consumption);
consumption.setUser(null);
consumption.setProduct(null);
}
}
--
#Entity
#NaturalIdCache
#org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Product {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#OneToMany(
mappedBy = "product",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<Consumption> consumptionList = new ArrayList<>();
public List<Consumption> getConsumptionList() {
return consumptionList;
}
}
This is my class to store consumptions.
#Entity
public class Consumption {
#EmbeddedId
private UserProductId id;
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("userId")
private User user;
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("productId")
private Product product;
public Consumption(User user, Product product) {
this.user = user;
this.product = product;
this.id = new UserProductId(user.getId(), product.getId());
}
}
And this is my composite Primary Key which.
#Embeddable
public class UserProductId implements Serializable {
#Column(name = "user_id")
private Long userId;
#Column(name = "product_id")
private Long productId;
private UserProductId() {
}
public UserProductId(Long userId, Long productId) {
this.userId = userId;
this.productId = productId;
}
}
I would like to be able to call a method such as "getConsumptionList(Page page)" which then returns a Pageable.
I hope you can help me!
Thank you in advance!
Well, if using Spring Boot you could use a Repository:
#Repository
public interface ConsumptionRepo extends JpaRepository<Consumption, Long>{
List<Consumption> findByUser(User user, Pageable pageable);
}
Then you can simply call it
ConsumptionRepo.findByUser(user, PageRequest.of(page, size);
I finally found a solution for my problem thanks to #mtshaikh idea:
Just implement a Paginationservice:
public Page<Consumption> getConsumptionListPaginated(Pageable pageable) {
int pageSize = pageable.getPageSize();
int currentPage = pageable.getPageNumber();
int startItem = currentPage * pageSize;
List<Consumption> list;
if (consumptionList.size() < startItem) {
list = Collections.emptyList();
} else {
int toIndex = Math.min(startItem + pageSize, consumptionList.size());
list = consumptionList.subList(startItem, toIndex);
}
return new PageImpl<>(list, PageRequest.of(currentPage, pageSize), consumptionList.size());
}
is there a chance to fetch join entity with using predicate?
#Entity
public class Student {
#Id
private int id;
private String hobby;
private String favouriteColor;
private String name;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.REFRESH)
#JoinColumn(name = "CLASS_ID", referencedColumnName = "ID")
private Classroom classroom;
}
#Entity
public class Classroom {
#Id
private int id;
private String classRoom;
private List<Student> students;
}
I have some predicate
public class StudentPredicate {
private StudentPredicate() {}
public static Predicate createPredicate(final Student filter) {
QStudent student = QStudent.student;
BooleanBuilder builder = new BooleanBuilder();
if (isNotBlank(filter.getHobby())) {
builder.and(student.hobby.eq(filter.getHobby());
}
if (isNotBlank(filter.getFavouriteColor())) {
builder.and(student.favouriteColor.eq(filter.getFavouriteColor()));
}
return builder.getValue();
}
}
and repository
#Repository
public interface StudentRepository extends CrudRepository<Student, Integer>, QueryDslPredicateExecutor<Student> {
}
and now how can I find all students with fetched classrooms?
studentRepository.findAll(predicate)
How to say to query dsl that these findAll should fetch classroom?
As there's FetchType.LAZY for classroom field in Student class, so here while you call getClassRoom() will actually fetch the related entity from db or either you can use FetchType.EAGER.
I'm facing a difficulty in developing a server in Spring (+ Hibernate + JPA) for a project.
The structure of the server (the part of interest in this case) is composed of catalogs composed of products that can have some related feedbacks.
Here I share the 3 entities:
Catalog.java
#Entity
#Data
#Table(name = "catalog")
public class Catalog {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#Column(nullable = false, unique = true)
private String name;
private String description;
#ManyToOne(cascade = CascadeType.MERGE)
#JoinColumn(name = "catalog_user_id", nullable = false)
private User user;
#ManyToMany(cascade = {CascadeType.DETACH, CascadeType.PERSIST, CascadeType.REFRESH})
#JoinTable(name = "catalog_product",
joinColumns = {#JoinColumn(name = "catalog_id")},
inverseJoinColumns = {#JoinColumn(name = "product_id")}
)
private List<Product> products;
public Catalog() {}
}
Product.java
#Entity
#Data
#Table(name = "product")
public class Product {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#Column(nullable = false, unique = true)
private String name;
private String description;
#Column(nullable = false, length = 1)
#MapKeyEnumerated(EnumType.ORDINAL)
private Category category;
#ManyToOne(cascade = CascadeType.MERGE)
#JoinColumn(name = "product_user_id", nullable = false)
private User user;
public Product() {}
}
Feedback.java
#Entity
#Data
#Table(name = "feedback")
public class Feedback {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#ManyToOne(cascade = CascadeType.MERGE)
#JoinColumn(name = "feedback_user_id", nullable = false)
private User user;
#Column(nullable = false, length = 1)
#MapKeyEnumerated(EnumType.ORDINAL)
private Rating rating;
private String text;
#ManyToOne(cascade = CascadeType.MERGE)
#JoinColumn(name = "product_id", nullable = false)
private Product product;
public Feedback() {}
}
The problem occurs when I try to delete some entities. What I want is:
when I delete a catalog also the catalog references in the "catalog_product" join table should be deleted (but the product linked with the catalog should not be deleted);
when I delete a product also the product references in the "catalog_product" join table and the feedbacks related to that product should be deleted;
when I delete a feedback nothing happens.
In the business layer I have this operations:
CatalogServiceImpl.java
#Service
public class CatalogServiceImpl implements CatalogService {
#Autowired
private CatalogDAO catalogDAO;
#Autowired
private ModelMapper mapper;
public CatalogDTO findById(Long id) {
Catalog catalog = catalogDAO.findById(id);
return mapper.map(catalog, CatalogDTO.class);
}
public CatalogDTO findByName(String name) {
Catalog catalog = catalogDAO.findByName(name);
return mapper.map(catalog, CatalogDTO.class);
}
public List<CatalogDTO> findByUserId(Long id) {
List<Catalog> catalogs = catalogDAO.findByUserId(id);
Type listCatalogsType = new TypeToken<List<CatalogDTO>>() {}.getType();
return mapper.map(catalogs, listCatalogsType);
}
public List<CatalogDTO> findAll() {
List<Catalog> catalogs = catalogDAO.findAll();
Type listCatalogsType = new TypeToken<List<CatalogDTO>>() {}.getType();
return mapper.map(catalogs, listCatalogsType);
}
public CatalogDTO createCatalog(CatalogDTO catalogDTO) {
Catalog catalog = mapper.map(catalogDTO, Catalog.class);
Catalog catalogFromDB = catalogDAO.save(catalog);
return mapper.map(catalogFromDB, CatalogDTO.class);
}
public CatalogDTO updateCatalog(CatalogDTO catalogDTO) {
Catalog catalog = mapper.map(catalogDTO, Catalog.class);
Catalog catalogFromDB;
if(catalogDAO.exists(catalog.getId())) {
catalogFromDB = catalogDAO.save(catalog);
} else {
catalogFromDB = null;
}
return mapper.map(catalogFromDB, CatalogDTO.class);
}
public void deleteCatalog(Long id) {
Catalog catalog = catalogDAO.findById(id);
if(catalog != null) {
catalogDAO.delete(catalog.getId());
}
}
}
ProductServiceImpl.java
#Service
public class ProductServiceImpl implements ProductService {
#Autowired
private ProductDAO productDAO;
#Autowired
private ModelMapper mapper;
public ProductDTO findById(Long id) {
Product product = productDAO.findById(id);
return mapper.map(product, ProductDTO.class);
}
public ProductDTO findByName(String name) {
Product product = productDAO.findByName(name);
return mapper.map(product, ProductDTO.class);
}
public ProductDTO findByCategory(Category category) {
Product product = productDAO.findByCategory(category);
return mapper.map(product, ProductDTO.class);
}
public List<ProductDTO> findByUserId(Long id) {
List<Product> products = productDAO.findByUserId(id);
Type listProductsType = new TypeToken<List<ProductDTO>>() {}.getType();
return mapper.map(products, listProductsType);
}
public List<ProductDTO> findAll() {
List<Product> products = productDAO.findAll();
Type listProductsType = new TypeToken<List<ProductDTO>>() {}.getType();
return mapper.map(products, listProductsType);
}
public ProductDTO createProduct(ProductDTO productDTO) {
Product product = mapper.map(productDTO, Product.class);
Product productFromDB = productDAO.save(product);
return mapper.map(productFromDB, ProductDTO.class);
}
public ProductDTO updateProduct(ProductDTO productDTO) {
Product product = mapper.map(productDTO, Product.class);
Product productFromDB;
if(productDAO.exists(product.getId())) {
System.out.println(product.toString());
productFromDB = productDAO.save(product);
} else {
productFromDB = null;
}
return mapper.map(productFromDB, ProductDTO.class);
}
public void deleteProduct(Long id) {
Product product = productDAO.findById(id);
if(product != null) {
productDAO.delete(product.getId());
}
}
}
Now, when I try performing the operations of deletion of catalog or product an error of constraint key fail is triggered. For example trying to delete a product which has a reference in the catalog_product join table:
com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Cannot delete or update a parent row: a foreign key constraint fails (`e01`.`catalog_product`, CONSTRAINT `FKdx5j7bcx77t7h0hjw6tvoxmp1` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`))
I don't understand if there's a way to set the relations between entities to make what I want in an automatic way with Spring, or if I have to remove records with reference manually before the deletion of the catalog/product.
Thanks a lot in advance to everyone!
Luca