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

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;
}

Related

Spring JPA hibernate how to persist children (remove, add, or update) from #OneToMany parent column?

I'm trying to solve this problem since a while and I haven't achieved a 100% solution.
First of all I have to describe my problem. I'm developping a restaurant application, and amoung the Entities, I have the Entity Ingredient and as you know Ingredient can consist of other Ingredient with a specific quantity. So I created an Entity SubIngredient with an Embedded Id.
And to persist subIngredients list I tried a combinations of Cascade and orphanRemoval, each combination worked for some operation but not for the others.
I started by using CascadeType.ALL and the new subIngredient persisted successfuly from the #OneToMany propertiy, But if I try to remove an subIngredient from the subIngredients list and save this error appear.
java.lang.StackOverflowError: null
at com.mysql.cj.NativeSession.execSQL(NativeSession.java:1109) ~[mysql-connector-java-8.0.23.jar:8.0.23]......
I loked in the net for a solution and I find the I have to use orphanremoval = true I tried it but it didn't work until I changed cascade from CascadeType.ALL to CascadeType.PERSIST. But this one make the persistance of new SubIngredient this error aprear
Caused by: javax.persistence.EntityNotFoundException: Unable to find com.example.Resto.domain.SubIngredient with id com.example.Resto.domain.SubIngredientKey#51b11186........
These are my Enities:
#Entity
public class Ingredient {
#Id
#GeneratedValue( strategy = GenerationType.IDENTITY)
#Column(name="ID")
private long id;
#NotNull
#Column(unique=true)
private String name;
private String photoContentType;
#Lob
private byte[] photo;
#JsonIgnoreProperties({"photoContentType","photo"})
#ManyToOne
private IngredientType ingredientType;
#OneToMany(mappedBy = "embId.ingredientId", fetch = FetchType.EAGER,
cascade = CascadeType.ALL /*or orphanRemoval = true, cascade = CascadeType.PERSIST*/ )
private Set<SubIngredient> subIngredients = new HashSet<SubIngredient>();
getters and setters.....
And
#Entity
#AssociationOverrides({
#AssociationOverride(name = "embId.ingredientId",
joinColumns = #JoinColumn(name = "ING_ID")),
#AssociationOverride(name = "embId.subIngredientId",
joinColumns = #JoinColumn(name = "SUB_ING_ID")) })
public class SubIngredient {
#EmbeddedId
private SubIngredientKey embId = new SubIngredientKey();
private double quantity;
getters and setters....
And
#Embeddable
public class SubIngredientKey implements Serializable{
#ManyToOne(cascade = CascadeType.ALL)
private Ingredient ingredientId;
#ManyToOne(cascade = CascadeType.ALL)
private Ingredient subIngredientId;
getters and setters...
The stackoverflow happen because you use a Set<> with Hibernate. When Hibernate retrieves the entities from your DB, it will fill up the Set<> with each entities. In order to that, hashode/equals will be used to determine wether or not the entitie is already present in the Set<>. By default, when you call the hashcode of Ingredient, this happen:
hashcode Ingredient -> hashcode SubIngredient -> hashcode Ingredient
which will result in an infinite call of hashcode method. That's why you have a stackoverflow error.
The same thing will happen with equals/toString.
So to avoid such an issue, it's best to override hashcode, equals and toString.
I have solved the problem by making some changes to may Entities and override equals/hashcode methods thanks Pilpo.
#Embeddable
public class SubIngredientKey implements Serializable{
private Long ingredientId;
private Long subIngredientId;
/**
* #return the ingredientId
*/
#Override
public int hashCode() {
return Objects.hash(ingredientId, subIngredientId);
}
#Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof SubIngredientKey)) {
return false;
}
SubIngredientKey other = (SubIngredientKey) obj;
return Objects.equals(ingredientId, other.ingredientId)
&& Objects.equals(subIngredientId, other.subIngredientId);
}
}
#Entity
public class SubIngredient {
#EmbeddedId
private SubIngredientKey embId = new SubIngredientKey();
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("ingredientId")
private Ingredient ingredient;
#ManyToOne(fetch = FetchType.LAZY)
#MapsId("subIngredientId")
private Ingredient subIngredient;
private double quantity;
#JsonIgnore
public SubIngredientKey getId() {
return embId;
}
public void setId(SubIngredientKey id) {
this.embId = id;
}
#JsonIgnoreProperties({"subIngredients","photo","photoContentType","ingredientType"})
public Ingredient getIngredient() {
return ingredient;
}
public void setIngredient(Ingredient ingredient) {
this.ingredient = ingredient;
}
#JsonIgnoreProperties({"subIngredients","photo","photoContentType","ingredientType"})
public Ingredient getSubIngredient() {
return subIngredient;
}
public void setSubIngredient(Ingredient subIngredient) {
this.subIngredient = subIngredient;
}
public double getQuantity() {
return quantity;
}
public void setQuantity(double quantity) {
this.quantity = quantity;
}
#Override
public String toString() {
return "subIngredient= " + getSubIngredient().getName() + " , quantity= " + getQuantity();
}
#Override
public int hashCode() {
return Objects.hash(ingredient,subIngredient);
}
#Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof SubIngredient)) {
return false;
}
SubIngredient other = (SubIngredient) obj;
return Objects.equals(ingredient, other.ingredient) && Objects.equals(subIngredient, other.subIngredient);
}
}
#Entity
public class Ingredient {
#Id
#GeneratedValue( strategy = GenerationType.IDENTITY)
#Column(name="ID")
private long id;
#NotNull
#Column(unique=true)
private String name;
private String photoContentType;
#Lob
private byte[] photo;
#JsonIgnoreProperties({"photoContentType","photo"})
#ManyToOne
private IngredientType ingredientType;
#OneToMany(mappedBy = "embId.ingredientId", fetch = FetchType.EAGER, cascade =
CascadeType.ALL, orphanRemoval = true)
private Set<SubIngredient> subIngredients = new HashSet<SubIngredient>();
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String getPhotoContentType() {
return photoContentType;
}
public void setPhotoContentType(String photoContentType) {
this.photoContentType = photoContentType;
}
public byte[] getPhoto() {
return photo;
}
public void setPhoto(byte[] photo) {
this.photo = photo;
}
public IngredientType getIngredientType() {
return this.ingredientType;
}
public void setIngredientType(IngredientType ingredientType) {
this.ingredientType = ingredientType;
}
public Set<SubIngredient> getSubIngredients() {
return subIngredients;
}
public void setSubIngredients(Set<SubIngredient> subIngredients) {
this.subIngredients = subIngredients;
}
public void addSubIngredient(SubIngredient subIngredient) {
this.subIngredients.add(subIngredient);
}
#Override
public String toString() {
String subIngsText = "";
for(var subIngredient:this.subIngredients) {
subIngsText = subIngsText + ", " + subIngredient.toString();
}
return "{id= "+id+",name=" + name +", ingredients="+subIngsText+"}";
}
#Override
public int hashCode() {
return Objects.hash(name);
}
#Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Ingredient)) {
return false;
}
Ingredient other = (Ingredient) obj;
return Objects.equals(name, other.name);
}
}

