Specifying a LIMIT clause in a JPA2 repository #Query statement - spring-data-jpa

I have the following MySQL statement:
"SELECT * FROM $this->tableName WHERE news_publication_id = '" + newsPublicationId + "' AND list_order > '" + listOrder +"' ORDER BY list_order LIMIT 1"
I started on the data handling path by first doing an Hibernate DAO layer.
It had the following criteria based method:
#Override
public NewsHeading findNextWithListOrder(NewsPublication newsPublication, int listOrder) {
Criteria criteria = getSession().createCriteria(getPersistentClass());
criteria.add(Restrictions.eq("newsPublication", newsPublication));
criteria.add(Restrictions.gt("listOrder", listOrder)).addOrder(Order.asc("listOrder")).setMaxResults(1);
return (NewsHeading) criteria.uniqueResult();
}
And it works just fine.
Then I tried doing a JPA2 repository layer.
It now has the following JPQL based method:
#Query("SELECT nh FROM NewsHeading nh WHERE nh.newsPublication = :newsPublication AND nh.listOrder > :listOrder ORDER BY nh.listOrder ASC LIMIT 1")
public NewsHeading findByNextListOrder(#Param("newsPublication") NewsPublication newsPublication, #Param("listOrder") int listOrder);
But it doesn't work. Because the LIMIT clause is not valid.
So I wonder what to do.
Can I still use the criteria based method of above in this JPA2 repository layer ?
I would like to avoid trusting the client to pass the limit value and prefer to hard code that value (of 1) into my layer.
Also, that statement returns only one object and so there is no pagination needed.
Any tip would be most welcomed.
Kind Regards,
Stephane Eybert

You can probably this with a PageRequest. http://docs.spring.io/spring-data/jpa/docs/1.6.2.RELEASE/reference/html/repositories.html#repositories.core-concepts
Hibernate wants you to do setMaxResults() on the Query object, but JPA doesn't interface with that directly.
Other option would be to use a #NamedNativeQuery

Related

Spring JPA repository casting error when using JPQL

I have a PagingAndSorting JPA repository declared. I am using the #Query annotation.
I am getting an exception when I call the get() method on an Optional object from the findById(id) method of the repository.
The weird thing is it only happens when I use JPQL.
The code works if my query is native:
#Override
public BatchDto findById(String id) {
Optional<Batch> findResult = this.batchRepository.findById(id);
if (!findResult.isPresent()) return null;
Batch entity = findResult.get(); **<-------- Cast Exception Here**
BatchDto dto = this.mapper.toDto(entity, BatchDto.class);
List<BatchTransaction> transactions = entity.getTransactions();
dto.setTransactionDtos(mapper.toListDto(transactions, TransactionDto.class));
return dto;
}
Inspecting the findResult object with a breakpoint - I can see:
Optional[net.domain.data.batch#4b8bb6f]
when I have nativeQuery = true in the #Query annotation.
#Query(value = Sql.FindBatchById, nativeQuery = true)
Here is the query being used:
SELECT DISTINCT(B.batchNumber), COUNT(B.batchNumber) as TransactionCount FROM BATCH B WHERE B.batchReferenceNumber = :id GROUP BY B.batchNumber
However if I change it to JPQL and remove the nativeQuery=true attribute - the findResult is
Optional[[Ljava.lang.Object;#76e04327].
and I get a ClassCastException:
java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to net.domain.data.batch
So bottom line - this works when specify nativeQuery=true and fails when I try to use JPQL.
I would prefer not to specify nativeQuery as we will eventually port this db to Oracle.
First of all the query shown below doesn't return a single Batch instance. Since there are distinct and count aggregate functions, the query will return a List of aggregates.
To be able to read that statistics you can add appropriate method into the batchRepository. Something like this:
#Query("SELECT DISTINCT(B.batchNumber) as dist, COUNT(B.batchNumber) as cnt FROM BATCH B GROUP BY B.batchNumber")
List<Map<Long, Long>> findStatistics();
and then iterate through the list.
UPD
If the id parameter exactly guarantee that will return a single record, you can change a return type to a Map
#Query("SELECT DISTINCT(B.batchNumber) as dist, COUNT(B.batchNumber) as cnt FROM BATCH B WHERE B.batchReferenceNumber = :id GROUP BY B.batchNumber")
Map<Long, Long> findStatisticsById(#Param("id") Long id);

JPA eclipselink global batch fetch?

when joining I get one select per row. Solution is batch fetch but I dont want that annotation everywhere...
http://eclipse.org/eclipselink/documentation/2.4/jpa/extensions/a_batchfetch.htm
Why do I even need this? One select per row is awful... How can I set this globally? Cheers
Maybe not the ideal solution, but you may try to use JPA hints along with Java generics:
public <T> TypedQuery<T>
createBatchQuery(String ql, Class<T> clazz, String type, String size, String relation) {
return em.createQuery(jpql, clazz)
.setHint(QueryHints.BATCH_TYPE, type)
.setHint(QueryHints.BATCH_SIZE, size)
.setHint(QueryHints.BATCH, relation);
}
The above query may then be used globally and extended with concrete query implementations according to you needs, i.e.
String jpql = "SELECT c FROM Country c WHERE c.name = :name"; // or #NamedQuery
TypedQuery<Country> q = createBatchQuery(jpql, Country.class, "JOIN", "64", "c.cities");
q.setParameter("name", "Australia");
Country c = q.getSingleResult();
Articles on this topic:
Batch fetching - optimizing object graph loading
EclipseLink/Examples/JPA/QueryOptimization

Finding Maximum query result using spring-data-jpa #Query annotation

Is is possible to limit query results using .setMaxResults(1).getResultList() and #Query - something like:
#Query("SELECT l FROM LocationLog l WHERE l.visit = :visit ORDER BY l.eventTime").setMaxResults(1).getResultList()
public LocationLog findLast(#Param("visit") Visit visit);
This code is not correct as the setMaxResults and so on is outside the #Query?l
You cannot set pagination options in #Query annotation, but it can be done in other way. You can change your method declaration to
public LocationLog find(#Param("visit") Visit visit, Pageable pageable);
Then you will be able to pass additional argument to query invocation:
Pageable firstFive = new PageRequest(0, 5);
LocationLog locLog = locationLogDao.find(visit, firstFive);
There's also option to know current context of the query results you can use Page interface changing method return type to Page<LocationLog>. You can find all necessary informations here.

How to use MySQL's full text search from JPA

I want to use MySQL's full text search features using JPA, without having to use a native query.
I am using EclipseLink, which has a function to support native SQL commands: FUNC. However, the help examples only show this being use with simple MySQL functions. My best effort attempt to get it to work with MATCH & AGAINST is as follows:
#PersistenceContext(name="test")
EntityManager em;
Query query = em.createQuery("SELECT person FROM People person WHERE FUNC('MATCH', person.name) FUNC('AGAINST', :searchTerm)");
...
query.getResultList();
Which gives the following exception:
Caused by: NoViableAltException(32#[()* loopback of 822:9: (m= MULTIPLY right= arithmeticFactor | d= DIVIDE right= arithmeticFactor )*])
at org.eclipse.persistence.internal.libraries.antlr.runtime.DFA.noViableAlt(DFA.java:159)
at org.eclipse.persistence.internal.libraries.antlr.runtime.DFA.predict(DFA.java:116)
at org.eclipse.persistence.internal.jpa.parsing.jpql.antlr.JPQLParser.arithmeticTerm(JPQLParser.java:4557)
... 120 more
I am open to alternatives other that using the FUNC method.
I am using EJB 3 and EclipseLink 2.3.1.
An improved answer of #Markus Barthlen which works for Hibernate.
Create custom dialect
public class MySQLDialectCustom extends MySQL5Dialect {
public MySQLDialect() {
super();
registerFunction("match", new SQLFunctionTemplate(StandardBasicTypes.DOUBLE,
"match(?1) against (?2 in boolean mode)"));
}
}
and register it by setting hibernate.dialect property.
Use it
in JPQL:
Query query = entityManager
.createQuery("select an from Animal an " +
"where an.type = :animalTypeNo " +
"and match(an.name, :animalName) > 0", Animal.class)
.setParameter("animalType", "Mammal")
.setParameter("animalName", "Tiger");
List<Animal> result = query.getResultList();
return result;
or with Criteria API:
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Animal> criteriaQuery = criteriaBuilder.createQuery(Animal.class);
Root<Animal> root = criteriaQuery.from(Animal.class);
List<Predicate> predicates = new ArrayList<>();
Expression<Double> match = criteriaBuilder.function("match", Double.class, root.get("name"),
criteriaBuilder.parameter(String.class, "animalName"));
predicates.add(criteriaBuilder.equal(root.get("animalType"), "Mammal"));
predicates.add(criteriaBuilder.greaterThan(match, 0.));
criteriaQuery.where(predicates.toArray(new Predicate[]{}));
TypedQuery<Animal> query = entityManager.createQuery(criteriaQuery);
List<Animal> result = query.setParameter("animalName", "Tiger").getResultList();
return result;
Some more details in this blog post: http://pavelmakhov.com/2016/09/jpa-custom-function
FUNC only works with normal printed functions,
i.e.
MATCH(arg1, arg2)
since MATCH arg1 AGAINST arg2 is not printed the way a function is normally printed, FUNC cannot be used to call it.
EclipseLink ExpressionOperators do support printing functions like this, so you could define your own ExpressionOperator, but ExpressionOperators are only supported through EclipseLink Expression queries currently, not through JPQL. You could log an enhancement to have operator support in JPQL.
You could also use a native SQL query.
Just to complete the answer: I had the same problem, but using the criteria builder. This is how you can get around the limitations in the standart implementation, if you are using EclipseLink:
Cast JPA expression to EclipseLink expression
Use the sql method
If you match against a compound index, create it using the function method
Example:
JpaCriteriaBuilder cb = (JpaCriteriaBuilder) cb;
List<String> args = new ArrayList();
args.add("Keyword");
Expression<Boolean> expr = cb.fromExpression (
cb.toExpression(
cb.function("", String.class,
table.get(Table_.text1), table.get(Table_.text2))
)
.sql("MATCH ? AGAINST (?)", args)
);
query.where(expr);
If you need to cast the expression to a predicate use the following:
query.where( cb.gt(expr, 0));
What about new SQL operator in EclipseLink 4.0? I think it can help you to do fulltext search from JPQL. But you have to upgrade to EclipseLink 4.0.
http://wiki.eclipse.org/EclipseLink/UserGuide/JPA/Basic_JPA_Development/Querying/Support_for_Native_Database_Functions#SQL
Edit:
Sorry for late update.
Verified correct use of EclispeLink 2.4.0 "SQL" operator with MySQL fulltext search is
SELECT person FROM People person WHERE SQL('MATCH(name) AGAINST( ? )', :searchTerm)"
where name is column on which Fulltext index is defined. :searchTerm is string you use for searching.
Works without problems.
To elaborate on the answer of James:
It seems like I had luck extending the mysql dialect using
registerFunction("match", new SQLFunctionTemplate(DoubleType.INSTANCE, "match(?1) against (?2 in boolean mode)"));
and invoking the function via the following jpql fragment
match(" + binaryDataColumn + ",'" + StringUtils.join(words, " ") + "') > 0
I had to guess the return type, but this should get you started.
FInally work
if you set your table colums wit index full search
#NamedNativeQuery(name = "searchclient",
query = "SELECT * FROM client WHERE MATCH(clientFullName, lastname, secondname, firstphone,"
+ " secondphone, workphone, otherphone, otherphone1,"
+ " otherphone2, detailsFromClient, email, company,"
+ " address, contractType, paymantCondition) AGAINST(?)",
List list = em.createNamedQuery("searchclient").setParameter(1, searchKey).getResultList();
The simplest variant is to use NativeQuery
Example of use it with mapping to JPA entity (FiasAddress):
public class FiasServiceBean implements FiasService {
#PersistenceContext(unitName = "fias")
EntityManager entityManager;
#Override
public Collection<FiasAddress> search(String name, int limit, int aolevel) {
Query query = entityManager.createNativeQuery(
"SELECT fa.* FROM fias.addressobject fa" +
" WHERE MATCH(FORMALNAME) AGAINST (:name IN NATURAL LANGUAGE MODE)" +
" AND AOLEVEL = :AOLEVEL" +
" LIMIT :limit",
FiasAddress.class
);
query.setParameter("name", name);
query.setParameter("limit", limit);
query.setParameter("AOLEVEL", aolevel);
Iterator iterator = query.getResultList().iterator();
ArrayList<FiasAddress> result = new ArrayList<>();
while (iterator.hasNext()) {
result.add((FiasAddress) iterator.next());
}
return result;
}
}

jpa call readonly composite table but getting "Exception Description: Missing descriptor for [CollectorInfo]"

In a Spring 3 app a controller is calling a JpaCollectorManager with calls a JpaCollectorInfoDao to get a list which is defined by a nativequery. The query calls 2 seperate tables which uses sql and jpql because I need to use a postgresql feature not implemented in jpql. When the controller tries to file the list I get the following error message:
Exception [EclipseLink-6007] (Eclipse Persistence Services - 2.1.2.v20101206-r8635): org.eclipse.persistence.exceptions.QueryException
Exception Description: Missing descriptor for [CollectorInfo].
Query: ReadAllQuery(referenceClass=CollectorInfo sql="select distinct ON ( col.collector_id,pst.process_source_type ) col.*,pst.process_source_timestamp,pst.process_source_type from perform_schema.collector col join perform_schema.process_set pst on pst.collector_id = col.collector_id order by col.collector_id, pst.process_source_type,pst.process_source_timestamp desc ")
The controller Java has the following call:
List<CollectorInfo> ps = this.collectorInfoManager.getLatestCollectorInfo();
The JpaCollectorInfoManager.java has this:
public List<CollectorInfo> getLatestCollectorInfo()
{
return collectorInfoDao.getLatestCollectorInfo();
}
The JpaCollectorInfoDao.java:
#Override
#Transactional
public List<CollectorInfo> getLatestCollectorInfo() {
Query query = entityManager.createNativeQuery( ( "select distinct ON ( col.collector_id," +
"pst.process_source_type ) " +
"col.*," +
"pst.process_source_timestamp," +
"pst.process_source_type " +
"from perform_schema.collector col " +
"join perform_schema.process_set pst " +
"on pst.collector_id = col.collector_id " +
"order by col.collector_id, " +
"pst.process_source_type," +
"pst.process_source_timestamp desc " ),
CollectorInfo.class );
return ( (List<CollectorInfo>) query.getResultList() );
}
The CollectorInfo class does not have an #Entity defined. If I set the #Entity defined then it tells me that the Table cannot be resolved (which is correct since the there is no actual table). I have tried all sorts of permutations and cannot seem to make this needle thread.
Not sure what you are trying to do exactly?
You need to map the class as an Entity in order to be able to select instances of it.
Either, do not include the class, the native SQL query will an Object[] of the data, which you can map in your own code to your class.
Or map it as an Entity excepting the data that you are returning. The #Table will not be relevant as you are mapping the object to the query results. This should not cause any errors though, unless you are auto creating table or using integrity checker.
Or map the objects to the table correctly. Then use a fetch join, or batch fetch to optimize your retrieval if required.