Limit records on onetomany relationship in Play! Framework - jpa

I am trying to determine the best way to page/limit the rows returned when querying the children of a OneToMany relationship while using JPA in Play! Framework.
#Entity
public class User extends Model {
#OneToMany(mappedBy="user", cascade=CascadeType.ALL)
public List<CaseFolder> caseFolders;
}
public class CaseFolder extends Model {
#Required
#ManyToOne
public User user;
#Required
public String number;
public String description;
}
I realize I can set the relationship the lazy fetching. However, that still doesn't seem to stop me from retrieving the entire list of CaseFolders when I finally access user.caseFolders.
Ideally, I would like to be able to do something like:
user.getCaseFolders().start(100).limit(10)
but I can't find anything about doing that "out of the box".
Does everyone really bring the entire related data set into memory every time they need a few of the "children" of a 1-m relationship?
I'm implementing a UI that has paging (using jqgrid) and a user can potentially have thousands of records.

After further research, here's the answer I came up with:
The "many" side of the relationship - the collection of all caseFolders - is a property of the User. By definition, that property's value is the entire collection.
To obtain a subset of those elements, I added the following method to my User model:
public List<CaseFolder> getCaseFolders(String sidx, String sord, int start, int limit) {
String orderBy = "c." + sidx + " " + ((sord.toUpperCase().equals("ASC")) ? "ASC" : "DESC");
String jpql = "SELECT c " +
"FROM CaseFolder c " +
"WHERE c.user = :user " +
"ORDER BY " + orderBy;
Query query = JPA.em().createQuery(jpql)
.setParameter("user", this)
.setFirstResult(start)
.setMaxResults(limit);
List<CaseFolder> result = query.getResultList();
return result;
}

Related

Dealing with non-spring data documents in couchbase

Is there a recommended way to go about dealing with documents that don't have the _class field with spring-data-couchbase( if there is one)? Trying it simply just throws an exception as expected.
Edit: Apologies if this was a bit too vague, let me add a bit more context.
I want to fetch data from couchbase for some student by name, let's say . The repository looks something like -
#Repository
public interface StudentRepository extends CouchbaseRepository {
Optional<StudentDocument> findByName(String name);
}
Now the documents in couchbase don't have the _class field OR say if we are entering a different "key" and "value" for _class field as we don't want to rely on it, so this method fails. I sort of hacked a workaround for this using -
`
#Override
public Student getStudent(String name) {
N1qlQuery query = N1qlQuery.simple(String.format("select *, META().id AS _ID, META().cas AS _CAS" +
" from student where name = \'%s\';", name));
return Optional.ofNullable(studentRepository.getCouchbaseOperations()
.findByN1QL(query, StudentWrapper.class)
.get(0))
.map(StudentWrapper::getStudent)
.orElseGet(() -> {
throw new HttpClientErrorException(HttpStatus.NOT_FOUND);
});
}
`
I was wondering if there is an alternate way of achieving this
While using Spring spEL, Couchbase will automatically include the _class (or whatever attribute you have defined as your type) for you:
public interface AreaRepository extends CouchbaseRepository<Area, String> {
//The _class/type is automatically included by Couchbase
List<Area> findByBusinessUnityIdAndRemoved(String businessId, boolean removed);
}
However, if you want to use N1QL, you have to add the #{#n1ql.filter} :
public interface BusinessUnityRepository extends CouchbaseRepository<BusinessUnity, String>{
#Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and companyId = $2 and $1 within #{#n1ql.bucket}")
BusinessUnity findByAreaRefId(String areaRefId, String companyId);
}
the #{#n1ql.filter} will automatically add the filter by type for you.

I am trying to use dynamic order by but the list retrieved is not ordered

public List<Series> findSeries(int period, String fieldname, int num) {
TypedQuery<Series> query = em.createQuery(
"select s from Series s where s.period = ?1 order by ?2",
Series.class);
query.setParameter(1, period);
query.setParameter(2, fieldname);
query.setMaxResults(num);
return query.getResultList();
}
This is the method I am using. I think order by isn't even getting executed, it doesn't give any error even when I pass incorrect fieldname.
When it comes to dynamic limit and ordering, its best to use PagingAndSortingRepository so now my Repository extends this repository. I can simply use JPA criteria query as below.
If u want to learn more about JPA criteria query i found this very helpful http://docs.spring.io/spring-data/data-jpa/docs/1.0.x/reference/html/#jpa.query-methods.query-creation
#Repository
public interface SeriesRepository extends PagingAndSortingRepository<Series,Long>{
List<Series> findByPeriod(int period, Pageable pageable);
}
And then when I call this method from my dao i can just instantiate PageRequest which is one of the implementation of Pageable. I can add limit and sorting order to this instance.
public List<Series> getSeriesByFilter(int period, String fieldname, int num) {
Sort sort = new Sort(Sort.Direction.ASC, fieldname);
Pageable pageable = new PageRequest(0, num, sort);
return seriesRepo.findByPeriod(period, pageable);
}
You cannot pass variables as column name in order by.
There is a work around which may help you achieve what you are trying.
public List<Series> findSeries(int period, String fieldname, int num) {
String query = "select s from Series s where s.period = "+period+" order by "+fieldname;
return entityManager.createQuery(query).getResultList();
}
Check this question Hibernate Named Query Order By parameter
There are ways to pass column name in order by in ASP, however I am not able to find anything in Spring or JPA.
"Order By" using a parameter for the column name
http://databases.aspfaq.com/database/how-do-i-use-a-variable-in-an-order-by-clause.html

