How can I retrieve multiple objects with JPA? - java

I'm using JPA2/hibernate with this data model:
class Stock {
#ManyToOne
private StockGroup stockGroup;
private boolean visible;
}
class StockGroup {
#OneToMany(mappedBy = "stockGroup")
private List<Stock> stocks;
}
I would like to retrieve StockGroup's containing Stock's where visible==true.
I've come up with this faulty code:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<StockGroup> q = cb.createQuery(StockGroup.class);
Root<StockGroup> r = q.from(StockGroup.class);
Join<StockGroup, Stock> j = r.join(StockGroup_.stocks, JoinType.INNER);
Predicate p = cb.equal(j.get(Stock_.visible), true);
// This becomes a cartesian product :(
List<StockGroup> l = em.createQuery(q.where(p)).getResultList();
// Stocks are not filtered on visible :(
l.get(0).getStocks();
Is it possible to retrieve the StockGroup and Stock Objects with one CriteriaQuery or can JPA only fill one type at once? Or can i add some Criteria when .getStocks() is filled lazily?

The trick to doing this is returning a tuple containing an old-fashioned join between the Stock and the StockGroup, like this:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Tuple> q = cb.createQuery(Tuple.class);
Root<Stock> sRoot = q.from(Stock.class);
Root<StockGroup> sgRoot = q.from(StockGroup.class);
q.select(cb.tuple(sRoot, sgRoot)).where(
cb.and(cb.equal(sRoot.get(Stock_.stockGroup), sgRoot),
cb.isTrue(sRoot.get(Stock_.visible))));
List<Tuple> l = em.createQuery(q).getResultList();
The tuple is then not fully type safe, but you can reach it by position (or by alias, if you've given your select expressions or roots an alias):
for (Tuple t : l) {
Stock s = (Stock) t.get(0);
StockGroup sg = (StockGroup) t.get(1);
System.out.println("Stock is : " + s + " .... StockGroup: " + sg);
}
There's a good article on IBM DeveloperWorks on JPA2 Typesafe Queries.
Good luck in your JPA2 endeavors!

Related

JPA - find by multiple attributes in collections of objects

I have an event object with following attributes:
class Event {
String name;
String location;
LocalDateTime date;
String description;
}
Lets say I get from web API a list of events:
List<Events> events = getEvents(); // e.g. 5 events
And now I want to check how many of these events I already have in my DB.
Event is unique if combination of values: name, location and date is also unique.
So basically I want to a create query to do this:
Optional<Event> getByNameAndLocationAndDate(String name, String location, LocalDate date);
but for a list of item in just one query. Something like:
Optional<Event> getByNameAndLocationAndDate(List<Events> events);
Is it possible with JPA?
There is no built-in or specially pretty way of doing this. But you could generate a query by using a loop:
public List<Event> getByNameAndLocationAndDate(List<Event> events) {
if (events.isEmpty()) {
return new ArrayList<>();
}
final StringBuilder queryBuilder = new StringBuilder("select e from Event e where ");
int i = 0;
for (final Event event : events) {
if (i > 0) {
queryBuilder.append("or")
}
queryBuilder.append(" (e.name = :name" + i);
queryBuilder.append(" and e.location = :location" + i);
queryBuilder.append(" and e.date = :date" + i + ") ");
i++;
}
final TypedQuery<Event> query = em.createQuery(queryBuilder.toString());
int j = 0;
for (final Event event : events) {
query.setParameter("name" + j, event.getName());
query.setParameter("location" + j, event.getLocation());
query.setParameter("date" + j, event.getDate());
}
return query.getResultList();
}
Like I said, not very pretty. Might be better with criteria API. Then again, unless you have very strict requirements for execution speed, you might be better off looping through the list checking one event at the time. It will result in the more queries run against the database, but also much prettier code.
Edit: Here is attempt using criteria API, haven't used it much so created just by googling, no guarantee it works as it is..
public List<Event> getByNameAndLocationAndDate(List<Event> events) {
if (events.isEmpty()) {
return new ArrayList<>();
}
final CriteriaBuilder cb = em.getCriteriaBuilder();
final CriteriaQuery<Event> query = cb.createQuery(Event.class);
final Root<Event> root = query.from(Event.class);
final List<Predicate> predicates = new ArrayList<>();
final List<Predicate> predicates = events.stream().map(event -> {
return cb.and(cb.equal(root.get("name"), event.getName()),
cb.equal(root.get("location"), event.getLocation()),
cb.equal(root.get("date"), event.getDate()));
}).collect(Collectors.toList());
query.select(root).where(cb.or(predicates.toArray(new Predicate[]{})));
return em.createQuery(query).getResultList();
}
try
List<Event> findByNameInAndLocationInAndDateIn(List<String> names,List<String> locations,List<Date> dates);
but this returns a list, not a single event, if you need verify if one event is not in database, the only way to do this is search one by one,
you can use this function for decide if needs that.
I'm not sure if this function behaves as you wish

How to do a like query with a variable set of strings?

I am triying to do a "like query" with a variable set of strings, in order to retrieve in a single query all texts that contains a set of words, that is:
public long countByTextLike(Set<String> strings) {
CriteriaBuilder builder = manager.getCriteriaBuilder();
CriteriaQuery<Long> query = builder.createQuery(Long.class);
Root<Example> root = query.from(Example.class);
query.select(builder.count(root.get("id"))).where(
builder.and(
builder.equal(root.get("lang"), "EN")
)
);
//this does not work
for (String word : strings) {
query.where(builder.or(builder.like(root.get("text"), word)));
}
return manager.createQuery(query).getSingleResult();
}
unfortunately this does not work because the where is overwritten in each loop. Only the last word of loop is used and "AND" restictions are being overwriten.
How is possible to do a "like query" with a variable number of strings? It is not posible?
I am using the spring framework but i think that the question could be extendable to hibernate
You can use predicates, and then add them all with only one where clause
public long countByTextLike(Set<String> strings) {
CriteriaBuilder builder = currentSession().getCriteriaBuilder();
CriteriaQuery<Long> query = builder.createQuery(Long.class);
Root<Example> root = query.from(Example.class);
Predicate[] predicates = new Predicate[strings.size()];
query.select(builder.count(root.get("id")));
Predicate langPredicate = builder.equal(root.get("lang"), "EN");
int cont = 0;
for (String word : strings) {
Predicate pred = builder.like(root.get("text"), "%" + word + "%");
predicates[cont++] = pred;
}
Predicate orPredicate = builder.or(predicates);
Predicate finalPredicate = builder.and(orPredicate, langPredicate);
return manager.createQuery(query).where(finalPredicate).getSingleResult();
}

jpa 2 criteria hibernate 5.2 nested joins

I'm trying to add to my crud services the possibility to specify what nested relationship I need so I don't have to read everything from the database.
Take for example I have those entities
Company.java
private List<Department> departments;
private SalaryCode salaryCode;
Department.java
private List<Employee> employees;
private Company company;
private SalaryCode salaryCode;
Employee.java
private Department department;
private SalaryCode salaryCode
And my Criteria query for now is this :
Session session = sessionFactory.openSession();
CriteriaBuilder builder = session.getCriteriaBuilder();
CriteriaQuery<T> criteriaQuery = builder.createQuery(clazz);
Root<T> root = criteriaQuery.from(clazz);
//nestedRelationships is a varargs passed as parameters
for(String nestedRelationship : nestedRelationships) {
root.fetch(nestedRelationship, JoinType.LEFT);
}
List<T> result = session.createQuery(criteriaQuery.select(root)).list();
The thing is if I specify "department" as nestedRelationship and querying for Employee entity it works well but when I try to specify "department.salaryCode" it doesn't work saying " Unable to locate Attribute with the the given name ".
Of course I'm fetching "department" first and then "department.salaryCode".
Is it supported? If yes how does it work and if it's not supported what can I do?
Yes,it is supported. You need to use Joins.
Root<Company> root = criteriaQuery.from(Company.class);
Join<Company,Department> joinDepartment = root.join( Company_.departments );
Join<Department,SalaryCode> joinSalaryCode = joinDepartment.join( Department_.salaryCode );
To generate metamodel classes(e.g. Department_ ) have a look at here.
I found a solution by making an algorithm using the Root element
protected void fetch(Root<T> root, String... joins) {
//Sort the joins so they are like this :
//A
//A.F
//B.E
//B.E.D
//B.G
Arrays.sort(joins);
Map<String, Fetch> flattenFetches = new HashMap<>();
for (String join : joins) {
try {
if (join.contains(".")) {
String[] subrelations = join.split("\\.");
Fetch lastRelation = null;
int i;
for (i = subrelations.length - 1; i >= 0; i--) {
String subJoin = String.join(".", Arrays.copyOf(subrelations, i));
if (flattenFetches.containsKey(subJoin)) {
lastRelation = flattenFetches.get(subJoin);
break;
}
}
if (lastRelation == null) {
lastRelation = root.fetch(subrelations[0], JoinType.LEFT);
flattenFetches.put(subrelations[0], lastRelation);
i = 1;
}
for (; i < subrelations.length; i++) {
String relation = subrelations[i];
String path = String.join(".", Arrays.copyOf(subrelations, i + 1));
if (i == subrelations.length - 1) {
Fetch fetch = lastRelation.fetch(relation, JoinType.LEFT);
flattenFetches.put(path, fetch);
} else {
lastRelation = lastRelation.fetch(relation, JoinType.LEFT);
flattenFetches.put(path, lastRelation);
}
}
} else {
Fetch fetch = root.fetch(join, JoinType.LEFT);
flattenFetches.put(join, fetch);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
and to use it I just have to do for example :
employeeController.getAll("punches", "currentSchedule.shifts", "defaultDepartment.currentSchedule.shifts",
"defaultDepartment.company.currentSchedule.shifts", "bankExtras")
I would like to comment the algorithm but I do not have time and it's pretty easy to understand

JPA CriteriaBuilder and Substring

I have a CriteriaBuilder where I am trying to get characters starting from 0 to 10. However I am not able to get the desired output.
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Emp> cq = cb.createQuery(Emp.class);
Root<Emp> c = cq.from(Emp.class);
cb.substring(c.<String>get("projDesc"), 0, 10);
cq.orderBy(cb.desc(c.get("salary")));
Query query = em.createQuery(cq);
.....
What could be the reason for this?
From the javadoc
Create an expression for substring extraction. Extracts a substring of
given length starting at the specified position. First position is 1.
Try doing cb.substring(c.<String>get("projDesc"), 1, 10);
I think you're forgetting to select the Expression<E>
Try cq.select(cb.substring(c.<String>get("projDesc"), 1, 10))
It will return List<String> if you need to Return the Emp you can use the
cb.construct(Emp.class, e.get("prop1"), e.get("prop2"), cb.substring(c.<String>get("projDesc"), 1, 10)));
I also, faced same problem in which I have to substring first three char and fetch all accounts starting from 107 which is number. for that I have used CriteriaBuilder and substring method like below.
Predicate accountNumber = criteriaBuilder.equal(criteriaBuilder.substring(from.get("accountNumber").as(String.class), 0, 3), cwipAcc);
but unfortunately it is not working for CriteriaBuilder and substring. so I have used like query to resolve this issue by given code below.
Predicate accountNumber = criteriaBuilder.like(from.get("accountNumber").as(String.class), String.valueOf(cwipAcc) + "%");
here, I have just fetched all the records which is starting from 107 and so on.
Example:
public List<GLCharts> findAccountForCWIP(Long cwipAcc, Long glInfo) {
Map<String, Object> data = new HashMap();
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<GLCharts> criteriaQuery = criteriaBuilder.createQuery(GLCharts.class);
Root<GLCharts> from = criteriaQuery.from(GLCharts.class);
Predicate accountNumber = criteriaBuilder.like(from.get("accountNumber").as(String.class), String.valueOf(cwipAcc) + "%");
Predicate glCompanyInfo = criteriaBuilder.equal(from.join("gLCompanyInfo").get("id"), glInfo);
Predicate finalPredicate = criteriaBuilder.and(accountNumber, glCompanyInfo);
criteriaQuery.select(from).where(finalPredicate).orderBy(Stream.of(criteriaBuilder.asc(from.get("accountNumber"))).collect(Collectors.toList()));
List<GLCharts> glChartsList = entityManager.createQuery(criteriaQuery).getResultList();
return glChartsList;
}
I faced the problem because my requirement was to use a number into substr. Following is the sample code.
#Override
public List<SampleProfile> findNonSampleProfileBySequence(Long SampleNo) {
List<SampleProfile> profiles = new ArrayList<>();
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<SampleProfile> criteriaQuery = criteriaBuilder.createQuery(SampleProfile.class);
Root<SampleProfile> SampleProfileRoot = criteriaQuery.from(SampleProfile.class);
List<Predicate> predicates = new ArrayList<Predicate>();
if (SampleUtil.isValidLong(SampleNo)) {
String SampleStr = Long.toString(SampleNo);
if (StringUtils.isNotBlank(SampleStr) && SampleStr.length() > 5) {
String SampleSequence = SampleStr.substring(5);
predicates.add(criteriaBuilder.equal(criteriaBuilder.substring(SampleProfileRoot.get(SampleProfile_.id).as(String.class), 6), SampleSequence));
predicates.add(criteriaBuilder.equal(SampleProfileRoot.get(SampleProfile_.address).get(Address_.department), SampleStr.substring(0,3)));
}
}
if (!CollectionUtils.isEmpty(predicates)) {
criteriaQuery.where(criteriaBuilder.and(Iterables.toArray(predicates, Predicate.class)));
profiles = entityManager.createQuery(criteriaQuery).setMaxResults(AbstractJpaDAO.MAX_ROW_LIMIT).getResultList();
}
return profiles;
}
Also note that for performance benefits you have to create an index on the same.
The Cast Keyword is important as Hibernate Dialect will create query like this, thus, it has to match with your index.
CREATE INDEX MY_SCHEMA_OWNER.IDX_SUBSTR_SMP_CODE ON MY_SCHEMA_OWNER.SMP_PROFILE (SUBSTR(**CAST**(SMP_CODE AS VARCHAR2(255 CHAR)),6));

building a criteria query with jpa 2.0 by using a dynamic list

I'm a bit confused while creating a criteriaQuery with JPA 2.0.
Prerequisites:
I have a Gui, where the user can mark some checkboxes of (let us say) wheatherstations with some options like temperature/wind/timeperiod/etc...
Now I want to set up a criteriaQuery to pick just the selected items from a sql database and return it as an object/Map/List for building some DataModels (this will be used for generating a few primefaces charts).
What i have so far:
// for presentation purposes just this mockup-data
Calendar start = new GregorianCalendar(2011, Calendar.APRIL, 1);
Calendar end = new GregorianCalendar(2011, Calendar.MAY, 1);
List<String> selectedStations = new LinkedList<String>() {{
add("PS1");
add("PS2");
add("PS3");
}};
Map<String, Object selectedOptions = new LinkedHashMap<String, Object>() {{
put("opt1","val1");
put("opt2","val2");
put("opt3","val3");
}};
List<String> sel = new LinkedList<String>() {{
add("selOpt1");
add("selOpt2");
add("selOpt3");
}};
criteriaBuilder, criteriaQuery and the mapping class:
// go for the criteriaBuilder
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Tuple> cq = cb.createTupleQuery();
Root<StationItem> r = cq.from(StationItem.class);
Setting up the predicates:
// ... where (name="PS1" or name="PS2" or name="PS3") ...
Predicate p1 = cb.disjunction();
for (String s : selectedStations) {
p1 = cb.or(p1, cb.equal(r.get("name").as(String.class), s));
}
Predicate p2 = cb.between(r.get("fetchDate").as(Date.class),
start.getTime(), end.getTime());
Predicate p3 = cb.conjunction();
for (Map.Entry<String, Object> param : selectedOptions.entrySet())
p3 = cb.and(p3, cb.equal(r.get(param.getKey()), param.getValue()));
And the final step to run the query and fetching the results:
At this point I do not know what is the best approach to fill the multiselect criteria with my selections. I would like to insert all items/selections from the List sel to cq.multiselect() with some kind of a loop in a dynamic way...Any idea is welcome!
// This is working but static :(
cq.multiselect(r.get(sel.get(0)), r.get(sel.get(1)), r.get(sel.get(2)));
// i would prefer to have something like
for (int i=0;i<sel.size();i++) {
cq.multiselect().add(r.get(sel.get(i)));
}
Concatenating my WHERE-clause and executing the query:
cq.where(cb.and(p1,p2,p3));
List<Tuple> res = em.createQuery(cq).getResultList();
for (Tuple t : res) {
// do something ...
};
return <something useful>
Following a pseudo SQL query to sum up what I want to achieve:
SELECT {items from List<String> sel}
FROM MyStationDatabase
WHERE (name = selectedStation.get(0) OR ... OR name = selectedStation.get(last))
AND {items from Map<String,Object> selectedOptions}
Well, sometimes it's too trivial to be true -.-
One way to fill the cq.multiselect() with my dynamic list is to just create a list of selections and pass this over to my multiselect-query.
List<Selection<?>> s = new LinkedList<Selection<?>>();
for (String item : sel) {
s.add(r.get(item));
}
cq.multiselect(s);
easy, but maybe someone has the same struggles with this :)
and even if not, see it as an example for a criteriaQuery ;)

Categories