How to filter entities dynamically on Spring boot where clauses will be defined by frontend - spring

I have an entity with the following properties
public class DistributorMasterData implements Serializable {
#Id
#Column(updatable = false, nullable = false)
private Long id;
private String distributorName;
private String distributorCode;
private String ownerName;
private String address;
}
I can write specification to filter data. But the filtering conditions are defined by me in the above way
#Override
public Predicate toPredicate(Root<DistributorMasterData> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
return codeSpec()
.and(idSpec())
.toPredicate(root, query, criteriaBuilder);
}
private Specification<DistributorMasterData> idSpec() {
return ((root, query, criteriaBuilder) -> Objects.isNull(criteria.getDistributorIds()) ?
null : root.get(DistributorMasterData_.ID).in(criteria.getDistributorIds())
);
}
private Specification<DistributorMasterData> codeSpec() {
return Objects.nonNull(criteria.getDistributorCode()) ? buildStringSpecification(criteria.getDistributorCode(), DistributorMasterData_.distributorCode) : null;
}
But what I want to achieve is total dynamic control where user will decide if the want to find data which are greater than the value the have selected or less than or equal. How can I achieve that?

Related

Limit search by id

I want to implement an endpoint which is used to search into table limited by class_id:
Table:
#Entity
#Table(name = "class_items")
public class ClassItems implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", unique = true, updatable = false, nullable = false)
private int id;
#Column(name = "class_id", length = 20)
private Integer classId;
#Column(name = "title", length = 75)
private String title;
}
#PostMapping("/{class_id}/find")
public Page<ClassCategoriesFullDTO> search(#PathVariable("class_id") Integer classId, #Valid ClassCategoriesSearchParams params, Pageable pageable) {
Page<ClassCategoriesFullDTO> list = classItemsRestService.findClassItemsByClassId(classId, params, pageable);
return list;
}
public Page<ClassCategoriesFullDTO> findClassItemsByClassId(Integer classId, ClassCategoriesSearchParams params, Pageable pageable) {
// Limit here queries by classId
Specification<Product> spec = (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
if (params.getTitle() != null) {
predicates.add(cb.equal(root.get("title"), params.getTitle()));
}
return cb.and(predicates.toArray(new Predicate[predicates.size()]));
};
return classItemsService.findAllByClassId(spec, pageable).map(classItemsMapper::toFullDTO);
}
#Service
#Transactional
public class ClassItemsServiceImpl implements ClassItemsService {
#PersistenceContext
private EntityManager entityManager;
private ClassItemsRepository dao;
#Autowired
public ClassItemsServiceImpl(ClassItemsRepository dao) {
this.dao = dao;
}
public Page<ClassItems> findAllByClassId(Specification spec, Pageable pageable) {
return this.dao.findAllByClassId(spec, pageable);
}
}
#Repository
public interface ClassItemsRepository extends JpaRepository<ClassItems, Long>, JpaSpecificationExecutor<ClassItems> {
Page<ClassItems> findAllByClassId(Specification spec, Pageable pageable);
}
I get error:
Parameter value [org.service.ClassItemsRestServiceImpl$$Lambda$1987/0x0000000801e21440#3c4de5a9] did not match expected type [java.lang.Integer (n/a)]; nested exception is java.lang.IllegalArgumentException: Parameter value [org.service.ClassItemsRestServiceImpl$$Lambda$1987/0x0000000801e21440#3c4de5a9] did not match expected type [java.lang.Integer (n/a)]",
Do you know how I can solve this issue?
You're getting this error because Spring Data is trying to generate a query based on the method name findAllByClassId, and so it expects an integer as the first parameter.
When working with specifications, you're supposed to use the methods provided by JpaSpecificationExecutor. Adding your own methods with specifications as parameters won't work. If you want to filter by classId, append the appropriate filter to the specification itself.
EDIT the solution is to add the extra condition when constructing the specification:
Specification<Product> spec = (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
if (params.getTitle() != null) {
predicates.add(cb.equal(root.get("title"), params.getTitle()));
}
predicates.add(cb.equal(root.get("classId"), classId));
return cb.and(predicates.toArray(new Predicate[predicates.size()]));
};
and then, in ClassItemsServiceImpl, call dao.findAll(spec)

Spring data jpa specification and pageable in #manytomany using join table repository

I have a use case to filter and paginate the record with #manytomany relation using a separate join table.
Below are the relation and entities
public class User {
private Long userId;
private String userName
#OneToMany(mappedBy = "user")
private List<UserRole> userRole;
}
public class Role {
private Long roleId;
private String roleName
#OneToMany(mappedBy = "role")
private List<UserRole> userRole;
}
public class UserRole{
private Long id;
private Integer active
#ManyToOne
#MapsId("userId")
private User user;
#ManyToOne
#MapsId("roleId")
private Role role;
}
#Repository
public interface UserRoleRepository extends
JpaRepository<UserRole, String>,
JpaSpecificationExecutor<UserRole> {
}
public class UserRoleSpecification implements Specification<UserRole>
{
private SearchCriteria criteria;
public RuleEntitySpecification(SearchCriteria criteria ) {
this.criteria = criteria;
}
#Override
public Predicate toPredicate(Root<UserRole> root,
CriteriaQuery<?> query,
CriteriaBuilder criteriaBuilder) {
if(criteria.getOperation().equalsIgnoreCase("eq")) {
if(root.get(criteria.getKey()).getJavaType() == String.class)
{
return criteriaBuilder.like(root.get(criteria.getKey()),
"%" + criteria.getValue() + "%");
} else {
return criteriaBuilder.equal(root.get(criteria.getKey()),
criteria.getValue());
}
}
return null;
}
}
public class SearchCriteria implements Serializable {
private String key;
private String operation;
private Object value;
}
UserRoleSpecificationBuilder specBuilder = new UserRoleSpecificationBuilder();
specBuilder.with("active", "eq" , 1); // giving us proper result
Specification<UserRole> spec = specBuilder.build();
Pageable paging = PageRequest.of(0, 5, Sort.by("user.userId"));
Page<UserRole> pagedResult = userRoleRepository.findAll(spec,paging);
But when we try to filter based on Rule/User table properties like userName/roleName specBuilder.with("user.userName", "eq" , "xyz");, I am getting following exception:
org.springframework.dao.InvalidDataAccessApiUsageException:
Unable to locate Attribute with the the given name
[user.userName] on this ManagedType
Kindly suggest if there is any way to achieve the filter using UserRole Join Table repository and specification
Pagination is also required hence using repository of Type UserRole JoinTable.
#Override
public Predicate toPredicate(Root<UserRole> root,
CriteriaQuery<?> query,
CriteriaBuilder criteriaBuilder) {
if (criteria.getOperation().equalsIgnoreCase("eq")) {
String key = criteria.getKey();
Path path;
if (key.contains(".")) {
String attributeName1 = key.split("\\.")[0];
String attributeName2 = key.split("\\.")[1];
path = root.get(attributeName1).get(attributeName2);
} else {
path = root.get(key);
}
if (path.getJavaType() == String.class) {
return criteriaBuilder.like(path, "%" + criteria.getValue() + "%");
} else {
return criteriaBuilder.equal(root.get(key), criteria.getValue());
}
}
return null;
}

combining #NamedQuery from JPA and #Filter from Hibernate

I have #NamedQuery. I want to add a lot of different filters to the query as per condition at runtime. There is another concept of #Filter in Hibernate. can this concept be a merge to have a combined result?
suppose I have
#NamedQuery(name="Users.someUsers",query="select u from Users where firstname='bob'")
suppose I want to filter the result according to some other parameter.
can I add #Filter that can do the trick?
supposed I want to an add age filer or a place filter over the existing Users.someUsers by enabling the corresponding filter on the underlying hibernate session?
I suppose you want to define named queries and filters at entity level and expect named queries to have filters which you defined.
I wrote a test for it:
#Entity
#Table(name = "DEPARTMENT")
#NamedQueries({#NamedQuery(name=DepartmentEntity.GET_DEPARTMENT_BY_ID, query=DepartmentEntity.GET_DEPARTMENT_BY_ID_QUERY),})
#FilterDef(name="deptFilter", parameters={#ParamDef( name="name", type="string")})
#Filters( {#Filter(name="deptFilter", condition=":name = name")})
public class DepartmentEntity implements Serializable {
static final String GET_DEPARTMENT_BY_ID_QUERY = "from DepartmentEntity d where d.id = :id";
public static final String GET_DEPARTMENT_BY_ID = "GET_DEPARTMENT_BY_ID";
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "ID", unique = true, nullable = false)
private Integer id;
#Column(name = "NAME", unique = true, nullable = false, length = 100)
private String name;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Now you can use both like this:
Session session = (Session) entityManager.getDelegate();
Filter filter = session.enableFilter("deptFilter");
filter.setParameter("name", name);
return (DepartmentEntity) session.getNamedQuery(DepartmentEntity.GET_DEPARTMENT_BY_ID)
.setParameter("id", id)
.uniqueResult();
Query generated by hibernate:
select department0_.ID as ID1_3_, department0_.NAME as NAME2_3_ from DEPARTMENT department0_ where ? = department0_.name and department0_.ID=?
You will need to add filters to session and then create named query. If this doesn't cover your use case then post example exactly what you want to acheive.
This is what CriteriaQuery is built for. It allows you to create queries at runtime.
Avoid using named queries for queries which you want to build at runtime based on user input.
Example
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<EntityType> criteria = builder.createQuery(EntityType.class);
Root<EntityType> root = criteria.from(EntityType.class);
criteria.where(
builder.equal(root.get("owner"), "something")
);
// Any conditions can be added to criteria here ar runtime based on user input
List<Topic> topics = entityManager
.createQuery(criteria)
.getResultList();
Named queries are precompiled at EntityManagerFactory startup, so they add performance benefits as long as queries are static, but for dynamic queries consider using CriteriaQueries

JPA Specification to filter entries based on a key in the jsonb map

I have an entity as follows:
#Entity
#Getter
#Setter
#NoArgsConstructor
#TypeDefs({
#TypeDef(name = "jsonb", typeClass = JsonbType.class)
})
public class Entity extends BasePersistableEntity<String> implements Serializable {
public static final String FIELD_NAME_SERVICES = "services";
public static final String FIELD_NAME_ORG_ID = "orgId";
#Column
public String orgId;
#Column
#Type(type = "jsonb")
public Map<String, String> services;
}
I want to write a query to fetch all the entities from a Postgres database that belong to an orgId and has a service in its services map.
For example something like,
select * from Entity where entity.ordId='abc' and 'xyz' in entity.services.keyset();
Currently, I have written the solution as follows:
#Override
public List<Entity> getEntitiesWithOrgIdAndService(String orgId, String service) {
return entityRepository.findEntityByOrgId(orgId).stream()
.filter(entity -> entity.services.containsKey(service)).collect(Collectors.toList());
}
Instead of this, I would like to filter it while getting it from the DB. How can I use JPA specification to do so?
Something like as follows:
#Override
public List<Proxy> getEntitiesWithOrgIdAndService(String orgId, String service) {
return proxyRepository.findAll(getSpecificationForEntity(orgId, service));
}
private Specification<Proxy> getSpecificationForProxyForEntity(String orgId, String service) {
return (Specification<Proxy>) (root, query, builder) -> {
List<Predicate> predicates = new ArrayList<>();
Predicate orgIdPredicate =
builder.equal(root.get(Entity.FIELD_NAME_ORG_ID), orgId);
// FIX NEEDED
/* Predicate servicePredicate =
builder.equal(builder.function("jsonb_extract_path_text",
String.class, root.<String>get(Entity.FIELD_NAME_SERVICES), service); */
Predicate orgIdServicePredicate = builder.and(orgIdPredicate, servicePredicate);
predicates.add(orgIdServicePredicate);
return builder.and(predicates.toArray(new Predicate[0]));
};
}
The below change solved the issue for me.
Predicate servicePredicate =
builder.equal(builder.function(Constants.FUNCTION_JSONB_EXTRACT_PATH_TEXT,
String.class, root.<String>get(Proxy.FIELD_NAME_SERVICES),
builder.literal(service)), String.valueOf(true));

