Dynamic search by criteria - spring

I am using QueryDSL with Spring Data Jpa and i want execute some dynamic search.
I follow this Answer and it's okey with BooleanBuilder But in my case I have to make joins.
So how can i make it if i have 3 joins on player, player_team, team and i have optional parameters on the name of player and name of his team ?
________ ___________________ _______
| player | | player_team | | team |
|------ | |---------------- | |-------|
| id | | player_team_id (pk) | | id |
| name | | player_id (fk) | | name |
------ | team_id (fk) | -------
-----------
player.java
#Entity
#Table(...)
public class Player implements java.io.Serializable {
private Integer idPlayer ;
private String namePlayer;
private Set<PlayerTeam> player_teams = new HashSet<PlayerTeam>(0);
...
}
team.java
#Entity
#Table(...)
public class Team implements java.io.Serializable {
private Integer idTeam ;
private String nameTeam;
private Set<PlayerTeam> player_teams = new HashSet<PlayerTeam>(0);
...
}
player_team.java
#Entity
#Table(...)
public class PlayerTeam implements java.io.Serializable {
private Integer idPlayerTeam ;
private Team team;
private Player paleyr;
...
}
and for each domaine i have respository like that :
public interface PlayerRespository extends JpaRepository<Player, Integer>, QueryDslPredicateExecutor<Player> {
}

If you don't put extra properties into PlayerTeam it shouldn't be modeled as an entity. Concerning the conditions it would be
player.namePlayer.eq(...)
and
new JPASubQuery().from(playerTeam)
.where(playerTeam.player.eq(player), palyerTeam.team.name.eq(...))
.exists()

Have you tried using Specification? Spring's JPA Repositories has this method for finding results using Specifications:
List<T> findAll(Specification<T> spec);
There are different approaches for building a specification, my approach is tailored for accepting request from my REST service, so I essentially make a blank entity of a given type (Foo in this case) and set whatever search criteria was provided in the request (name for example), then I build predicates from each field (if name field is specified then add 'name equals "bob"' predicate).
Here is an example of a Specification builder:
import static org.springframework.data.jpa.domain.Specifications.where;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.jpa.domain.Specification;
import com.acme.model.security.Principal;
public class FooSpecification {
private final Foo criteria;
private String query;
public FooSpecification(String query, Foo criteria) {
this.query = query;
this.criteria = criteria;
}
public Specification<Foo> trainerEquals() {
if (criteria.getTrainer() == null) {
return null;
}
return new Specification<Foo>() {
#Override
public Predicate toPredicate(Root<Foo> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
return cb.equal(root.<Principal>get("trainer").<Long>get("id"), criteria.getTrainer().getId());
}
};
}
public <T> Specification<Foo> valueEquals(final T value, final String field) {
if (value == null) {
return null;
}
return new Specification<Foo>() {
#Override
public Predicate toPredicate(Root<Foo> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
return cb.equal(root.<T> get(field), value);
}
};
}
/**
* Convert input string to lower case, appends '%' around it and does SQL LIKE comparison with the field value, also lower cased.
* If value is null, no comparison is done. Example:
*
* value = "John";
* field = "firstName";
*
* resulting specification = "name like '%john%'"
*
* #param value string or null
* #param field field name
* #return SQL LIKE specification for the given value or null if value is null
*/
public Specification<Foo> stringLike(final String value, final String field) {
if (StringUtils.isBlank(value)) {
return null;
}
return new Specification<Foo>() {
#Override
public Predicate toPredicate(Root<Foo> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
return cb.like(cb.lower(root.<String> get(field)), getLikePattern(value));
}
};
}
private String getLikePattern(String searchTerm) {
return new StringBuilder("%")
.append(searchTerm.toLowerCase().replaceAll("\\*", "%"))
.append("%")
.toString();
}
public Specification<Foo> fullSearch() {
return where(trainerEquals())
.and(valueEquals(criteria.getName(), "name"))
.and(valueEquals(criteria.getInstructions(), "description"))
.and(valueEquals(criteria.isAwesome(), "isAwesome"))
.and(
where(
stringLike(query, "name"))
.or(stringLike(query, "instructions")
)
);
}
}

Related

Why might Room database IDs all show as 0 in a log/Toast message?

