Given
#Entity
public class Document {
#Id
#Column(name = "DOCUMENT_ID")
private Long id;
#ElementCollection
#CollectionTable(
name="TAG",
joinColumns=#JoinColumn(name="DOCUMENT_ID")
)
#Column(name="TAG")
private Set<String> tags;
}
find all documents tagged with a specific collection of tags. Essentially, an EclipseLink equivalent of:
SELECT d FROM Document d WHERE :tag1 MEMBER OF d.tags
INTERSECT
SELECT d FROM Document d WHERE :tag2 MEMBER OF d.tags
...
SELECT d FROM Document d WHERE :tagn MEMBER OF d.tags
but using a JPA CritieraQuery.
Use an aggregate query with a having clause to select rows that matched all the required tags:
CriteriaBuilder cb = entityManager().getCriteriaBuilder();
CriteriaQuery<Long> q = cb.createQuery(Long.class);
Root<Document> from = q.from(Document.class);
List<Predicate> predicates = new ArrayList<Predicate>();
Expression<Long> id = from.get("id");
Expression<Collection<String>> documentTags = from.get("tags");
for (String tag : searchedTags) {
Expression<String> param = cb.literal(tag);
Predicate predicate = cb.isMember(param, documentTags);
predicates.add(predicate);
}
q.multiselect(id).where(
cb.or(predicates.toArray(new Predicate[0])));
q.distinct(true);
q.groupBy(id);
q.having(cb.equal(cb.count(id), searchedTags.size()));
TypedQuery<Long> query = entityManager().createQuery(q);
Related
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
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.
I have two entity:
public class public class Person implements Serializable {
private static final long serialVersionUID = -8729624892493146858L;
#Column(name="name")
private String name;
...
#JoinColumn(name = "idcity",referencedColumnName = "id",nullable = true)
#ManyToOne(targetEntity = City.class, fetch = FetchType.EAGER)
private City city
...
}
and the related entity (extract):
public class City{
Long id;
String name;
...
}
Now i'm creating a criteria query in a standard way, querying the Person class:
CriteriaBuilder cb = getEntityManager().getCriteriaBuilder();
CriteriaQuery query = cb.createQuery(selectClass);
Root<T> root = query.from(this.entityClass);
Selection selezioni[] = new Selection[selections.length];
for(int i=0; i< selections.length; i++){
selezioni[i] = CriteriaHelper.getField(selections[i], cb, root);
}
query.select(cb.construct(selectClass, selezioni));
where entityClass is Person and selection and selectClass are used to compile the SELECT clause. In the select i've person.city.name field.
This system create a query with where clause:
select person.name, ..., city.name from person, city WHERE person.idcity = city.id...
but city is not required, so the records without city are not fetched.
Without changing all my automatic system, does exists a simpler way to force the use on LEFT JOIN for the relationship than adding a system to create root.join("field",LEFT)?
Note: the method CriteriaHelper.getField() return a Path starting from the root object
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();
I'm using jpa and I have the following entity:
#Entity
#Table(name="favorites_folders")
public class FavoritesFolder {
private static final long serialVersionUID = 1L;
#Id
private String id;
#NotNull
#Size(min = 1, max = 50)
public String name;
#ElementCollection(fetch = FetchType.LAZY)
#CollectionTable(
name="favorites_products",
joinColumns=#JoinColumn(name="folder_id")
)
#Column(name="product_id")
#NotNull
private Set<String> productsIds = new HashSet<String>();
}
What I want to do is to get a set of FavoritesFolder entities that contains the string "favorite-id" in their productsIds member set.
Does anyone know how can it be done in criteria api?
Update:
I'm thinking the following sql should do the trick but I'm not sure how to do it in either JPQL or Criteria API:
select * from favorites_folders join favorites_products on favorites_folders.id = favorites_products.folder_id where favorites_products.product_id = 'favorite-id'
To get a set of FavoritesFolder entities that contains the string "favorite-id" in their productsIds member set using criteria api you should do the following:
CriteriaBuilder cb = em.getCriteriaBuilder(); //em is EntityManager
CriteriaQuery<FavoritesFolder> cq = cb.createQuery(FavoritesFolder.class);
Root<FavoritesFolder> root = cq.from(FavoritesFolder.class);
Expression<Collection<String>> productIds = root.get("productsIds");
Predicate containsFavoritedProduct = cb.isMember("favorite-id", productIds);
cq.where(containsFavoritedProduct);
List<FavoritesFolder> favoritesFolders = em.createQuery(cq).getResultList();
More information on Collections in JPQL and Criteria Queries.
Just another way using IN
#Entity
public class UserCategory implements Serializable {
private static final long serialVersionUID = 8261676013650495854L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#ElementCollection
private List<String> categoryName;
(...)
}
Then you can write a Criteria query like
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<UserCategory> q = cb.createQuery(UserCategory.class);
Root<UserCategory> root = q.from(UserCategory.class);
Predicate predicate = cb.conjunction();
Predicate p1 = cb.equal(root.get(UserCategory_.targetSiteType), siteType.getName());
Predicate p2 = root.get(UserCategory_.categoryName).in(category);
predicate = cb.and(p1,p2);
q.where(predicate);
TypedQuery<UserCategory> tq = entityManager.createQuery(q);
List<UserCategory> all = tq.getResultList();
if (all == null || all.size() == 0){
return null;
}else if (all.size() > 1){
throw new Exception("Unexpected result - "+all.size());
}else{
return all.get(0);
}
This is my work around that works.
I'm using Springboot 1.5.9. I don't have time to identify the root cause. What I know is such nested property been ignored when get through JacksonMappingAwareSortTranslator.
So what I did to workaround this is not to use Sort object created by resolvers.
Here's my code in Kotlin. Without doing this, the pageable.sort is null and sorting does not work. And my code will create a new PageRequest object that has non-null sort that works.
#RequestMapping("/searchAds", method = arrayOf(RequestMethod.POST))
fun searchAds(
#RequestBody cmd: AdsSearchCommand,
pageable: Pageable,
resourceAssembler: PersistentEntityResourceAssembler,
sort: String? = null
): ResponseEntity<PagedResources<Resource<Ads>>> {
val page = adsService.searchAds(cmd, pageable.repairSortIfNeeded(sort))
resourceAssembler as ResourceAssembler<Ads, Resource<Ads>>
return adsPagedResourcesAssembler.toResource(page, resourceAssembler).toResponseEntity()
}
fun Pageable.repairSortIfNeeded(sort: String?): Pageable {
return if (sort.isNullOrEmpty() || this.sort != null) {
this
} else {
sort as String
val sa = sort.split(",")
val direction = if (sa.size > 1) Sort.Direction.valueOf(sa[1]) else Sort.Direction.ASC
val property = sa[0]
PageRequest(this.pageNumber, this.pageSize, direction, property)
}
}