Correct way to statelessly update a one-to-many relationship in JPA?

I have a REST interface for a datamodel that has several one-to-many and many-to-many relationships between entities. While many-to-many relationships seem easy to manage statelessly, I'm having trouble with one-to-many. Consider the following one-to-many relationship:
Employee:
#ManyToOne
#JoinColumn(name = "Company_id")
private Company company;
Company:
#OneToMany(mappedBy = "company", cascade = CascadeType.ALL, orphanRemoval=true)
public Set<Employee> employees = new HashSet<Employee>();
When a company is updated, its employee collection may have been updated as well (employees removed or added) but since the REST interface only allows updating the company as a whole, I cannot explicitly delete or add employees.
Simply replacing the collection does not work, but I found that this seems to work:
public void setEmployees(Set<Employee> employee) {
this.employees.clear(); // magic happens here?
this.employees.addAll(employees);
for (Iterator<Employee> iterator = employees.iterator(); iterator.hasNext();) {
Employee employee = (Employee) iterator.next();
employee.setCompany(this);
}
}
Is this the way it should be done, or is there a better way?
EDIT: In fact the above does not work! It appears to work at first, but then it will break with:
Exception in thread "main" java.lang.IllegalStateException: An entity copy was already assigned to a different entity.
I assume this happens because the db already contains a set of employees and if any of the "old" employees are also part of the replacement set, they collide with the ones in the database.
So what is the right way to replace the set?
First make sure equals is implemented properly. As per hibernate spec: http://docs.jboss.org/hibernate/orm/4.1/manual/en-US/html/ch04.html#persistent-classes-equalshashcode
I had a similar problem doing a merge. Essentially I had to fetch the existing employees associated with the company. I had to merge any changes to existing employees, and then add any new employees.
Query query = em.createQuery("select e from Employee e where e.company = '" + company.getId() + "'");
Collection<Employee> existingEmployees = new LinkedList<Employee>();
try{
Iterables.addAll(existingEmployees, (Collection<Employee>) query.getResultList());
}
catch(NoResultException nre){
//No results
}
for(Employee existingEmployee : existingEmployees){
for(Employee employee : company.getEmployees()){
if(existingEmployee.name().equals(employee.name())){
employee.setId(existingEmployee.getId());
}
employee.setCompany(company);
}
}
i think you have no better choice then to replace the existing collection and simply set the new one provided by the REST response.

JPQL LIKE expression with attributes of target entity in a null valued relationship

I have an entity named Item with relationship to another entity Category which can be null. The two entities are as follows:
Item
#Entity
public class Item {
#Id
private String id;
private String name;
private String code;
#ManyToOne
private Category category;
}
Category
#Entity
public class Category {
#Id
private String id;
private String name;
}
Now, I have to select items having name, code or category name similar to a search term, for which I tried the following query:
"SELECT item FROM Item item "
+ "WHERE item.code LIKE :searchTerm OR item.name LIKE :searchTerm "
+ "OR item.category.name LIKE :searchTerm"
searchTerm is set using
query.setParameter("searchTerm", "%" + searchTerm + "%");
But it doesn't result will be empty if category is null (otherwise works). I tried the following query also.
"SELECT item FROM Item item "
+ "WHERE item.code LIKE :searchTerm OR item.name LIKE :searchTerm "
+ "OR (item.category IS NOT NULL AND item.category.name LIKE :searchTerm)"
This also didn't work. How can I make it to check for category name only if category is not null?
Try to use a left join:
"SELECT item FROM Item item LEFT JOIN item.category cat"
+ "WHERE item.code LIKE :searchTerm OR item.name LIKE :searchTerm "
+ "OR (cat.categoryID IS NOT NULL AND cat.name LIKE :searchTerm)"
JPQL documentation
Simply check null for the above parameterized value. The jpa does not return null pointer exception because we give the null only within the double quotes. So like java, it take null as a string and tries the value of '%null%' from your db. So that, it returns empty list.