I'm trying to get the id of the last inserted object into a database using Room with Android. I can fetch the last object using an SQL query and can call other methods to get the various properties of that object which the user has set when saving the object. But getId() always returns 0. When I examine the table contents in Android Studio's app inspector, I can clearly see that Room is generating a unique primary key for each row, but I just can't get at it. Can anyone suggest what the problem might be?
Here's the Dao query:
#Query("SELECT * FROM gamebooks_table WHERE gamebookId=gamebookId ORDER BY gamebookId DESC LIMIT 1")
LiveData<Gamebook> getSingleGamebookByID();
And here's the annotated entity class:
#Entity(tableName = "gamebooks_table")
public class Gamebook {
#PrimaryKey(autoGenerate = true)
private long gamebookId;
private String gamebookName;
private String gamebookComment;
private String gamebookPublisher;
private float gamebookStarRating;
public Gamebook(String gamebookName, String gamebookComment, String gamebookPublisher, float gamebookStarRating) {
this.gamebookName = gamebookName;
this.gamebookComment = gamebookComment;
this.gamebookPublisher = gamebookPublisher;
this.gamebookStarRating = gamebookStarRating;
}
public long getGamebookId() {
return gamebookId;
}
public String getGamebookName() {
return gamebookName;
}
public String getGamebookComment() {
return gamebookComment;
}
public String getGamebookPublisher() {
return gamebookPublisher;
}
public float getGamebookStarRating(){
return gamebookStarRating;
}
public void setGamebookId(long gamebookId) {
this.gamebookId = gamebookId;
}
}
SOLVED
Finally sorted this by adding an Observer to my DAO method which returns a single gamebook. Within the Observer's onChanged() method, I can loop through all Gamebooks in the LiveData List (even though there's only one because I'm limiting it to one in the SQL query) and call getId() to get their respective IDs.
mainViewModel.getSingleGamebook().observe(this, new Observer<List<Gamebook>>() {
#Override
public void onChanged(List<Gamebook> gamebooks) {
int i=0;
for(Gamebook gamebook : gamebooks){
gamebookId= gamebook.getGamebookId();
Log.d(TAG, "Gamebook Name: "+gamebook.getGamebookName()+ " Database ID: " +gamebookId);
i++;
}
}
});
I believe that your issue is due to the only constructor being available not setting the id so the LiveData uses the default value of 0 for a long.
I'd suggest having a default constructor and thus all setters/getters and (optionally) using #Ignore annotation for one of the constructors..
without #Ignore you get warnings Gamebook.java:8: warning: There are multiple good constructors and Room will pick the no-arg constructor. You can use the #Ignore annotation to eliminate unwanted constructors. public class Gamebook {
e.g. :-
#Entity(tableName = "gamebooks_table")
public class Gamebook {
#PrimaryKey(autoGenerate = true)
private long gamebookId;
private String gamebookName;
private String gamebookComment;
private String gamebookPublisher;
private float gamebookStarRating;
public Gamebook(){} /*<<<<< ADDED */
#Ignore /*<<<<< ADDED - is not required - could be on the default constructor but not both*/
public Gamebook(String gamebookName, String gamebookComment, String gamebookPublisher, float gamebookStarRating) {
this.gamebookName = gamebookName;
this.gamebookComment = gamebookComment;
this.gamebookPublisher = gamebookPublisher;
this.gamebookStarRating = gamebookStarRating;
}
public long getGamebookId() {
return gamebookId;
}
public String getGamebookName() {
return gamebookName;
}
public String getGamebookComment() {
return gamebookComment;
}
public String getGamebookPublisher() {
return gamebookPublisher;
}
public float getGamebookStarRating(){
return gamebookStarRating;
}
public void setGamebookId(long gamebookId) {
this.gamebookId = gamebookId;
}
/* ADDED setters */
public void setGamebookName(String gamebookName) {
this.gamebookName = gamebookName;
}
public void setGamebookComment(String gamebookComment) {
this.gamebookComment = gamebookComment;
}
public void setGamebookPublisher(String gamebookPublisher) {
this.gamebookPublisher = gamebookPublisher;
}
public void setGamebookStarRating(float gamebookStarRating) {
this.gamebookStarRating = gamebookStarRating;
}
}
You also probably want to be able to pass the respective id to the getSingleGamebookByID, so you may wish to change this to:-
#Query("SELECT * FROM gamebooks_table WHERE gamebookId=:gamebookId /*<<<<< ADDED to use id passed */ ORDER BY gamebookId DESC LIMIT 1")
LiveData<Gamebook> getSingleGamebookByID(long gamebookId /*<<<<< ADDED to use id passed */);
you would probably want to remove the comments.
Note the LiveData aspect has not been tested and is conjecture.
Example
This example shows that room is fine with your original code but that the issues is on the LiveData/Viewmodel side :-
public class MainActivity extends AppCompatActivity {
TheDatabase db;
GamebookDao dao;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
/* Note The Database has .allowMainThreadQueries */
db = TheDatabase.getInstance(this);
dao = db.getGamebookDao();
long gb1id = dao.insert(new Gamebook("Gamebook1","blah","Gamebook1 Publisher", 10.1F));
long gb2id = dao.insert(new Gamebook("Gamebook2","blah","Gamebook2 Publisher", 6.1F));
long gb3id = dao.insert(new Gamebook("Gamebook3","blah","Gamebook3 Publisher", 10.1F));
logGameBook(dao.getSingleGamebookByID());
logGameBook(dao.getSingleGamebookByID());
logGameBook(dao.getSingleGamebookByID());
/* Alternative that allows the ID to be specified */
logGameBook(dao.getSingleGamebookByIDAlternative(gb1id));
logGameBook(dao.getSingleGamebookByIDAlternative(gb2id));
logGameBook(dao.getSingleGamebookByIDAlternative(gb3id));
}
void logGameBook(Gamebook gb) {
Log.d("GAMEBOOKINFO","Gamebook is " + gb.getGamebookName() + " id is " + gb.getGamebookId());
}
}
The above uses your original code, the TheDatabase is a basic #Database annotated class BUT with .allowMainThreadQueries so it is run on the main thread.
The log, after running, includes:-
2022-03-12 08:16:12.556 D/GAMEBOOKINFO: Gamebook is Gamebook3 id is 3
2022-03-12 08:16:12.558 I/chatty: uid=10132(a.a.so71429144javaroomidreturnedaszero) identical 1 line
2022-03-12 08:16:12.561 D/GAMEBOOKINFO: Gamebook is Gamebook3 id is 3
2022-03-12 08:16:12.568 D/GAMEBOOKINFO: Gamebook is Gamebook1 id is 1
2022-03-12 08:16:12.572 D/GAMEBOOKINFO: Gamebook is Gamebook2 id is 2
2022-03-12 08:16:12.574 D/GAMEBOOKINFO: Gamebook is Gamebook3 id is 3
Note how the first just returns the same object and thus id.

