Sorry if my terminology isn't correct.
We are using spring data, JpaRepositories and criteria queries as our method to query data from our database.
I have a problem that when I combine two specifications such as I do with hasTimeZone and hasCity in hasCityAndTimeZone in the code example below it does a join on the same table twice, so the query below will look something like
select * from Staff, Location, Location
Is there any way to have the two specifications use the same join instead of each defining their own join that is essentially the same?
Sorry the code probably isn't complete I was just trying to show a quick example.
class Staff {
private Integer id;
private Location location;
}
class Location {
private Integer id;
private Integer timeZone;
private Integer city;
}
class StaffSpecs {
public static Specification<Staff> hasTimeZone(Integer timeZone) {
return new Specification<Staff>() {
#Override
public Predicate toPredicate(Root<Staff> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
Path<Integer> timeZonePath = root.join(Staff_.location).get(Location_.timeZone);
return cb.equal(timeZonePath, timeZone);
}
}
}
public static Specification<Staff> hasCity(Integer city) {
return new Specification<Staff>() {
#Override
public Predicate toPredicate(Root<Staff> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
Path<Integer> cityPath = root.join(Staff_.location).get(Location_.city);
return cb.equal(cityPath, city);
}
}
}
public static Specification<Staff> hasCityAndTimeZone(Integer city, Integer timeZone) {
return where(hasCity(city)).and(hasTimeZone(timeZone));
}
}
There's no out of the box way unfortunately. Spring Data internally uses some reuse of joins within QueryUtils.getOrCreateJoin(…). You could find out about potentially already existing joins on the root and reuse them where appropriate:
private static Join<?, ?> getOrCreateJoin(From<?, ?> from, String attribute) {
for (Join<?, ?> join : from.getJoins()) {
boolean sameName = join.getAttribute().getName().equals(attribute);
if (sameName && join.getJoinType().equals(JoinType.LEFT)) {
return join;
}
}
return from.join(attribute, JoinType.LEFT);
}
Note, that this only works as we effectively know which joins we add ourselves. When using Specifications you should also do, but I just want to make sure nobody considers this a general solution for all cases.
Based on #Oliver answer I created an extension to Specification interface
JoinableSpecification.java
public interface JoinableSpecification<T> extends Specification<T>{
/**
* Allow reuse of join when possible
* #param <K>
* #param <Z>
* #param query
* #return
*/
#SuppressWarnings("unchecked")
public default <K, Z> ListJoin<K, Z> joinList(From<?, K> from, ListAttribute<K,Z> attribute,JoinType joinType) {
for (Join<K, ?> join : from.getJoins()) {
boolean sameName = join.getAttribute().getName().equals(attribute.getName());
if (sameName && join.getJoinType().equals(joinType)) {
return (ListJoin<K, Z>) join; //TODO verify Z type it should be of Z after all its ListAttribute<K,Z>
}
}
return from.join(attribute, joinType);
}
/**
* Allow reuse of join when possible
* #param <K>
* #param <Z>
* #param query
* #return
*/
#SuppressWarnings("unchecked")
public default <K, Z> SetJoin<K, Z> joinList(From<?, K> from, SetAttribute<K,Z> attribute,JoinType joinType) {
for (Join<K, ?> join : from.getJoins()) {
boolean sameName = join.getAttribute().getName().equals(attribute.getName());
if (sameName && join.getJoinType().equals(joinType)) {
return (SetJoin<K, Z>) join; //TODO verify Z type it should be of Z after all its ListAttribute<K,Z>
}
}
return from.join(attribute, joinType);
}
/**
* Allow reuse of join when possible
* #param <K>
* #param <Z>
* #param query
* #return
*/
#SuppressWarnings("unchecked")
public default <K, Z> Join<K, Z> joinList(From<?, K> from, SingularAttribute<K,Z> attribute,JoinType joinType) {
for (Join<K, ?> join : from.getJoins()) {
boolean sameName = join.getAttribute().getName().equals(attribute.getName());
if (sameName && join.getJoinType().equals(joinType)) {
return (Join<K, Z>) join; //TODO verify Z type it should be of Z after all its ListAttribute<K,Z>
}
}
return from.join(attribute, joinType);
}
}
How to use
class StaffSpecs {
public static Specification<Staff> hasTimeZone(Integer timeZone) {
return new JoinableSpecification<Staff>() {
#Override
public Predicate toPredicate(Root<Staff> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
Path<Integer> timeZonePath = this.joinList(root,Staff_.location,JoinType.INNER).get(Location_.timeZone);
return cb.equal(timeZonePath, timeZone);
}
}
}
public static Specification<Staff> hasCity(Integer city) {
return new JoinableSpecification<Staff>() {
#Override
public Predicate toPredicate(Root<Staff> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
Path<Integer> cityPath = this.joinList(root,Staff_.location,JoinType.INNER).get(Location_.city);
return cb.equal(cityPath, city);
}
}
}
private static Join<?, ?> getOrCreateJoin(From<?, ?> from, String attribute) {
for (Join<?, ?> join : from.getJoins()) {
boolean sameName = join.getAttribute().getName().equals(attribute);
if (sameName && join.getJoinType().equals(JoinType.LEFT)) {
return join;
}
}
return from.join(attribute, JoinType.LEFT);
}
And in CustomSpecification
#Override
public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
query.distinct(true);
String[] parts = criteria.getKey().split("\\.");
Path<?> path = root;
for (String part : parts) {
if(path.get(part).getJavaType() == Set.class){
path = getOrCreateJoin(root, part);
}else{
path = path.get(part);
}
}
}
....
if (path.getJavaType() == String.class) {
return builder.like(path.as(String.class), "%" + criteria.getValue().toString() + "%");
....
This is old question but i wrote this answer for people who having this problem.
I implemented a generic method to search a join alias if there is no join with this alias then it creates the join with given pathFunction.
As Oliver say you can use defined joins but if you have multiple joins for an entity you need to know alias of your defined join.
//Get or create join with name of alias
protected <F extends From<FF, FR>, FF, FR, J extends Join<JF, JR>, JF, JR>J getOrCreateCriteriaJoin(F from, String alias, BiFunction<F, CriteriaBuilder, J> pathFunction) {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
Set<Join<FR, ?>> joins = from.getJoins();
Optional<J> optionalJoin = findJoin((Set) joins, alias);
return optionalJoin.orElseGet(() -> {
J join = pathFunction.apply(from, criteriaBuilder);
join.alias(alias);
return join;
}
);
}
//Recursively searches for 'alias' named join
protected Optional<Join> findJoin(Set<Join> joins, String alias) {
List<Join> joinList = new ArrayList<>(joins);
for (Join j : joinList) {
if (j.getAlias() != null && j.getAlias().equals(alias)) {
return Optional.of(j);
}
}
// Breadth first search
for (Join j : joinList) {
Optional<Join> res = findJoin(j.getJoins(), alias);
if (res.isPresent()) {
return res;
}
}
return Optional.empty();
}
Example Usage;
private Join<E, ExampleEntity> getOrCreateExampleEntityJoin(Root<E> mainRoot, String alias) {
return getOrCreateCriteriaJoin(mainRoot, alias, (root, cb) -> root.join(ExampleEntity_.someFieldName));
}
specification = (root, query, criteriaBuilder) -> criteriaBuilder.equal(getOrCreateExampleEntityJoin(root, "exampleAlias").get(ExampleEntity_.someAnOtherField), "ExampleData");
I have slightly modified the implementation so that there is no need to copy-paste aliases and functions
abstract class ReusableJoinSpecification<T> implements Specification<T> {
protected <F extends From<FF, FR>, FF, FR, J extends Join<JF, JR>, JF, JR> J getOrCreateJoin(F from,
JoinData<F, J> joinData) {
Set<Join<FR, ?>> joins = from.getJoins();
//noinspection unchecked
Optional<J> optionalJoin = (Optional<J>) findJoin(joins, joinData.getAlias());
return optionalJoin.orElseGet(() -> {
J join = joinData.getCreationFunction().apply(from);
join.alias(joinData.getAlias());
return join;
}
);
}
private Optional<Join<?, ?>> findJoin(#NotNull Set<? extends Join<?, ?>> joins, #NotNull String alias) {
List<Join<?, ?>> joinList = new ArrayList<>(joins);
for (Join<?, ?> join : joinList) {
if (alias.equals(join.getAlias())) {
return Optional.of(join);
}
}
for (Join<?, ?> j : joinList) {
Optional<Join<?, ?>> res = findJoin(j.getJoins(), alias);
if (res.isPresent()) {
return res;
}
}
return Optional.empty();
}
JoinData:
#Data
class JoinData<F extends From<?, ?>, J extends Join<?, ?>> {
#NotNull
private final String alias;
#NotNull
private final Function<F, J> creationFunction;
}
Usage:
private final JoinData<Root<Project>, Join<Project, Contractor>> contractorJoinData =
new JoinData<>("contractor", root -> root.join(Project_.contractor));
private final JoinData<Join<Project, Contractor>, Join<Contractor, Address>> contractorLegalAddressJoinData =
new JoinData<>("contractorLegalAddress", root -> root.join(Contractor_.legalAddress));
public Specification<Project> contractorLegalAddressCityLike(String address) {
if (address == null)
return null;
return new ReusableJoinSpecification<>() {
#Override
public Predicate toPredicate(Root<Project> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
Join<Project, Contractor> contractorJoin = getOrCreateJoin(root, contractorJoinData);
Join<Contractor, Address> contractorAddressJoin = getOrCreateJoin(contractorJoin, contractorLegalAddressJoinData);
return criteriaBuilder.like(contractorAddressJoin.get(Address_.city), simpleLikePattern(address));
}
};
}
Update:
I made a small tool to make it easier to reuse joins, get rid of the "if" when checking filter-dto fields for null, and use a type checks:
https://github.com/koval666/springspecwrapper
Related
I am using spring boot 2.2.6
I am implementing an API that has the option of filtering data and it must be returned paginated
so I used specifications (JpaSepecificationExecutor) and the specification has a join with another table
and this join is mandatory as a customerId is always given and added to predicate list
the problem is that the paginated info returned is wrong mainly (totalElements) and (totalPages)
this is the specifications class
`
#Data
#AllArgsConstructor
#NoArgsConstructor
public class CustomerSalesOrderSpecification implements Specification<SalesOrder> {
#NonNull
private Integer customerId;
private Integer orderId;
#DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate purchasedDateFrom;
#DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate purchasedDateTo;
private String customerFirstname;
private BigDecimal baseGrandTotalFrom;
private BigDecimal baseGrandTotalTo;
private String storeName;
#Override
public Predicate toPredicate(#NonNull Root<SalesOrder> root, #NonNull CriteriaQuery<?> query,
#NonNull CriteriaBuilder builder) {
List<Predicate> predicateList = new ArrayList<>();
predicateList.add(customerId(customerId).toPredicate(root, query, builder));
if (Objects.nonNull(orderId)) {
predicateList.add(orderId(orderId).toPredicate(root, query, builder));
}
if (Objects.nonNull(purchasedDateFrom)) {
predicateList.add(purchaseDateFrom(purchasedDateFrom).toPredicate(root, query, builder));
}
if (Objects.nonNull(purchasedDateTo)) {
predicateList.add(purchaseDateTo(purchasedDateTo).toPredicate(root, query, builder));
}
if (Objects.nonNull(customerFirstname)) {
predicateList.add(customerFirstname(customerFirstname).toPredicate(root, query, builder));
}
if (Objects.nonNull(baseGrandTotalFrom)) {
predicateList.add(baseGrandTotalFrom(baseGrandTotalFrom).toPredicate(root, query, builder));
}
if (Objects.nonNull(baseGrandTotalTo)) {
predicateList.add(baseGrandTotalTo(baseGrandTotalTo).toPredicate(root, query, builder));
}
if (Objects.nonNull(storeName)) {
predicateList.add(storeName(storeName).toPredicate(root, query, builder));
}
return builder.and(predicateList.toArray(new Predicate[]{}));
}
public Specification<SalesOrder> customerId(Integer customerId) {
return (root, query, builder) -> {
Join<SalesOrder, CustomerEntity> salesOrderCustomer = root.join("customerEntity");
return builder.equal(salesOrderCustomer.get("entityId"), customerId);
};
}
public Specification<SalesOrder> orderId(Integer orderId) {
return ((root, query, builder) -> builder.equal(root.get("entityId"), orderId));
}
public Specification<SalesOrder> purchaseDateFrom(LocalDate purchasedDateFrom) {
return (root,query,builder) -> builder.or(
builder.greaterThan(root.get("createdAt"),Timestamp.valueOf(purchasedDateFrom.atStartOfDay())),
builder.between(root.get("createdAt"),Timestamp.valueOf(purchasedDateFrom.atStartOfDay()),
Timestamp.valueOf(purchasedDateFrom.atTime(23,59))));
}
public Specification<SalesOrder> purchaseDateTo(LocalDate purchasedDateTo) {
return (root,query,builder) -> builder.or(
builder.lessThan(root.get("createdAt"),Timestamp.valueOf(purchasedDateTo.atStartOfDay())),
builder.between(root.get("createdAt"),Timestamp.valueOf(purchasedDateTo.atStartOfDay()),
Timestamp.valueOf(purchasedDateTo.atTime(23,59))));
}
public Specification<SalesOrder> customerFirstname(String customerFirstname) {
return ((root, query, builder) -> builder.like(root.get("customerFirstname"), "%" + customerFirstname + "%"));
}
public Specification<SalesOrder> baseGrandTotalFrom(BigDecimal baseGrandTotalFrom) {
return ((root, query, builder) -> builder.greaterThanOrEqualTo(root.get("baseGrandTotal"), baseGrandTotalFrom));
}
public Specification<SalesOrder> baseGrandTotalTo(BigDecimal baseGrandTotalTo) {
return ((root, query, builder) -> builder.lessThanOrEqualTo(root.get("baseGrandTotal"), baseGrandTotalTo));
}
public Specification<SalesOrder> storeName(String storeName) {
return ((root, query, builder) -> builder.like(root.get("storeName"), "%" + storeName + "%"));
}
}
`
for example if I filter based on id and name I get totalElements 20 when the real number is 377
and totalPages is 1 which is wrong of course
I tried writing the whole query in JPQL and it worked correctly so the issue is only I use specifications
with pagination and a join
I checked similar questions, but they were related to an error when using join fetch, I am using a normal join and there is no error just wrong pagination info in response
what might be the reason ?
I have a case where I need to execute an insert statement via createNativeQuery. I have an entity list I'm looping through in order to set the properties accordingly from another bean class, and then persist that data to the oracle database.
The problem I am facing is persisting the data that is part of the embeddedId (item, loc, weekstart, type, forecastId, insertTS). I need to persist that data for the new records to be inserted into the database. When I try to set the values from the POJO bean to my set method for the properties of my entity bean, nothing happens. Below is my code for setting the values of the properties from the POJO bean to my entity bean, along with my persistence method and the insert query being executed:
Validation class where validation occurs beforehand (missing to get the point) that includes the setting of my entity properties from the POJO bean:
List <InsertPromoData> insertPromos = new ArrayList<InsertPromoData>();
promo.forEach(record -> {
if (record.getErrorList().size() == 0) {
rowsSuccessful++;
Util.writeSuccessToFile(templateCd, successFile, record, successFields);
try {
InsertPromoData insertData = new InsertPromoData();
insertData.getId().setItem(record.getItem());
insertData.getId().setLoc(record.getLoc());
insertData.getId().setWeekStart(record.getWeek_Start_Date());
insertData.setNumberOfWeeks(record.getNumber_Of_Weeks());
insertData.getId().setType(record.getType());
insertData.getId().setForecastId(record.getForecast_ID());
insertData.setQty(record.getUnits());
insertPromos.add(insertData);
}
catch (Exception e) {
logger.error("Error with setting insertPromolist from promo list values and the error is " + e.getMessage());
}
}
else {
if (rowsFailure == 0) {
Util.writeHeaderToFile(templateCd, errorFile);
}
rowsFailure++;
Util.writeErrorToFile(templateCd, errorFile, record, record.getErrorList());
}
});
errorFile.close();
successFile.close();
OracleImpl.insertPromoData(insertPromos);
POJO bean (promo is the variable representing this list of beans in validation class above):
public class PromoBean extends ErrorListBean
{
public String Item;
public String Loc;
public String Week_Start_Date;
public String Units;
public String Forecast_ID;
public String Type;
public String Number_Of_Weeks;
public String getItem() {
return Item;
}
public void setItem(String item) {
Item = item;
}
public String getLoc() {
return Loc;
}
public void setLoc(String loc) {
Loc = loc;
}
public String getWeek_Start_Date() {
return Week_Start_Date;
}
public void setWeek_Start_Date(String week_Start_Date) {
Week_Start_Date = week_Start_Date;
}
public String getNumber_Of_Weeks() {
return Number_Of_Weeks;
}
public void setNumber_Of_Weeks(String number_Of_Weeks) {
Number_Of_Weeks = number_Of_Weeks;
}
public String getType() {
return Type;
}
public void setType(String type) {
Type = type;
}
public String getForecast_ID() {
return Forecast_ID;
}
public void setForecast_ID(String forecast_ID) {
Forecast_ID = forecast_ID;
}
public String getUnits() {
return Units;
}
public void setUnits(String units) {
Units = units;
}
}
Embeddable class representing the composite primary key of the table:
#Embeddable
public class PromoID implements Serializable {
#Column(name = "ITEM")
private String item;
#Column(name = "LOC")
private String loc;
#Column(name = "WK_START")
private String weekStart;
#Column(name = "TYPE")
private String type;
#Column(name = "FCSTID")
private String forecastId;
#Column(name = "U_TIMESTAMP")
private String insertTS;
public PromoID() {
}
public PromoID (String item, String loc, String weekStart, String type, String forecastId, String insertTS) {
this.item = item;
this.loc = loc;
this.weekStart = weekStart;
this.type = type;
this.forecastId = forecastId;
this.insertTS = insertTS;
}
public String getItem() {
return item;
}
public void setItem(String item) {
this.item = item;
}
public String getLoc() {
return loc;
}
public void setLoc(String loc) {
this.loc = loc;
}
public String getWeekStart() {
return weekStart;
}
public void setWeekStart(String weekStart) {
this.weekStart = weekStart;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getForecastId() {
return forecastId;
}
public void setForecastId(String forecastId) {
this.forecastId = forecastId;
}
public String getInsertTS() {
return insertTS;
}
public void setInsertTS(String insertTS) {
this.insertTS = insertTS;
}
//hashcode and equals methods
Persistence Bean:
#Entity
#Table(name = "U_USER_PROMO")
public class InsertPromoData {
#EmbeddedId
private PromoID id;
#Column(name="NUMBER_OF_WEEKS")
String numberOfWeeks;
#Column(name="QTY")
String qty;
#Id
#AttributeOverrides(
{
#AttributeOverride(name = "item",column = #Column(name="ITEM")),
#AttributeOverride(name = "loc", column = #Column(name="LOC")),
#AttributeOverride(name = "weekStart", column = #Column(name="WK_START")),
#AttributeOverride(name = "type", column = #Column(name="TYPE")),
#AttributeOverride(name = "forecastId", column = #Column(name="FCSTID"))
}
)
public PromoID getId() {
return id;
}
public void setId(PromoID id) {
this.id = id;
}
public String getNumberOfWeeks() {
return numberOfWeeks;
}
public void setNumberOfWeeks(String numberOfWeeks) {
this.numberOfWeeks = numberOfWeeks;
}
public String getQty() {
return qty;
}
public void setQty(String qty) {
this.qty = qty;
}
}
DAO class method to execute the update (entitymanagerfactory emf already initialized):
public static void insertPromoData(List<InsertPromoData> insertData) {
logger.debug("Execution of method insertPromoData in Dao started");
System.out.println("Size of the insertData list is " + insertData.size());
EntityManager em = emf.createEntityManager();
try {
em.getTransaction().begin();
System.out.println("Beginning transaction for insertPromoData");
Query query = em.createNativeQuery(env.getProperty("insertPromoUploadData"));
for (InsertPromoData promoData : insertData) {
query.setParameter("item", promoData.getId().getItem());
query.setParameter("location", promoData.getId().getLoc());
query.setParameter("wkStart", promoData.getId().getWeekStart());
query.setParameter("numberOfWeeks", promoData.getNumberOfWeeks());
query.setParameter("type", promoData.getId().getType());
query.setParameter("fcstId", promoData.getId().getForecastId());
query.setParameter("quantity", promoData.getQty());
query.executeUpdate();
}
em.getTransaction().commit();
}
catch(Exception e) {
logger.error("Exception in beginning transaction");
e.printStackTrace();
}
finally {
em.clear();
em.close();
}
logger.debug("Execution of method insertPromoData in Dao ended");
}
Query in properties file:
insertPromoUploadData = INSERT INTO {h-schema}U_USER_PROMO (ITEM, LOC, WK_START, NUMBER_OF_WEEKS, TYPE, FCSTID, QTY, U_TIMESTAMP) VALUES (:item, :location, TO_DATE(:wkStart,'MM DD YYYY'), :numberOfWeeks, :type, :fcstId, :quantity, SYSDATE)
My list size from my DAO class is returning as 0 once I begin the transaction and not sure why it is empty. Is there a reason that it is empty? I'm trying to persist each of the fields to the database (including the composite key fields) via insert query. Any help appreciated.
After looking into this for hours, I finally came to the conclusion that the simplest way to executeUpdate() without running into issues due to my current #EmbeddedId/#Embeddable logic was to change it to use #IdClass for my composite PK class, and annotate the fields from the PK in my entity with #Id. This allowed my data to be persisted to the database. Another slight difference was adding the insertTS in my entity class and annotating with #Id and generating getters/setters. This was necessary for JPA to recognize all the properties being referenced that I am wanting to persist, though I am persisting insertTS using SYSDATE function from the oracle DB instead of utilizing the get/set methods and setting to the current time from the java side.
I am sure there is a way to use #EmbeddedId/#Embeddable logic and be able to persist the fields that are part of the EmbeddedId, however, this I found to be a more simplistic way of doing it without further complexity in the code.
I am passing following json from front end :
{names: 'ABC MKL-56-2,ABC MKL-56-3'};
In service layer,I am trying to run in query with the help of criteria builder as follows :
public List<APDetails> getWP(String names) {
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<APDetails> query = builder.createQuery(APDetails.class);
Root<APDetails> root = query.from(APDetails.class);
Predicate hasA = builder.in(root.get(APDetails_.names).in(Arrays.asList(names.split(","))));
query.where(builder.and(hasA));
List<APDetails> APs = em.createQuery(query.select(root)).getResultList();
return APs;
}
I am getting following error :
Error message: org.hibernate.hql.internal.ast.QuerySyntaxException:
unexpected token: in near line 1, column 163 [select generatedAlias0
from com.app.ow.APDetails as generatedAlias0 where generatedAlias0.names in (:param0, :param1) in ()]
First of all, if you're using springboot, I suggest you extend the JpaSpecificationExecutor class (check here, here, and here for more information) from your APDetailsRepository (I believe you're using them somewhere...):
public interface APDetailsRepository extends JpaRepository<APDetails, Long>, JpaSpecificationExecutor<APDetails> {
Then, try this:
#Autowired
public APDetailsRepository apDetailsRepository;
........
public List<APDetails> getWP(String names) {
List<String> namesAsList = Arrays.asList(names.split(","));
List<APDetails> listAPDetails = this.apDetailsRepository.findAll(createSpecification(namesAsList));
return listAPDetails;
}
public Specification<APDetails> createSpecification(List<String> names) {
return new Specification<APDetails>() {
private static final long serialVersionUID = 1L;
#Override
public Predicate toPredicate(Root<APDetails> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
List<Predicate> predicates = new ArrayList<Predicate>();
if (names!= null && !names.isEmpty()) {
List<Predicate> predicatesNames = new ArrayList<Predicate>();
for (String name : names) {
predicatesNames.add(builder.equal(root.<String>get("names"), name));
//I believe that the "APDetails_.names" attribute is a String...
}
predicates.add(builder.or(predicatesNames.toArray(new Predicate[] {})));
}
return builder.and(predicates.toArray(new Predicate[] {}));
}
};
}
I was working on java using jdo where I used to write query functions like below, which queries from an Entity based on what parameters are passed to the function.
Now Im moving to spring-boot, and want to know if I can achieve the same using spring-boot.Any help or suggestions would be heartfully appreciated.Thank you!!
public List<Result> getQueryResult(int filter1, String filter2,Float filter3,Long id){
Query query = new Query("select from Entity1");
String filter = "id == "+id;
if(filter1 != null){
filter = filter+" && filter1 == "+filter1+";
}
if(filter2 != null){
filter = filter+" && filter2 == '"+filter2+"'";
}
if(filter3 != null){
filter = filter+"filter3 == "+filter3;
}
query.setFIlter(filter);
List<Result> results = query.excute();
return results;
}
You have two options - you can use JPA Criteria Builder or JPA Specifications
class Person {
String firstName;
String lastName;
int age;
}
JPA Criteria Builder
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<Person> query = builder.createQuery(Person.class);
Root<Person> root = query.from(Person.class);
Predicate sellAlcohol = builder.ge(root.get(Person_.age), 21);
Predicate toMindy = builder.equal(root.get(Person_.firstName), "Mindy");
Usage
query.where(builder.and(sellAlcohol, toMindy));
em.createQuery(query.select(root)).getResultList();
Specificatons
public PersonSpecifications {
public static Specification<Person> sellAlcohol() {
return new Specification<Person> {
public Predicate toPredicate(Root<T> root, CriteriaQuery query, CriteriaBuilder cb) {
return cb.ge(root.get(Person_.age), 21);
}
};
}
public static Specification<Person> toMindy() {
return new Specification<Person> {
public Predicate toPredicate(Root<T> root, CriteriaQuery query, CriteriaBuilder cb) {
return cb.equal(root.get(Person_.firstName), "Mindy");
}
};
}
}
Usage
personRepository.findAll(where(sellAlcohol()).and(toMindy()));
I got stuck using a Spring Project with Spring Data + specification + criteria api.
I will try to simulate the situation with general entities we used write to get easy example.
The Entities:
Consider all attributes of the each entity is passed on the constructor showed below
Country(Long id, String name, String iso)
State(Long id, String name, String iso)
City(Long id, String name, String iso)
This is my repository:
public interface CityRepository extends PagingAndSortingRepository<City, Integer>, JpaSpecificationExecutor<City> {
}
As you can see, I don't need to implement anything on the repository
This is my service
#Service
#Transactional
public class CityService {
#Autowired
private CityRepository cityRepository;
#Transactional(readOnly = true)
public CityListVO findByNameLike(String name, PageRequest pageRequest) {
name = "%" + name + "%";
if (pageRequest == null) {
List<City> result = cityRepository.findAll(fillGridCriteria(name));
return new CityListVO(1, result.size(), result);
} else {
Page<City> result = cityRepository. findAll(fillGridCriteria(name), pageRequest);
return new CityListVO(result.getTotalPages(), result.getTotalElements(), result.getContent());
}
}
private static Specification<City> fillGridCriteria(String name) {
return new Specification<City>() {
#Override
public Predicate toPredicate(
Root<City> root,
CriteriaQuery<?> query,
CriteriaBuilder builder) {
/*
The current return I can do a like by name, and it works fine.
My problem is if for any reason I need to do multiple joins like the folow jpql:
select ci FROM City ci, State st, Country co where ci.st = st AND st.co = co AND co.name = 'Canada';
How to do this from here ? Inside this method.
How is gonna be the return for this method ?
*/
return builder.like(root.get("name"), name.trim());
}
};
}
}
Let's assume you want all the cities that their country's name like name and you have a relational Model in which :
Country(Long id, String name, String iso)
State(Long id,Long country, String name, String iso)
City(Long id, Long state, String name, String iso)
Predicate:
private static Specification<City> fillGridCriteria(String name) {
return new Specification<City>() {
#Override
public Predicate toPredicate(
Root<City> root,
CriteriaQuery<?> query,
CriteriaBuilder builder) {
return
builder.like(root.get("state").get("country").get("name"), name.trim());
}
};
}