JPA: Selecting entities based on multiple criterions on multiple child entities

I have a problem getting the following scenario to work. A student can take tests. A student have over time taken a few tests and got a score for each test. Each student entity have a list of tests that they have completed mapped as #OneToMany.
Now I want to select all students that have completed tests on a range of grouped criterions. I want for example to search for all students that have:
Group 1: Completed "Test 1" and got a score "between 75 and 100"
and/or
Group 2: Completed "Test 2" and got a score "between 50 and 80"
This is what I have so far but it does not do what I need (cannot search by multiple parameters meaning that I have to perform the query multiple times):
SELECT s FROM Student s JOIN s.tests t WHERE t.score BETWEEN :minScore AND :maxScore AND t.testName = :testName
Is there a way to use a single NamedQuery to achieve what I want? To retrieve all Students that have completed a test that matches at least one of the parameter groups above? I've been experimenting with joins but keep running into the wall.
I made a sample code skeleton below to illustrate what I'm trying to do.
#Entity
#NamedQueries({
#NamedQuery(name="Student.findStudentByParams", query="????????") // What should this query look like to satisfy the criteria? (see below for more detail)
})
public class Student {
// .. Some other variables that are not relevant for this example
#Id
private String name;
#OneToMany(fetch=FetchType.EAGER, mappedBy = "student")
private List<Test> tests;
// Setters and getters
}
#Entity
public class Test {
private double score;
private String testName;
// .. Some other variables that are not relevant for this example
#ManyToOne(cascade=CascadeType.ALL)
private Student student;
// Setters and getters
}
public class SearchParameters {
private double minScore;
private double maxScore;
private String testName;
public SearchParameters(String minScore, String maxScore, String testName) {
this.minScore = minScore;
this.maxScore = maxScore;
this.testName = testName;
}
// Setters and getters
}
public class MainClass {
public static List<Student> getStudents(List<SearchParameters> searchParams) {
// Database initialization stuff
// What should the query look like to find all students that match any of the combined requirements in the searchParams list?
// Is it possible to do in a single query or should i make multiple ones?
// What parameters should i set? Is it possible to put in the entire array and do some sort of join?
// Retrieve all students which matches any of these search parameters:
// Have either:
// Completed "Test 1" and got a score between 75 and 100
// and/or:
// Completed "Test 2" and got a score between 50 and 80
Query namedQuery = em.createNamedQuery("Student.findStudentByParams");
namedQuery.setParameter(??);
return (List<Student>)namedQuery.getResultList();
}
public static void main() {
List<SearchParams> searchParams = new ArrayList<SearchParams();
searchParams.add(new SearchParameters(75,100, "Test 1"));
searchParams.add(new SearchParameters(50,80, "Test 2"));
// Retrieve all students which matches any of these search parameters:
// Have either:
// Completed "Test 1" and got a score between 75 and 100
// and/or:
// Completed "Test 2" and got a score between 50 and 80
ArrayList<Student> students = getStudents(searchParams);
for(Student s: students) // Print all user that match the criteria
{
System.out.println("Name: " + s.getName());
}
}
}
You need to use Criteria Builder (and eventually the canonical Metamodel).
Try something like this (code not tested):
EntityManager em; // put here your EntityManager instance
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Student> cq = cb.createQuery(Student.class);
Root<Student> student = cq.from(Student.class);
Predicate predicate = cb.disjunction();
for (SearchParams param : searchParams) {
ListJoin<Student, Test> tests = student.join(Student_.tests);
Predicate tempPredicate1 = cb.equal(tests.get(Test_.testName), param.getTestName());
Predicate tempPredicate2 = cb.ge(tests.get(Test_.score), param.getMinScore());
Predicate tempPredicate3 = cb.le(tests.get(Test_.score), param.getMaxScore());
Predicate tempPredicate = cb.and(tempPredicate1, tempPredicate2, tempPredicate3);
predicate = cb.or(predicate, tempPredicate);
}
cq.where(predicate);
TypedQuery<Student> tq = em.createQuery(cq);
return tq.getResultList();
I don't see how it would be possible without composing the query dynamically. Consider using the Criteria API to create it.
I would design the query like this:
select s from Student s where
exists (select t.id from Test t where t.student.id = s.id and ...)
or
exists (select t.id from Test t where t.student.id = s.id and ...)
or
exists (...)
As you see, there's a repeating pattern, and all these subqueries are similar an are combined into a disjunction.