How to load a full graph of 2 entities that are in relationship #OneToMany each other with a Join Table

I'm using Spring Boot and Spring Data and I have a problem when trying to load entities using JPA and EntityGraph.
I have a Patient and Insurance entities. Each Patient can have many Insurances and each Insurance can be assigned to many patients. I decided to use a Join Table PatientInsurance because I need to store extra fields like 'active', and also the relation code (a Patient can be a Member, Spouse, or Child for that specific insurance).
Using Spring Data repositories I annotated the method to find a patient, with an EntityGraph, to have ready the list of PatientInsurances (and Insurances) for that patient in one query.
This is the code (I removed the non-necessary parts in the scope)
Patient class
#Entity
#Table(name = "patient")
public class Patient {
#NotNull
#NotEmpty
#Column(length = 60, nullable = false)
private String patientFirstName;
#NotNull
#NotEmpty
#Column(length = 60, nullable = false)
private String patientLastName;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "patient", cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH})
List<PatientInsurance> patientsInsurances = new ArrayList<>();
public void addPatientInsurance(PatientInsurance patientIns) {
if (!patientsInsurances.contains(patientIns)) {
patientsInsurances.add(patientIns);
}
}
//other properties...
Insurance class
#Entity
#Table(name = "insurance")
public class Insurance {
#Column(name = "policy_id", length = 20)
private String policyId;
#OneToMany(mappedBy = "insurance", fetch = FetchType.LAZY,cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH})
private List<PatientInsurance> patientsInsurances = new ArrayList<PatientInsurance>();
public void addPatientInsurance(PatientInsurance patientIns) {
if (!patientsInsurances.contains(patientIns)) {
patientsInsurances.add(patientIns);
}
}
//other properties
Entity for the join table between patient and insurance (needed a join table for extra field in this entity like active and relCode
#Entity
#IdClass(PatientInsurance.PatientInsurancePK.class)
#Table(name = "patient_insurance")
public class PatientInsurance implements Serializable {
#Id
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "patient_id")
private Patient patient;
#Id
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "insurance_id")
private Insurance insurance;
#Column(name = "active")
private boolean active;
#Column(length = 1)
private String relCode;
public PatientInsurance() {
insurance = new Insurance();
patient = new Patient();
}
public PatientInsurance(Patient p, Insurance i, boolean active, String relCode) {
this.patient = p;
this.insurance = i;
this.active = active;
this.relCode = relCode;
p.addPatientInsurance(this);
i.addPatientInsurance(this);
}
public Patient getPatient() {
return patient;
}
public Insurance getInsurance() {
return insurance;
}
public void setInsurance(Insurance insurance) {
this.insurance = insurance;
insurance.addPatientInsurance(this);
}
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
public void setPatient(Patient patient) {
this.patient = patient;
patient.addPatientInsurance(this);
}
public String getRelCode() {
return relCode;
}
public void setRelCode(String relCode) {
this.relCode = relCode;
}
static public class PatientInsurancePK implements Serializable {
protected Patient patient;
protected Insurance insurance;
public PatientInsurancePK() {
}
public PatientInsurancePK(Patient patient, Insurance insurance) {
this.patient = patient;
this.insurance = insurance;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof PatientInsurancePK)) return false;
PatientInsurancePK that = (PatientInsurancePK) o;
if (!patient.equals(that.patient)) return false;
return insurance.equals(that.insurance);
}
#Override
public int hashCode() {
int result = (patient != null) ? patient.hashCode() : 0;
result = 31 * result + ((insurance != null) ? insurance.hashCode() : 0);
return result;
}
}
}
Implementation of the PatientService
#Transactional
#Service("patientService")
public class PatientServiceImpl implements PatientService {
#Autowired
PatientRepository patientRepository;
#Override
public Optional<Patient> findByIdFull(Long id) {
Optional<Patient> patient = patientRepository.findById(id);
return patient;
}
//other methods...
Patient Repository
public interface PatientRepository extends JpaRepository<Patient, Long> {
#EntityGraph(
attributePaths = {
"patientsInsurances",
"patientsInsurances.patient",
"patientsInsurances.insurance"},
type = EntityGraph.EntityGraphType.LOAD)
Optional<Patient> findById(Long id);
A snippet of code that calls the method in PatientService
Optional<Patient> patientOptional = patientService.findByIdFull(p.getId());
if (patientOptional.isPresent()) {
Patient patient1 = patientOptional.get();
List<PatientInsurance> patientInsurances = patient1.getPatientInsurances();
PatientInsurances patientInsurance = patientInsurances.get(0);
Patient patient2 = patientInsurance.getPatient(); //and this is same istance of patient1, it's ok
Insurance insurance = patientInsurance.getInsurance();
//here is the problem!!!
insurance.getPatientInsurances();
//Unable to evaluate the expression Method threw 'org.hibernate.LazyInitializationException' exception.
So the problem seems that when I go inside the patient side, I can loop into his Insurances without problems, but when I try to do the same starting from the Insurance instance, I cannot loop into its patients cause they are lazily loaded.
So how to make jpa download the full graph in the correct way?

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)

Getting empty contents in the Postman API when filtering through Spring data Specification

I'm using the Spring Data JPA Specifications for Filtering data.
But When i'm hitting this URL http://localhost:9091/api/student/all?salary_like=1500
if i'm filtering through name also getting empty contents.
i'm getting the empty contents here.
But in eclipse console it's generating the correct query:
Hibernate: select student0_.id as id1_0_, student0_.address as address2_0_, student0_.age as age3_0_, student0_.name as name4_0_, student0_.salary as salary5_0_ from student_data_with_projection student0_ where (student0_.name like ?) and (student0_.age like ?) and (student0_.address like ?) and student0_.salary=1500.0 limit ?
And My code is:
POJO
#Data
#Component
#NoArgsConstructor
#AllArgsConstructor
#Entity
#Setter
#Getter
#Table(name = "student_data_with_projection")
public class Student {
#Id
#Column(name = "id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "name")
private String name;
#Column(name = "age")
private int age;
#Column(name = "salary")
private Float salary;
#Column(name = "address")
private String address;
}
Controller
#RestController
#RequestMapping(path = "api/student/") //This is a Base URL in Our Controller.
public class StudentController {
#Autowired
StudentRepository studentRepository;
#GetMapping(path = "all")
public #ResponseBody
Iterable<Student> getAllStudentWIthProjection(#RequestParam(required = false, defaultValue = "") String name_like,
#RequestParam(required = false, defaultValue = "") int age_like,
#RequestParam(required = false) Float salary_like,
#RequestParam(required = false, defaultValue = "") String address_like,
#RequestParam(required = false, defaultValue = "0") int pageNum,
#RequestParam(required = false, defaultValue = "20") int pageSize) {
StudentSpecification spec1 =
new StudentSpecification(new SearchCriteria("name", ":", name_like));
StudentSpecification spec2 =
new StudentSpecification(new SearchCriteria("age", ":", age_like));
StudentSpecification spec3 =
new StudentSpecification(new SearchCriteria("address", ":", address_like));
Specification<Student> specGroup = Specification.where(spec1).and(spec2).and(spec3);
if (salary_like != null) {
StudentSpecification spec4 =
new StudentSpecification(new SearchCriteria("salary", ":", salary_like));
specGroup = specGroup.and(spec4);
}
Page<Student> findAll = studentRepository.findAll(specGroup, PageRequest.of(pageNum, pageSize));
return findAll;
}
}
Repository
public interface StudentRepository extends PagingAndSortingRepository<Student, Long>, JpaSpecificationExecutor<Student> {
}
Specification
#AllArgsConstructor
public class StudentSpecification implements Specification<Student> {
private SearchCriteria criteria;
public StudentSpecification(SearchCriteria searchCriteria) {
super();
this.criteria=searchCriteria;
}
public SearchCriteria getCriteria() {
return criteria;
}
#Override
public Predicate toPredicate
(Root<Student> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
if (criteria.getOperation().equalsIgnoreCase(">")) {
if (root.get(criteria.getKey()).getJavaType() == Date.class) {
return builder.greaterThanOrEqualTo(root.<Date>get(criteria.getKey()), (Date)criteria.getValue());
} else {
return builder.greaterThanOrEqualTo(
root.<String> get(criteria.getKey()), criteria.getValue().toString());
}
}
else if (criteria.getOperation().equalsIgnoreCase("<")) {
if (root.get(criteria.getKey()).getJavaType() == Date.class) {
return builder.lessThanOrEqualTo(root.<Date>get(criteria.getKey()), (Date)criteria.getValue());
} else {
return builder.lessThanOrEqualTo(
root.<String> get(criteria.getKey()), criteria.getValue().toString());
}
}
else if (criteria.getOperation().equalsIgnoreCase(":")) {
if (root.get(criteria.getKey()).getJavaType() == String.class) {
return builder.like(
root.<String>get(criteria.getKey()), "%" + criteria.getValue() + "%");
} else {
return builder.equal(root.get(criteria.getKey()), criteria.getValue());
}
}
return null;
}
}
Criteria
#Data
#AllArgsConstructor
public class SearchCriteria {
private String key;
private String operation;
private Object value;
public SearchCriteria(String key, String operation, Object value) {
super();
this.key = key;
this.operation = operation;
this.value = value;
}
/* Getter and Setter */
}
I have uploaded the code in GitHub: https://github.com/avinashm294/Filters.git
How can i fix this to filter my data.
I had used the #Getter and #Setter annotation of lombok in the POJO which was not working. After Adding the getter and setter explicitly now it's working.
the like in the query work as =
you need to append % before and after the name
example
StudentSpecification spec1 =
new StudentSpecification(new SearchCriteria("name", ":", "%"+name_like+"%"));

Multi column search using Specifications Spring Data Jpa within associated entity?

I am taking this question Perform multi column search on Date, Integer and String Data type fields of Single Table? and This method must return a result of type Specification<Employee> in Java 8 further ahead.
Actually I wanted to search within association entity as well as a part of global search. Will that be possible using JPA 2 Specifications API ?
I've Employee and Department #OneToMany bi-directional relationship.
Employee.java
#Data
#Builder
#AllArgsConstructor
#NoArgsConstructor
#Entity
public class Employee implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "EMPLOYEE_ID")
private Long employeeId;
#Column(name = "FIRST_NAME")
private String firstName;
#Column(name = "LAST_NAME")
private String lastName;
#Column(name = "EMAIL_ID")
private String email;
#Column(name = "STATUS")
private String status;
#Column(name = "BIRTH_DATE")
private LocalDate birthDate;
#Column(name = "PROJECT_ASSOCIATION")
private Integer projectAssociation;
#Column(name = "GOAL_COUNT")
private Integer goalCnt;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "DEPT_ID", nullable = false)
#JsonIgnore
private Department department;
}
Department.java
#Data
#Builder
#AllArgsConstructor
#NoArgsConstructor
#Entity
public class Department implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "DEPT_ID")
private Long departmentId;
#Column(name = "DEPT_NAME")
private String departmentName;
#Column(name = "DEPT_CODE")
private String departmentCode;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "department")
#JsonIgnore
private Set<Employee> employees;
}
and I saved Data like below.
MyPaginationApplication.java
#SpringBootApplication
public class MyPaginationApplication implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(MyPaginationApplication.class, args);
}
#Autowired
private EmployeeRepository employeeRepository;
#Autowired
private DepartmentRepository departmentRepository;
#Override
public void run(String... args) throws Exception {
saveData();
}
private void saveData() {
Department department1 = Department.builder()
.departmentCode("AD")
.departmentName("Boot Depart")
.build();
departmentRepository.save(department1);
Employee employee = Employee.builder().firstName("John").lastName("Doe").email("john.doe#gmail.com")
.birthDate(LocalDate.now())
.goalCnt(1)
.projectAssociation(2)
.department(department1)
.build();
Employee employee2 = Employee.builder().firstName("Neha").lastName("Narkhede").email("neha.narkhede#gmail.com")
.birthDate(LocalDate.now())
.projectAssociation(4)
.department(department1)
.goalCnt(2)
.build();
Employee employee3 = Employee.builder().firstName("John").lastName("Kerr").email("john.kerr#gmail.com")
.birthDate(LocalDate.now())
.projectAssociation(5)
.department(department1)
.goalCnt(4)
.build();
employeeRepository.saveAll(Arrays.asList(employee, employee2, employee3));
}
}
EmployeeController.java
#GetMapping("/employees/{searchValue}")
public ResponseEntity<List<Employee>> findEmployees(#PathVariable("searchValue") String searchValue) {
List<Employee> employees = employeeService.searchGlobally(searchValue);
return new ResponseEntity<>(employees, HttpStatus.OK);
}
EmployeeSpecification.java
public class EmployeeSpecification {
public static Specification<Employee> textInAllColumns(Object value) {
return (root, query, builder) -> builder.or(root.getModel().getDeclaredSingularAttributes().stream()
.filter(attr -> attr.getJavaType().equals(value.getClass()))
.map(attr -> map(value, root, builder, attr))
.toArray(Predicate[]::new));
}
private static Object map(Object value, Root<?> root, CriteriaBuilder builder, SingularAttribute<?, ?> a) {
switch (value.getClass().getSimpleName()) {
case "String":
return builder.like(root.get(a.getName()), getString((String) value));
case "Integer":
return builder.equal(root.get(a.getName()), value);
case "LocalDate":
return builder.equal(root.get(a.getName()), value);//date mapping
default:
return null;
}
}
private static String getString(String text) {
if (!text.contains("%")) {
text = "%" + text + "%";
}
return text;
}
}
When I hit the /employees/{searchValue}, I want searching to be happened in Department Table along with Employee table (may be using Joins something like that). Is that possible ? If yes, how can we do that ?
Or:
Will this be good approach to put like here? Got reference from Using #Query
#Query("SELECT t FROM Todo t WHERE " +
"LOWER(t.title) LIKE LOWER(CONCAT('%',:searchTerm, '%')) OR " +
"LOWER(t.description) LIKE LOWER(CONCAT('%',:searchTerm, '%'))")
List<Todo> findBySearchTerm(#Param("searchTerm") String searchTerm);
Any pointers?
If you take a look at my post actually I have a solution for join
#Override
public Specification<User> getFilter(UserListRequest request) {
return (root, query, cb) -> {
query.distinct(true); //Important because of the join in the addressAttribute specifications
return where(
where(firstNameContains(request.search))
.or(lastNameContains(request.search))
.or(emailContains(request.search))
)
.and(streetContains(request.street))
.and(cityContains(request.city))
.toPredicate(root, query, cb);
};
}
private Specification<User> firstNameContains(String firstName) {
return userAttributeContains("firstName", firstName);
}
private Specification<User> lastNameContains(String lastName) {
return userAttributeContains("lastName", lastName);
}
private Specification<User> emailContains(String email) {
return userAttributeContains("email", email);
}
private Specification<User> userAttributeContains(String attribute, String value) {
return (root, query, cb) -> {
if(value == null) {
return null;
}
return cb.like(
cb.lower(root.get(attribute)),
containsLowerCase(value)
);
};
}
private Specification<User> cityContains(String city) {
return addressAttributeContains("city", city);
}
private Specification<User> streetContains(String street) {
return addressAttributeContains("street", street);
}
private Specification<User> addressAttributeContains(String attribute, String value) {
return (root, query, cb) -> {
if(value == null) {
return null;
}
ListJoin<User, Address> addresses = root.joinList("addresses", JoinType.INNER);
return cb.like(
cb.lower(addresses.get(attribute)),
containsLowerCase(value)
);
};
}
private String containsLowerCase(String searchField) {
return "%" + searchField.toLowerCase() + "%";
}
Here you can see how I search the users by their address columns (city and street).
EDIT: Also you cannot use the #Query annotation that much dinamically (you van insert parameter values dinamically, but not parameters. That's where Specificaion is handy)
EDIT2: I know this is not the 2.x.x Spring version, but 1.5.x, but the idea is the same for joins.

Resources