jpa merge Predicate array + search for foreign key value - jpa

I would like to have a search like this:
Select * From Users where ( ( username Like %criteria% OR firstName Like %criteria% ...) AND (CarId = carId) )
I did with 2 Predicate combined into an array, but need to do it with 1 Predicate only
#Entity
#Table(name = "users", uniqueConstraints = { #UniqueConstraint(columnNames = { "username" }) })
public class User{
#Id
#GeneratedValue(generator = "seq_id_user", strategy = GenerationType.SEQUENCE)
#SequenceGenerator(name = "seq_id_user", sequenceName = "seq_id_user")
private Long id;
#Column(unique = true)
#NotNull
private String username;
#Column(name = "first_name")
private String firstName;
....
#ManyToOne
#JoinColumn(name = "car_id", nullable = false, foreignKey = #ForeignKey(name = "CAR_ID_FK"))
private Car car;
...
The Car is a similar class, the User has 1 foreign key.
#Override
public List<User> searchUsers(String criteria, Car car) {
//TODO: check params and return values based on test cases
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<User> query = builder.createQuery(User.class);
Root<User> r = query.from(User.class);
Predicate predicate = builder.conjunction();
predicate = builder.or(predicate, builder.like(r.get("username"), "%" + criteria + "%"));
predicate = builder.or(predicate, builder.like(r.get("firstName"), "%" + criteria + "%"));
predicate = builder.or(predicate, builder.like(r.get("lastName"), "%" + criteria + "%"));
predicate = builder.or(predicate, builder.like(r.get("nickname"), "%" + criteria + "%"));
// how to implement the filter by foreign key value with AND? - can be a sub query to, which will be executed first time
// I need to use 1 Predicate, not a Predicate Array!
query.select(r).where(predicate);
List<User> result = entityManager.createQuery(query).getResultList();

Related

How to query a many to many entity list via Specification API in Spring Data JPA

I have 2 entities with Many-To-Many relationships
public class Enterprise{
#Id
#Column(name = "id", nullable = false, length = 50)
#GeneratedValue(generator = "jpa-uuid")
private String id;
fields...
#ManyToMany
#JoinTable(name = "enterprise_to_tag",
joinColumns = #JoinColumn(name = "enterprise"),
inverseJoinColumns = #JoinColumn(name = "tag"))
private Set<EnterpriseTag> tags;
}
and
public class EnterpriseTag{
#Id
#Column(name = "id", nullable = false, length = 50)
#GeneratedValue(generator = "jpa-uuid")
private String id;
fields...
#ManyToMany(mappedBy = "tags")
private Set<Enterprise> enterprises;
}
I want to query enterprise list by some tags' ID then pack them to Page
private Page<Enterprise> searchEnterprise(int number, int size, String keyword, String tags, String county)
throws BusinessException {
validPageNumberAndPageSize(number, size);
Pageable pageable = PageRequest.of(number, size);
Specification<Enterprise> specification = (Specification<Enterprise>) (root, criteriaQuery, criteriaBuilder) -> {
criteriaQuery.distinct(true);
List<Predicate> predicates = new ArrayList<>();
if (StringUtils.isNoneBlank(keyword)) {
Predicate predicateName = criteriaBuilder.like(root.get("name"), "%" + keyword + "%");
Predicate predicateSerialNumber = criteriaBuilder.like(root.get("serialNumber"), "%" + keyword + "%");
predicates.add(criteriaBuilder.and(criteriaBuilder.or(predicateName, predicateSerialNumber, predicateOrganizationCode)));
}
return criteriaQuery.where(predicates.toArray(new Predicate[0])).getRestriction();
};
//filter by tags here
if (StringUtils.isNoneBlank(tags)) {
List<String> tagIds = Arrays.asList(StringUtils.split(tags, ','));
List<Enterprise> enterprises = enterpriseRepository.findAll(specification).stream().filter(enterprise ->
enterprise.getTags().stream().map(EnterpriseTag::getId).collect(Collectors.toList()).containsAll(tagIds))
.collect(Collectors.toList());
return new PageImpl<>(enterprises, pageable, enterprises.size());
} else {
return enterpriseRepository.findAll(specification, pageable);
}
}
I don't know how to write this query. I have to handle it base on a database query result. But it's risky. If too much data is queried from the database, it will take up a lot of memory. Please help me to write this query by Specification API. Thanks.

How can I fetch multiple lazy loaded OneToMany lists?

I have an Entity with multiple unidirectional OneToMany relationships like following.
How can I fetch all this fields in one query?
What would be the best way if I have up to 10 Arraylists with a OneToMany relationship?
#Data
#EqualsAndHashCode(callSuper = true)
#Entity
#Table(name = "members")
public class Member extends Auditable<String> {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
#Setter(AccessLevel.NONE)
private Long id;
#OneToOne
private Gender gender;
private String lastName;
private String firstName;
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
#JoinColumn(name = "person_id")
private List<Phone> phoneList = new ArrayList<>();
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
#JoinColumn(name = "person_id")
private List<EMail> eMailList = new ArrayList<>();
// more Lists with OneToMany relationship
}
#Data
#EqualsAndHashCode(callSuper = true)
#Entity
#Table(name = "emails")
public class EMail extends Auditable<String> {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
#Setter(AccessLevel.NONE)
private Long id;
private Type type;
private String value;
}
I tried following step in my MemberRepository class which is ending in a MultipleBagFetchException:
#Query("SELECT m " +
"FROM Member m " +
"LEFT JOIN FETCH m.eMailList " +
"LEFT JOIN FETCH m.phoneList " +
"WHERE m.memberId = ?1")
Optional<Member> findByMemberIdWithAllInfoQuery(Long id); // MultipleBagFetchException
Then I tried following step with this information https://vladmihalcea.com/hibernate-multiplebagfetchexception/ which also doesn't work properly:
public Optional<Member> findMemberWithAllFieldsQuery(Long memberId) {
Member _member = entityManager.createQuery(
"SELECT DISTINCT m " +
"FROM Member m " +
"LEFT JOIN FETCH m.eMailList " +
"WHERE m.memberId = :id ", Member.class)
.setParameter("id", memberId)
.setHint(QueryHints.HINT_PASS_DISTINCT_THROUGH, false)
.getSingleResult();
_member = entityManager.createQuery(
"SELECT DISTINCT m " +
"FROM Member m " +
"LEFT JOIN FETCH m.phoneList " +
"WHERE m in :member ", Member.class)
.setParameter("member", _member)
.setHint(QueryHints.HINT_PASS_DISTINCT_THROUGH, false)
.getSingleResult();
return Optional.of(_member);
}
Thanks for your help/hints!
Try to use Set instead of List ? I've run into this exception and solved it after changing my collection to Set
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
#JoinColumn(name = "person_id")
private Set<Phone> phoneList = new HashSet<>();
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
#JoinColumn(name = "person_id")
private Set<EMail> eMailList = new HashSet<>();

JPQL: How to join via #ElementCollection with #MapKeyJoinColumn

I have problems creating the correct JPQL query for joining through the following tables:
While between GROUPS and USERS there is a conventional #ManyToMany mapping table, DOCUMENTS_GROUPS is what causes the trouble. As you can see in the following entity, I want the relationship between DOCUMENTS and GROUPS to be mapped as a Map containing the access_mode (which works just fine except for the query):
#Entity
#Table(name = "DOCUMENTS")
#NamedQueries({
#NamedQuery(
name = "Documents.findAccessibleByUser",
query = "SELECT d FROM Document d INNER JOIN d.groups g INNER JOIN KEY(g).members m WHERE m.id = :userId"
)
})
public class Document {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
#ElementCollection
#CollectionTable(name = "DOCUMENTS_GROUPS", joinColumns = {#JoinColumn(name = "document_id")})
#MapKeyJoinColumn(name = "group_id")
#Column(name = "access_mode")
#Enumerated(EnumType.STRING)
private Map<Group, AccessMode> groups = new HashMap<>();
/* ... */
}
With Group being rather normal:
#Entity
#Table(name = "GROUPS")
public class Group {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
#Column(length = 255)
private String name;
#ManyToMany
#JoinTable(name = "USERS_GROUPS", //
joinColumns = {#JoinColumn(name = "group_id")}, //
inverseJoinColumns = {#JoinColumn(name = "user_id")} //
)
private Set<User> members = new HashSet<>();
/* ... */
}
My question is now: How do I need to modify the second JOIN in my JPQL query?
SELECT d FROM Document d
INNER JOIN d.groups g
INNER JOIN KEY(g).members m
WHERE m.id = :userId
is syntactically wrong (unexpected KEY after INNER JOIN).
Of course, I have already tried a plain INNER JOIN g.members m, but since we're dealing with a Map<Group, AccessMode>, this fails with cannot dereference scalar collection element: members.
I was facing the same problem with a simple key-value Map<String, String> like:
#Entity Item.java
#ElementCollection
#MapKeyColumn(name = "name")
#Column(name = "value")
#CollectionTable(indexes = #Index(columnList = "value"))
private Map<String, String> attributes = new HashMap<>();
Joining the attributes was possible:
Query query = em.createQuery("SELECT i FROM Item i INNER JOIN i.attributes attr");
but not querying fields:
Query query = em.createQuery("SELECT i FROM Item i INNER JOIN i.attributes attr WHERE attr.value = 'something'");
I debugged the Hibernate internals and found out that the alias attr is already resolved to the value (e.attributes.value), so the only thing you can do here is:
Query query = em.createQuery("SELECT i FROM Item i INNER JOIN i.attributes attr WHERE attr = 'something'");
But I did not find any documentation or JPQL examples pointing that out. The behaviour is is useless in my case, because I want to have conditions for both key and value. Thats why I migrated to a foreign entity collection with key mapping and composite primary key. Its way more complicated but works as expected.
The composite key entity to prevent single primary keys
#Embeddable
public class ItemAttributeName implements Serializable {
private String name;
#ManyToOne
#JoinColumn(nullable = false)
private Item item;
// Empty default constructor is important
public ItemAttributeName() {
}
public ItemAttributeName(Item item, String name) {
this.item = article;
this.name = name;
}
}
The real attribute entity
#Entity
public class ItemAttribute {
#EmbeddedId
private ItemAttributeName id;
private String value;
// Empty default constructor is important
public ItemAttribute() {
}
public ItemAttribute(Item item, String name) {
this.id = new ItemAttributeName (item, name);
}
public String getValue() {
return value;
}
}
#Entity Item.java
#OneToMany(mappedBy = "id.item",cascade = CascadeType.PERSIST)
#MapKeyColumn(name = "name")
public Map<String, ItemAttribute> attributes = new HashMap<>();
Creating entities:
Item item = new Item ();
ItemAttribute fooAttribute = new ItemAttribute(item, "foo");
fooAttribute.setValue("356");
item.attributes.put("foo", fooAttribute);
Querying entities:
Query query = em.createQuery("SELECT i FROM Item i JOIN i.attributes attr WHERE attr.id.name = 'foo' AND attr.value='bar'");
List<Item> resultList = query.getResultList();
System.out.println(resultList.get(0).attributes.get("foo").getValue());
Prints out: bar

JPA Create Criteria with GROUP COUNT and HAVING on nested objects

Given this SQL query
SELECT
ug.lookup_key,
count(ug.id) as count
FROM user u
INNER JOIN user_group ug on ug.id = u.id
WHERE
u.age >= 11 AND
u.age <= 20 AND
ug.lookup_key in('12345')
GROUP BY ug.lookup_key
HAVING count(ug.id) < 7
I have written this
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Object[]> query = criteriaBuilder.createQuery(Object[].class);
Root<UserGroup> d = query.from(UserGroup.class);
Join<UserGroup, User> join = d.join("users");
Predicate pred1 = criteriaBuilder.between(join.get("age"), ageFrom, ageTo);
Expression<String> exp = d.get("lookupKey");
Predicate pred2 = exp.in(lookupKeys);
query.where(pred1, pred2);
query.multiselect(d.get("lookupKey"), criteriaBuilder.count(d.get("id"))).groupBy(d.get("lookupKey"));
List<Object[]> results = entityManager.createQuery(query).getResultList();
for(Object[] object : results){
System.out.println(object[0] + " " + object[1]);
}
The SQL returns {"12345",4} whereas the code returns {"12345", 37}. The SQL is the correct result. There are 37 users in the database for groups with that lookup key, so I understand where the numbers are coming from but I do not understand how to do the JOIN, GROUP BY, HAVING with the CreateCriteria query so that I get the results. I don't want to use JPQL.
The entities...
#Entity
public class User {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String name;
private int age;
private double salary;
#ManyToOne(optional=false,cascade=CascadeType.ALL, targetEntity=UserGroup.class)
#JsonBackReference
private UserGroup group;
// Getters and Setters //
}
#Entity
public class UserGroup {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String name;
private String lookupKey;
#OneToMany(mappedBy="group",targetEntity=User.class, fetch=FetchType.EAGER)
#JsonManagedReference
private Collection users;
// Getters and Setters //
}
And also, here is the method in which it is implemented
public void summarizeGroupsByLookupKey(long ageFrom, long ageTo, List<String> lookupKeys, long numUsers){
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Object[]> query = criteriaBuilder.createQuery(Object[].class);
Root<UserGroup> d = query.from(UserGroup.class);
Join<UserGroup, User> join = d.join("users");
Predicate pred1 = criteriaBuilder.between(join.get("age"), ageFrom, ageTo);
Expression<String> exp = d.get("lookupKey");
Predicate pred2 = exp.in(lookupKeys);
query.where(pred1, pred2);
query.multiselect(d.get("lookupKey"), criteriaBuilder.count(d.get("id")));
query.groupBy(d.get("lookupKey"));
query.having(criteriaBuilder.<Long>lessThan(criteriaBuilder.count(d.get("id")), numUsers));
List<Object[]> results = entityManager.createQuery(query).getResultList();
for(Object[] object : results){
System.out.println(object[0] + " " + object[1]);
}
}
By way of info...using Spring Boot 1.5.1 and all the default JPA, Hibernate, etc. from there.
Can a JPA expert offer some help? Thanks!
Change the multiselect part to use countDistinct(..)
query.multiselect(d.get("lookupKey")
,criteriaBuilder.countDistinct(d.get("id")));
and also having(..)
query.having(criteriaBuilder.<Long>lessThan(
criteriaBuilder.countDistinct(d.get("id")), numUsers)
);
Original query returned row per matching user in which rows userGroup.id was then multiplied.

Group By in Java Persistence/JPQL

I have an Entity class and it has #ManyToOne relationships. I need to use GROUP BY as in SQL query.
I have written a JPQL but its not working. My code is :
#NamedQuery(name = "AssetDepModel.findByAssedId",
query = "SELECT dep FROM AssetDepModel dep "
+ "JOIN dep.faDetails fad "
+ "WHERE fad.assetId.assId = :assetId_passed "
+ "GROUP BY dep.faDetails,dep.faDetails.id,dep.fiscalModel.fyId,dep.depAmt,dep.depId,dep.depMethodId,dep.depRate,dep.depTypeId,dep.quarterId,dep.createdDt,dep.createdBy,dep.updatedDt,dep.updatedby "
+ "ORDER BY fad.id")
public class AssetDepModel implements Serializable{
/**
*
*/
private static final long serialVersionUID = 1L;
public static final String FIND_BY_ASSET_ID = "AssetDepModel.findByAssedId";
public static final String FIND_BY_DETAIL_ID = "AssetDepModel.findByDetailId";
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "dep_id")
private int depId;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "fiscal_id", referencedColumnName = "fy_id")
private FiscalYrModel fiscalModel;
#Column(name = "quarter_id")
private int quarterId;
#ManyToOne
#JoinColumn(referencedColumnName = "id", name = "fa_details_id")
private FADetailsModel faDetails;
#Column(name = "dep_type_id")
private int depTypeId;
#Column(name = "dep_method_id")
private int depMethodId;
#Column(name = "dep_rate")
private Double depRate;
#Column(name = "dep_amt")
private Double depAmt;
#Column(name = "created_dt")
#Temporal(TemporalType.TIMESTAMP)
private Date createdDt;
#Column(name = "created_by")
private int createdBy;
#Column(name = "updated_dt")
#Temporal(TemporalType.TIMESTAMP)
private Date updatedDt;
#Column(name = "updated_by")
private int updatedby;
I tried this code but while calling the JPQL it always gives error saying that objects in Select is not included in Group By clause.
I need to GROUP BY according to a foreign key field.
I get following error :
Internal Exception: com.microsoft.sqlserver.jdbc.SQLServerException: Column
'inv_asset_depreciation.fa_details_id' is invalid in the select list because
it is not contained in either an aggregate function or the GROUP BY clause.
Error Code: 8120
Call: SELECT t0.dep_id, t0.created_by, t0.created_dt, t0.dep_amt, t0.dep_method_id,
t0.dep_rate, t0.dep_type_id, t0.quarter_id, t0.updated_dt, t0.updated_by,
t0.fa_details_id, t0.fiscal_id FROM inv_asset_depreciation t0, fiscal_yr t2,
inv_fixed_asset_detail_mcg t1 WHERE ((t1.asset_id = ?) AND ((t1.id = t0.fa_details_id)
AND (t2.fy_id = t0.fiscal_id))) GROUP BY t1.id, t1.asset_given_name,
t1.brand_name_description, t1.created_by, t1.created_date,
t1.dispose_dt_en,t1.dispose_dt_np, t1.dispose_value, t1.req_form_no,
t1.start_use_dt_en,t1.start_use_dt_np,t1.update_count, t1.updated_by,
t1.updated_date, t1.asset_id,t1.dept_id, t1.status, t1.id,t2.fy_id, t0.dep_amt,
t0.dep_id, t0.dep_method_id,t0.dep_rate, t0.dep_type_id,t0.quarter_id,
t0.created_dt, t0.created_by,t0.updated_dt, t0.updated_by
ORDER BY t1.id
bind => [1 parameter bound]
Query: ReportQuery(name="AssetDepModel.findByAssedId" referenceClass=AssetDepModel
sql="SELECT t0.dep_id, t0.created_by, t0.created_dt, t0.dep_amt,
t0.dep_method_id,t0.dep_rate,t0.dep_type_id, t0.quarter_id, t0.updated_dt,
t0.updated_by, t0.fa_details_id,t0.fiscal_id FROM inv_asset_depreciation t0,
fiscal_yr t2, inv_fixed_asset_detail_mcg t1 WHERE ((t1.asset_id = ?)
AND ((t1.id = t0.fa_details_id) AND (t2.fy_id = t0.fiscal_id)))
GROUP BY t1.id, t1.asset_given_name, t1.brand_name_description,
t1.created_by,t1.created_date, t1.dispose_dt_en, t1.dispose_dt_np,
t1.dispose_value, t1.req_form_no, t1.start_use_dt_en, t1.start_use_dt_np,
t1.update_count, t1.updated_by, t1.updated_date,t1.asset_id, t1.dept_id,
t1.status, t1.id, t2.fy_id, t0.dep_amt, t0.dep_id, t0.dep_method_id,
t0.dep_rate, t0.dep_type_id, t0.quarter_id, t0.created_dt,
t0.created_by, t0.updated_dt,t0.updated_by ORDER BY t1.id")
I modified a little bit like this :
#SuppressWarnings("unchecked")
public List<Object> findByAssetIdForSaleWriteOff(int assetId){
Query query = getEntityManager().createQuery("SELECT fad.id,dep.depAmt FROM AssetDepModel dep "
+ "JOIN dep.faDetails fad "
+ "WHERE fad.assetId.assId = "+assetId+" "
+ "GROUP BY fad.id,dep.depAmt "
+ "ORDER BY fad.id",AssetDepModel.class);
return (List<Object>)query.getResultList();
}
List<Object> objList = assetDepEJB.findByAssetIdForSaleWriteOff(faObj.getAssId());
Double amountDepTillNow = 0.0;
int fadId = 0;
int i=0;
for (Iterator<Object> iterator3 = objList.iterator(); iterator3
.hasNext();) {
Object[] obj = (Object[]) iterator3
.next();
if (i>0) {
if (fadId != (Integer) obj[0]) {
break;
}
}
fadId = (Integer) obj[0];
amountDepTillNow += (Double)obj[1];
i++;
}
It worked for me but If there is another efficient way, PLEASE DO SUGGEST ME.