I'm trying to see how I would create a JPA Critera query which allows for fully dynamic filtering with multiple levels.
For example
select *
from table
where (
(column1 = 'A'
and
(column2 = 'B' or column3 = 'C')
)
or
column3 = 'D'
You need to create a Specification as in the test below. This can be dynamic.
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(classes=HelloWorldConfig.class)
public class SpecificationTest {
#Autowired
private AccountRepository repository;
#Test
public void test1() {
final List<String> names = Arrays.asList(new String[]{"George","Max"});
Specification<Account> specification = new Specification<Account>() {
public Predicate toPredicate(Root<Account> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
List<Predicate> predicates = new ArrayList<Predicate>();
predicates.add(root.get("name").in(names).not());
return builder.and(predicates.toArray(new Predicate[predicates.size()]));
}
};
assertNotNull(repository);
repository.save(makeAccount("Greg", "123456787", "01-02-01"));
repository.save(makeAccount("George", "123456788", "01-02-02"));
repository.save(makeAccount("Max", "123456789", "01-02-03"));
List<Account> accounts = repository.findAll(specification);
assertEquals(1,accounts.size());
assertEquals("123456787",accounts.get(0).getAccountNumber());
}
private Account makeAccount(String name, String accountNumber, String sortCode) {
Account account = new Account();
account.setName(name);
account.setAccountNumber(accountNumber);
account.setSort(sortCode);
return account;
}
}
Where the repository looks like :
#Repository
public interface AccountRepository extends JpaRepository<Account, Long>, JpaSpecificationExecutor<Account> {
}
Related
I have 4 tables in DB, such as
Product => PK productid
Variant => PK variantid FK productid
Images => PK imageid FK variantid
Attribute => PK attributid FK variantid
So need to perform following action in Java Rest API
Pagination with sorting and filtering
One Product Object has different Variant with there images and attributes.
Below is the #Repository code for single table pagination i.e Product
#Repository
public class PaginProductCriteriaRepository {
private final EntityManager entityManager;
private final CriteriaBuilder criteriaBuilder;
public PaginProductCriteriaRepository(EntityManager entityManager) {
this.entityManager = entityManager;
this.criteriaBuilder = entityManager.getCriteriaBuilder();
}
public Page<Product> findAllWithFilter(PaginProductPage paginPage, PaginProductSearchCriteria paginSearchCriteria){
CriteriaQuery<Product> productCriteriaQuery = criteriaBuilder.createQuery(Product.class);
Root<Product> rootProduct = productCriteriaQuery.from(Product.class);
Predicate predicate = getPrediate(paginSearchCriteria,rootProduct);
productCriteriaQuery.where(predicate);
setOrder(paginPage,productCriteriaQuery,rootProduct);
TypedQuery<Product> typedQuery = entityManager.createQuery(productCriteriaQuery);
typedQuery.setFirstResult(paginPage.getPageNumber() * paginPage.getPageSize());
typedQuery.setMaxResults(paginPage.getPageSize());
Pageable pageable = getPageable(paginPage);
long paginCount = getPaginCountMethod(predicate);
return new PageImpl<>(typedQuery.getResultList(),pageable,paginCount);
}
private Predicate getPrediate(PaginProductSearchCriteria paginSearchCriteria, Root<Product> root) {
List<Predicate> predicates = new ArrayList<>();
if (Objects.nonNull(paginSearchCriteria.getpName())) {
predicates.add(criteriaBuilder.like(root.get("pName"),"%" + paginSearchCriteria.getpName() + "%"));
}
return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
}
private void setOrder(PaginProductPage paginPage, CriteriaQuery<Product> criteriaQuery, Root<Product> root) {
if (paginPage.getSortDirection().equals(Sort.Direction.ASC)) {
criteriaQuery.orderBy(criteriaBuilder.asc(root.get(paginPage.getSortBy())));
} else {
criteriaQuery.orderBy(criteriaBuilder.desc(root.get(paginPage.getSortBy())));
}
}
private Pageable getPageable(PaginProductPage paginPage) {
Sort sort = Sort.by(paginPage.getSortDirection(),paginPage.getSortBy());
return PageRequest.of(paginPage.getPageNumber(),paginPage.getPageSize(),sort);
}
private long getPaginCountMethod(Predicate predicate) {
CriteriaQuery<Long> countQuery = criteriaBuilder.createQuery(Long.class);
Root<Product> countRoot = countQuery.from(Product.class);
countQuery.select(criteriaBuilder.count(countRoot)).where(predicate);
return entityManager.createQuery(countQuery).getSingleResult();
}
}
Expectation:
Using above code pagination is working fine using single Table i.e Product BUT I need to join multiple tables with product and get full product object contain variants, images and attributes.
Note: Single Product has multiple variants, variant has muluple attribute and images
My system required to add filters,and I'm wonder if there any query that like this
SELECT *
FROM posts p
when byDate is not null then (where p.createAt BETWEEN :startDate AND :endDate)
when byType is not null then (where p.type = :type)
I knew that the query is not valid, but I want at one query to get the data wherever the request has (no filter or all filters or some of filters).
My goal is to create one query to achieve all cases.
It's usually not a good idea to write a big SQL query when you can tell in advance the actual query you want to run.
If you want to run a different query based on conditions you know before running the query, there are different approaches in JPA or Spring that you can use
Spring
You can define the different queries using Spring Data query methods?
public class PostRepository implements JpaRepository<Post, Long> {
List<Post> findByCreatedAtBetween(Date startDate, Date endDate);
List<Post> findByTypeIs(String type);
}
And then somewhere in the code, you can:
List<Post> results = null;
if (byDate != null) {
results = repository.findByCreatedAtBetween(startDate, endDate);
} else if (byType != null) {
results = repository.findByTypeIs(type);
} else {
results = repository.findAll();
}
Criteria
With criteria you can create a dynamic query at runtime and execute it:
public class PostRepository implements PostRepositoryCustom {
#PersistenceContext
private EntityManager em;
#Override
public List<Post> findPosts(Filter filter) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<User> query = cb.createQuery(Post.class);
Root<User> user = query.from(Post.class);
if ((filter.getByDate() != null)) {
// byDate is not null
ParameterExpression<Date> startDate = builder.parameter( Date.class );
ParameterExpression<Date> endDate = builder.parameter( Date.class );
query.where(builder.between( b.get( "createdAt" ), startDate, endDate));
return em.createQuery(query)
.setParameter(startDate, ...)
.setParameter(endDate, ...)
.getResultList();
}
if (filter.getByType() != null) {
ParameterExpression<Date> typeParam = builder.parameter( Date.class );
query.where(builder.and(root.get("type"), typeParam));
return em.createQuery(query)
.setParameter(typeParam, ...)
.getResultList();
}
return entityManager.createQuery(query)
.getResultList();
}
}
Assuming that your entity has the fields type and createdAt.
This approach works well if you don't know in advance what's your query looks like. For example, when you don't know how many conditions you will have to add to it.
But, if I know already which query I want to run, then I prefer to use HQL/JPQL.
HQL
If your queries don't change and you already know what they look like,
I find it easier to define them with HQL:
public class PostRepository implements PostRepositoryCustom {
#PersistenceContext
private EntityManager em;
#Override
public List<Post> findPosts(Filter filter) {
if (filter.getByDate() != null) {
return em.createQuery("from Post p where p.createdAt between :startDate and :endDate", Post.class)
.setParameter("startDate", ...)
.setParameter("endDate", ...)
.getResultList();
}
if (filter.getByType() != null) {
return em.createQuery("from Post p where p.type =:type", Post.class)
.setParameter("type", ...)
.getResultList();
}
return em.createQuery("from Post", Post.class)
.getResultList();
}
}
You can refactor the code to make it more elegant, but it should give you an idea. Note that if you need to reuse the same queries in different services, it might be helpful to define them using the annotation #NamedQuery.
Filters
In Hibernate (not JPA) you can also define filters. They are SQL filter conditions that one can apply at runtime:
#Entity
#FilterDef(name = Post.BY_DATE, defaultCondition = "createdAt between :startDate and :endDate", parameters = {#ParamDef(name = "startDate", type = "date"), #ParamDef(name = "startDate", type = "date") })
#FilterDef(name = Post.BY_TYPE, defaultCondition = "type = :type", parameters = #ParamDef(name = "startDate", type = "date"))
#Filter(name = Post.BY_DATE)
#Filter(name = Post.BY_TYPE)
class Post {
static final String BY_DATE = "Post.byDateFilter";
static final String BY_TYPE = "Post.byFilter"
private String type;
private Date createdAt;
...
}
Then:
public class PostRepository implements PostRepositoryCustom {
#PersistenceContext
private EntityManager em;
#Override
public List<Post> findPosts(Filter filter) {
enableFilters(em);
return em.createQuery("from Post", Post.class).getResultList();
}
private void enableFilters(Filter filter, EntityManager em) {
if (filter.getByDate() != null) {
em.unwrap(Session.class)
.enableFilter( Post.BY_DATE )
.setParameter("startDate", ...)
.setParameter("endDate", ...);
} else if (filter.getByType() != null) {
em.unwrap(Session.class)
.enableFilter( Post.BY_TYPE )
.setParameter("type", ...);
}
}
}
I am trying to do testing my save method in my service impl class. It has Page as return type. The test succeeds but I am writting something wrong because it succeeds for all the cases which normally shouldn't Please see my code below.
Service Class Implementation
#Service
#Transactional
public class CompanyServiceImpl implements CompanyService {
private final CompanyRepository companyRepository;
public CompanyServiceImpl(CompanyRepository companyRepository) {
this.companyRepository = companyRepository;
}
#Override
public Page<Company> findAll(Pageable pageable) {
Page<Company> result = companyRepository.findAll(pageable);
return result;
}
#Override
public Page<Company> searchCompany(String companyName, Long companyGroupId, Pageable pageable) {
Page<Company> result = companyRepository.findByParametersWeb(companyName,companyGroupId,pageable);
return result;
}
#Override
public Optional<Company> findById(Long id) {
Optional<Company> entity = companyRepository.findById(id);
return entity;
}
#Override
public Company save(Company company) {
Company entity = companyRepository.save(company);
return entity;
}
#Override
public void delete(Long id) {
companyRepository.deleteById(id);
}
}
Testing Service class
class CompanyServiceImplTest {
#Mock
private CompanyRepository companyRepository;
private CompanyService companyService;
private Company company;
#BeforeEach
void setUp() {
MockitoAnnotations.initMocks(this);
companyService = new CompanyServiceImpl(companyRepository);
company = new Company();
company.setName("company");
company.setCompanyGroupId(1L);
}
#Test
void searchCompany() {
List<Company> companies = new ArrayList<>();
Pageable pageable= PageRequest.of(0,5);
Page<Company> result = new PageImpl<>(companies,pageable,1);
when(companyRepository.findByParametersWeb(anyString(),anyLong(),any(Pageable.class))).thenReturn(result);
Page<Company> newResult = companyService.searchCompany("giorgos",1L,pageable);
assertEquals(newResult.getTotalElements(),result.getTotalElements());
}
}
Finally My Company Repository
#Repository
public interface CompanyRepository extends JpaRepository<Company, Long> {
#Query("SELECT a FROM Company a WHERE (:name is null or ((a.name LIKE :name AND LENGTH(:name) > 0) OR ( a.name = '%')))")
List<Company> findByCompanyName(#Param("name") String name);
#Query("SELECT a FROM Company a WHERE (:name is null or (LENGTH(:name) > 0 " +
" AND ((:option = 'yes' AND a.name = :name) or (:option = 'start' AND a.name LIKE CONCAT(:name,'%')) " +
" or (:option = 'end' AND a.name LIKE CONCAT('%',:name)) or (a.name LIKE CONCAT('%',:name,'%'))))) " +
" AND (:companyGroupId is null or a.companyGroupId = :companyGroupId) ORDER BY a.name")
Page<Company> findByParametersWeb(String name,Long companyGroupId, Pageable pageable);
List<Company> findAllByNameOrderByName();
}
So you want to differentiate unit tests from integration or component tests here.
Your test would qualify as a unit test, it solely tests the functionality of your service layer isolated from everything else.
That is also why you mock your repository layer, to be independent from a database.
Contrary to that, integration and component tests test your whole application stack.
For this, the spring boot environment must be running, so you have to annotate your testclass with #ExtendWith(SpringExtension.class).
For these kind of tests you need an active db, so commonly you use a h2 database for your tests, that is filled with data prior to your tests. Have a look at this and this.
In the test itself you either inject your service and test from there, or you call your endpoints using RestTemplate.
#ExtendWith(SpringExtension.class)
#SpringBootTest
#Sql("/schema.sql")
public class DocumentManagementBackendApplicationTest {
#Autowired
private final CompanyServiceImpl companyServiceImpl;
#Test
#Sql("/searchCompany.sql")
public void testSearchCompany() {
List<Company> companies = new ArrayList<>();
Pageable pageable= PageRequest.of(0, 5);
Page<Company> result = companyService.searchCompany("giorgos",1L,pageable);
// now here you know, based on what you insert into your db with your sql scripts,
// what you should expect and so you can test for it
(...)
}
}
I'm trying to build dynamic subquery with a JPA Specification. How can I add predicates to the subquery and build it?
for example, I'll have 2 tables:
User and Usercard:
#Entity
#Table(name = "users", schema = "someschema")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
private String surName;
private String email;
#OneToMany
private List<Usercard> usercardList;
//other methods...
}
and
#Entity
#Table(name = "usercard", schema = "someschema")
public class Usercard {
#Id
private Long id;
private String account;
private String value;
#ManyToOne
private User user;
//other methods...
}
I have my repo:
#Repository
public interface UserRepository extends JpaRepository<User, Long>,
JpaSpecificationExecutor<User> {}
And trying to build smth like:
public List<User> findByPredicate(String email) {
return userRepository.findAll((Specification<User>) (root,
criteriaQuery, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
if (email != null) {
predicates.add(criteriaBuilder.and(criteriaBuilder.equal(
root.get("email"), email)));
}
return criteriaBuilder.and(predicates.toArray(new
Predicate[predicates.size()]));
});
}
but for subquery with predicates.
I've tried methods like this:
public List<User> findByUsercardAccount(String email, String account) {
return userRepository.findAll(new Specification<User>() {
#Override
public Predicate toPredicate(Root<User> root, CriteriaQuery<?>
criteriaQuery, CriteriaBuilder criteriaBuilder) {
Subquery<Usercard> subquery =
criteriaQuery.subquery(Usercard.class);
Root<Usercard> subRoot = subquery.from(Usercard.class);
List<Predicate> predicates = new ArrayList<>();
//predicates for Users table
predicates.add(criteriaBuilder.and(criteriaBuilder.equal(
root.get("email"), email)));
//predicates for Usercard table
predicates.add(criteriaBuilder.equal(subRoot.get("account"),
account));
return criteriaBuilder.and(predicates.toArray(new
Predicate[predicates.size()]));
}
});
}
So, I need a method for dynamic search within a few tables where I can pass arguments for a dynamic query as well as a dynamic subquery. I would be grateful for any help.
Found this solution:
public List<User> findByUsercardAccount(String account, String email) {
return userRepository.findAll((Specification<User>) (root, criteriaQuery, criteriaBuilder) -> {
Subquery<User> subquery = criteriaQuery.subquery(User.class);
Root<Usercard> subRoot = subquery.from(Usercard.class);
List<Predicate> predicates = new ArrayList<>();
List<Predicate> subPredicates = new ArrayList<>();
if (account != null)
subPredicates.add(criteriaBuilder.equal(subRoot.get("account"), account));
if (email != null)
predicates.add(criteriaBuilder.and(criteriaBuilder.equal(root.get("email"), email)));
subquery.select(subRoot.get("id")).where(subPredicates.toArray(new Predicate[predicates.size()]));
predicates.add(criteriaBuilder.exists(subquery));
return criteriaBuilder.and(predicates.toArray(new Predicate[predicates.size()]));
});
}
}
So, I've created subroot and subquery, added some restrictions (subPredicates) and passed them to the main root as a predicate.
There are two entity
#Entity
public class Event{
...
#ManyToMany(fetch = FetchType.LAZY)
private Set<EventGroup> eventGroups;
}
#Entity
public class EventGroup {
...
#ManyToMany(fetch = FetchType.LAZY)
private Set<Event> events;
}
I need to get Events which has EventGroups with given ids.
Using spring data CrudRepository.
#Repository
public interface EventRepository extends CrudRepository<Event, Long>, JpaSpecificationExecutor {
}
Im calling
eventRepository.findAll(buildSpecification(filter);
This is how i build specification:
private Specification<Event> buildSpecification(final EventFilter filter) {
final Specification<Event> specification = new Specification<Event>() {
#Override
public Predicate toPredicate(Root<Event> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder cb) {
root = criteriaQuery.distinct(true).from(Event.class);
Predicate predicate = cb.conjunction();
if (filter.getEventGroupIds() != null) {
Join<Event, EventGroup> join = root.join(Event_.eventGroups);
predicate.getExpressions().add( join.get(EventGroup_.id).in(filter.getEventGroupIds()) );
}
return criteriaQuery.where(predicate).getRestriction();
}
};
return specification;
}
But result query is
SELECT DISTINCT
event0_.id AS id1_1_,
event0_.createdAt AS createdA2_1_,
event0_.date AS date3_1_,
event0_.image_id AS image_id6_1_,
event0_.moderated AS moderate4_1_,
event0_.name AS name5_1_,
event0_.owner_id AS owner_id7_1_
FROM Event event0_
CROSS JOIN Event event1_
INNER JOIN Event_EventGroup eventgroup2_ ON event1_.id = eventgroup2_.Event_id
INNER JOIN EventGroup eventgroup3_ ON eventgroup2_.eventGroups_id = eventgroup3_.id
WHERE eventgroup3_.id IN (15)
This cross join corrupt everything.
What should i do? May be there is another way to get it?
Solved
private Specification<Event> buildSpecification(final EventFilter filter) {
final Specification<Event> specification = new Specification<Event>() {
#Override
public Predicate toPredicate(Root<Event> root, CriteriaQuery<?> cq, CriteriaBuilder cb) {
cq.distinct(true);
Predicate predicate = cb.conjunction();
if (filter.getEventGroupIds() != null) {
Join<Event, EventGroup> join = root.join(Event_.eventGroups);
predicate.getExpressions().add(join.get(EventGroup_.id).in(filter.getEventGroupIds()) );
}
return predicate;
}
};
return specification;
}