How can I update specific filed of my class by PUT method in SpringBoot rest api

I used SpringBoot, and in the PUT method I check if the score exists then I want to update the score and also update the history by adding the latest score to it.
The Score Class:
package thesisMongoProject;
import java.util.Date;
import java.util.List;
import javax.validation.constraints.NotBlank;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import com.fasterxml.jackson.annotation.JsonView;
#Document(collection = "score")
public class Score {
#Id
#NotBlank
#JsonView(Views.class)
private String score;
#NotBlank
#JsonView(Views.class)
private String player;
#NotBlank
#JsonView(Views.class)
private String code;
#JsonView(Views.class)
private Date date;
private List<History> history;
public String getScore() {
return score;
}
public void setScore(String score) {
this.score = score;
}
public String getPlayer() {
return player;
}
public void setPlayer(String player) {
this.player = player;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public List<History> getHistory() {
return history;
}
public void setHistory(List<History> history) {
this.history = history;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
#Override
public String toString() {
return "Score [score=" + score + ", player=" + player + ", code=" + code + ", history=" + history + ", date="
+ date + "]";
}
}
The ScoreRepository:
package thesisMongoProject.Repository;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
import thesisMongoProject.Score;
import thesisMongoProject.ScoreDto;
#Repository
public interface ScoreRepository extends MongoRepository<Score, String>{
public Score findByScore(String score);
public void save(ScoreDto scoredto, String score);
}
But the PUT method save a new instance into the MongoDB instead of updating the existing one
The PUT method:
//Update Score By ID
#PutMapping("/{score}")
public ResponseEntity<?> updatePlayerByID(
#PathVariable("score")String score,
#RequestBody #JsonView(Views.class) #Valid Score score1){
Score findscore = srepo.findByScore(score);
if(findscore == null)
return ResponseEntity.status(404).body("There is not Score!");
else {
history = new ArrayList<History>();
h = new History();
h.setScore(score1.getScore());
h.setDate(score1.getDate());
history.add(h);
score1.setHistory(history);
srepo.save(score1);
return ResponseEntity.ok(score1);
}
}
Also i tried to use ScoreDTO and #PatchMapping like this:
The ScoreDTo Class:
package thesisMongoProject;
import java.util.List;
public class ScoreDto {
private String score;
List<History> history;
public String getScore() {
return score;
}
public void setScore(String score) {
this.score = score;
}
public List<History> getHistory() {
return history;
}
public void setHistory(List<History> history) {
this.history = history;
}
}
And the PATCHMAPPING:
#PatchMapping("/{score}")
public ResponseEntity<?> updateByScore(
#PathVariable("score")String score,
#RequestBody ScoreDto score1){
Score findscore = srepo.findByScore(score);
if(findscore == null)
return ResponseEntity.status(404).body("There is not Score!");
else {
srepo.save(score1, score);
return ResponseEntity.ok(score1);
}
}
but in my console I have an error:
org.springframework.data.mapping.PropertyReferenceException: No property save found for type Score! Did you mean 'date'?
could you help me how can i update the existing field of score, please?!
The primary key of a database should not be mutable. If there are multiple players with the same score, the earlier players' data would be replaced.
Ideally, for updating an existing document where id and all its new fields are known, something like this can be done:
score1.setScore(score);
srepo.save(score1);
Assuming score is the id of the document that is to be updated and score1 contains all other fields correctly, this will replace the existing document with id score with the new one score1.
In the first code ( the PUT method ), score1 should have the same id as findscore, then it will update the existing document.
Score findscore = srepo.findByScore(score);
if(findscore == null)
return ResponseEntity.status(404).body("There is not Score!");
else {
history = new ArrayList<History>();
h = new History();
h.setScore(score1.getScore());
h.setDate(score1.getDate());
history.add(h);
Also, for the exception you are getting, this save method
public void save(ScoreDto scoredto, String score);
can't be handled by the spring data repository automatically, you will have to define its implementation. More on what kind of methods can be defined or not here. The Standard save method in the repository can be used to achieve the required.

Spring Specification Criteria Multiple Joins ? How?

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

Oracle char type issue in Hibernate HQL query

I have Oracle table, which contains char type columns. In my Entity class i mapped oracle char type to java string type.
Here is the code for my Entity class.
#Entity
#Table(name="ORG")
public class Organization {
private String serviceName;
private String orgAcct;
//Some other properties goes here...
#Column(name="ORG_ACCT", nullable=false, length=16)
public String getOrgAcct() {
return this.orgAcct;
}
public void setOrgAcct(String orgAcct) {
this.orgAcct = orgAcct;
}
#Column(name="SERVICE_NAME",nullable=true, length=16)
public String getServiceName() {
return this.serviceName;
}
public void setServiceName(String serviceName) {
this.serviceName = serviceName;
}
}
Here both serviceName and orgAcct are char type variables in Oracle
In my DAO class I wrote a HQL query to fetch Oranization entity object using serviceName and orgAcct properties.
#Repository
#Scope("singleton") //By default scope is singleton
public class OrganizationDAOImpl implementsOrganizationDAO {
public OrganizationDAOImpl(){
}
public Organization findOrganizationByOrgAcctAndServiceName(String orgAcct,String serviceName){
String hqlQuery = "SELECT org FROM Organization org WHERE org.serviceName = :serName AND org.orgAcct = :orgAct";
Query query = getCurrentSession().createQuery(hqlQuery)
.setString("serName", serviceName)
.setString("orgAct", orgAcct);
Organization org = findObject(query);
return org;
}
}
But when I call findOrganizationByOrgAcctAndServiceName() method , I am getting Organization object as null(i.e. HQL query is not retrieving Char type data ).
Please help me to fix this issue. Here I can't change Oracle type char to Varchar2. I need to work with oracle char type variables.
#EngineerDollery After going throw above post, I modified my Entity class with columnDefinition , #Column annotation attribute.
#Column(name="SERVICE_NAME",nullable=true,length=16,columnDefinition="CHAR")
public String getServiceName() {
return this.serviceName;
}
But still I am not able to retrieve the data for corresponding columns.
and I added column size as well in columnDefinition attribute.
#Column(name="SERVICE_NAME",nullable=true,length=16,columnDefinition="CHAR(16)
But still same issue I am facing.
Any thing Am I doing wrong. Please help me.
I resolved this problem using OraclePreparedStatement and Hibernate UserType interface.
Crated a new UserType class by extending org.hibernate.usertype.UserType interface and provided implementation for nullSafeSet(), nullSafeGet() methods .
nullSafeSet() method, we have first parameter as PreparedStatement, inside the method I casted PreparedStatement into OraclePreparedStatement object and pass String value using setFixedCHAR() method.
Here is the complete code of UserType impl class.
package nc3.jws.persistence.userType;
import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import org.apache.commons.lang.StringUtils;
import org.hibernate.type.StringType;
import org.hibernate.usertype.UserType;
/**
*
* based on www.hibernate.org/388.html
*/
public class OracleFixedLengthCharType implements UserType {
public OracleFixedLengthCharType() {
System.out.println("OracleFixedLengthCharType constructor");
}
public int[] sqlTypes() {
return new int[] { Types.CHAR };
}
public Class<String> returnedClass() {
return String.class;
}
public boolean equals(Object x, Object y) {
return (x == y) || (x != null && y != null && (x.equals(y)));
}
#SuppressWarnings("deprecation")
public Object nullSafeGet(ResultSet inResultSet, String[] names, Object o) throws SQLException {
//String val = (String) Hibernate.STRING.nullSafeGet(inResultSet, names[0]);
String val = StringType.INSTANCE.nullSafeGet(inResultSet, names[0]);
//System.out.println("From nullSafeGet method valu is "+val);
return val == null ? null : StringUtils.trim(val);
}
public void nullSafeSet(PreparedStatement inPreparedStatement, Object o,
int i)
throws SQLException {
String val = (String) o;
//Get the delegatingStmt object from DBCP connection pool PreparedStatement object.
org.apache.commons.dbcp.DelegatingStatement delgatingStmt = (org.apache.commons.dbcp.DelegatingStatement)inPreparedStatement;
//Get OraclePreparedStatement object using deletatingStatement object.
oracle.jdbc.driver.OraclePreparedStatement oraclePreparedStmpt = (oracle.jdbc.driver.OraclePreparedStatement)delgatingStmt.getInnermostDelegate();
//Call setFixedCHAR method, by passing string type value .
oraclePreparedStmpt.setFixedCHAR(i, val);
}
public Object deepCopy(Object o) {
if (o == null) {
return null;
}
return new String(((String) o));
}
public boolean isMutable() {
return false;
}
public Object assemble(Serializable cached, Object owner) {
return cached;
}
public Serializable disassemble(Object value) {
return (Serializable) value;
}
public Object replace(Object original, Object target, Object owner) {
return original;
}
public int hashCode(Object obj) {
return obj.hashCode();
}
}
Configured this class with #TypeDefs annotation in Entity class.
#TypeDefs({
#TypeDef(name = "fixedLengthChar", typeClass = nc3.jws.persistence.userType.OracleFixedLengthCharType.class)
})
Added this type to CHAR type columns
#Type(type="fixedLengthChar")
#Column(name="SERVICE_NAME",nullable=true,length=16)
public String getServiceName() {
return this.serviceName;
}
char types are padded with spaces in the table. This means that if you have
foo
in one of these columns, what you actually have is
foo<space><space><space>...
until the actual length of the string is 16.
Consequently, if you're looking for an organization having "foo" as its service name, you won't find any, because the actual value in the table if foo padded with 13 spaces.
You'll thus have to make sure all your query parameters are also padded with spaces.

Query from combined spring data specification has multiple joins on same table

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

Resources