how not to consider #NotBlank in some methods

I'm doing a restful app in Spring boot,jpa,mysql. I have annoted some of my model fields #NotBlank to print an error in the creation of an object if those fields are blank.
Now when i'm updating, I don't want to get that error if I don't set some fields in my json body.My goal is to update just the fields which are present.
So I want to know if there is a way not to consider an #NotBlank in my updating method.
This is the code source :
For the Entity
public class Note implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#NotBlank(name)
private String title;
#NotBlank
private String content;
//Getters and Setters
}
The controller
#RestController
#RequestMapping("/api")
public class NoteController {
#Autowired
NoteRepository noteRepository;
// Create a new Note
#PostMapping("/notes")
public Note createNote(#Valid #RequestBody Note note) {
return noteRepository.save(note);
}
// Update a Note
#PutMapping("/notes/{id}")
public Note partialUpdateNote(#PathVariable(value = "id") Long noteId,
#RequestBody Note noteDetails) {
Note note = noteRepository.findById(noteId)
.orElseThrow(() -> new ResourceNotFoundException("Note", "id", noteId));
//copyNonNullProperties(noteDetails, note);
if(note.getTitle()!= null) {
note.setTitle(noteDetails.getTitle());
}else {
note.setTitle(note.getTitle());
}
if(note.getContent()!= null) {
note.setContent(noteDetails.getContent());
}else {
note.setContent(note.getContent());
}
Note updatedNote = noteRepository.save(note);
return updatedNote;
}
// Delete a Note
#DeleteMapping("/notes/{id}")
public ResponseEntity<?> deleteNote(#PathVariable(value = "id") Long noteId) {
Note note = noteRepository.findById(noteId)
.orElseThrow(() -> new ResourceNotFoundException("Note", "id", noteId));
noteRepository.delete(note);
return ResponseEntity.ok().build();
}
}
ResourceNotFoundException is the class responsible to throws errors.
You can use groups for that.
Add two interfaces CreateGroup and UpdateGroup.
Use them by this way:
#NotBlank(groups = CreateGroup.class)
#Null(groups = UpdateGroup.class)
private String title;
In the create endpoint
#Valid #ConvertGroup(from = Default.class, to = CreateGroup.class) Note note
In the update endpoint
#Valid #ConvertGroup(from = Default.class, to = UpdateGroup.class) Note note
Probably you don't need UpdateGroup. It is just to show a common approach.
Also for the nested objects inside Note something like
#ConvertGroup(from = CreateGroup.class, to = UpdateGroup.class)
can be used.

Resources