How to select multiple rows without using JPQL? - jpa

I am using JPA, so to get one row of data, I could do:
Person p = this.em.find(Person.class, 123);
// Note: 'em' is the EntityManager.
But how can I get multiple rows? How can I get something like SQL's SELECT * FROM people WHERE age>18;?
I know how to use JPQL to get multiple rows (in this case, all rows):
TypedQuery<Person> q = em.createQuery("SELECT p FROM Person p", Person.class);
List<Person> results = q.getResultList();
But is there a way to do this without having to write the JPQL (i.e. SELECT p FROM Person p) at all?
Note: This is a learning exercise. I want to stick with Jakarta EE only (no Spring or any other API that is outside Jakarta EE).

CriteriaQuery is the way to go, however another "standard" approach is:
// imports
#Entity
#Table(name = "Person")
#NamedQuery(
name = "Person.queryAll",
query = "SELECT p FROM Person p")
public class Person implements Serializable {
...
}
The benefit of using the criteria is that errors can be detected earlier. Compile time vs Run time. However, a lot of readers/devs find JPQL easier to use and understand.
Using CriteriaQuery will look like below:
//skipping imports & config
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Person> q = cb.createQuery(Person.class);
Root<Person> c = q.from(Person.class);
q.select(c);

Example of CriteriaQuery API:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Person> criteria = cb.createQuery(Person.class);
Root<Person> root = criteria.from(Person.class);
criteria.select(root);
criteria.where(cb.equal(root.get("id"), 123));
List<Person> people = em.createQuery(criteria).getResultList();
official docs

You can use the Criteria API as an alternative to JPQL:
// Imports.
import java.util.List;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
// Example usage of the Criteria API to get a list of all Persons in the database.
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Person> cq = cb.createQuery(Person.class);
Root<Person> root = cq.from(Person.class);
cq.select(root);
TypedQuery<Person> q = em.createQuery(cq);
List<Person> allPeople = q.getResultList();
where em is a javax.persistence.EntityManager.
Note: Most people use JPQL instead of Criteria API because JPQL (e.g. SELECT p FROM Person p) is far more concise than the code above. But the Criteria API has an advantage over JPQL in terms of type safety.
Further reading
Tutorial from Oracle: Using the Criteria API to Create Queries
Examples on Wikibooks: Java Persistence/Criteria
Javadoc for javax.persistence.criteria

Related

JPA search query using Criteria Builder for multiple columns

I am trying to make a universal search for my entity with criteria builder where a given text is matched with all the columns in the entity.
String likeSearchText = "%" + searchText + "%";
List<Customer> searchedCustomers = null;
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery query = builder.createQuery(Customer.class);
Root <Customer> root = query.from(Customer.class);
ArrayList<Predicate> conditions = new ArrayList<>();
conditions.add(builder.like(root.<String>get("firstName"), likeSearchText));
conditions.add(builder.like(root.<String>get("lastName"), likeSearchText));
conditions.add(builder.like(root.<String>get("middleName"), likeSearchText));
conditions.add(builder.like(root.<String>get("companyName"), likeSearchText));
conditions.add(builder.like(root.<String>get("industry"), likeSearchText));
query.where(builder.or(conditions.toArray(new Predicate[conditions.size()])));
query.select(root);
searchedCustomers = entityManager.createQuery(query).getResultList();
return searchedCustomers;
When I run this method I always get an empty list. I tried changing the like to notLike and that works perfectly fine by giving me a list containing elements which are not like the given search text so I am really confused as to what's wrong with my like method.
Any kind of help would be appreciated!
I had similar problems when I made some testing and had entities with the same (simple)name in the classpath. So for example there were entities like:
org.example.one.Customer
org.example.two.Customer
If you do not have explicitly set different table names like:
package org.example.one;
#Entity("customer_one")
public class Customer { ...
and
package org.example.two;
#Entity("customer_two")
public class Customer { ...
hibernate might:
mix stuff in the same table in db
try to find field from wrong table when constructing the query
Also I thibk you do not need this:
query.select(root);

Multi-level subquery with JPA CriteriaBuilder

I have the following JPA entities
#Entity
#Table(name="application_user")
public class ApplicationUser {
#Id
#Column(name="user_id")
private String userid;
#Column(name="last_write_time")
private Instant lastWrite;
//other fields omitted
}
#Entity
#Table(name="demographic")
public class Demographic {
#Id
#Column(name="user_id")
private String userid;
//primary key is a foreign key link
#OneToOne
#PrimaryKeyJoinColumn(name="user_id", referencedColumnName="user_id")
private ApplicationUser user;
//other fields omitted
}
My goal is to retrieve all of the Demographics that contains users where the last write time is the max value in the column. I pretty much want to write the following SQL using the JPA CriteriaBUilder
select * from demographic where
userid in (
select userid from application_user where
last_write in (
select max(last_write) from application_user
)
)
I tried writing the following CriteriaBuilder Code to accomplish this goal and it compiles successfully. Note I am using the generated Metamodel classes.
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Demographic> c = cb.createQuery(Demographic.class);
Root<Demographic> root = c.from(Demographic.class);
root.fetch(Demographic_.user, JoinType.INNER);
Subquery<Instant> sqLatestUsers = c.subquery(Instant.class);
Root<ApplicationUser> subRootLatestUsers = sqLatestUsers.from(ApplicationUser.class);
sqLatestUsers.select(cb.greatest(subRootLatestUsers.<Instant>get(ApplicationUser_.LAST_WRITE)));
Predicate predicateLatestUsers = subRootLatestUsers.get(ApplicationUser_.LAST_WRITE).in(sqLatestUsers);
Subquery<ApplicationUser> sq = c.subquery(ApplicationUser.class);
Root<Demographic> subRoot = sq.from(Demographic.class);
sq.select(subRoot.<ApplicationUser>get(Demographic_.USER)).where(predicateLatestUsers);
Predicate containsUsers = subRoot.get(Demographic_.USER).in(sq);
c.select(root).where(containsUsers);
The code compiles and successfully deploys in Wildfly 14, but when I execute the code, the get the following error (with white space to improve readability):
Invalid path: 'generatedAlias2.user' : Invalid path: 'generatedAlias2.user'
...
Caused by: org.hibernate.hql.internal.ast.QuerySyntaxException: Invalid path: 'generatedAlias2.user' [
select generatedAlias0 from com.company.model.Demographic as generatedAlias0
inner join fetch generatedAlias0.user as generatedAlias1
where generatedAlias2.user in (
select generatedAlias2.user from com.company.model.Demographic as generatedAlias2 where generatedAlias3.lastWrite in (
select max(generatedAlias3.lastWrite) from com.company.model.StarfishUser as generatedAlias3
)
)
]
Is chaining subqueries (nested subqueries) allowed by the JPA spec? Did I find something that is syntactically correctly but not actually allowed?
I figure out how to get the subquery to work. First is my updated Utility method
public static <R, T> Subquery<T> getLatestSubelement(CriteriaBuilder cb, CriteriaQuery<R> c, Class<T> clazz, SingularAttribute<T, Instant> attribute) {
//Get latest timestamp
Subquery<Instant> sq = c.subquery(Instant.class);
Root<T> subRoot = sq.from(clazz);
sq.select(cb.greatest(subRoot.<Instant>get(attribute)));
//Get object with the latest timestamp
Subquery<T> sq2 = c.subquery(clazz);
Root<T> subRoot2 = sq2.from(clazz);
sq2.where(subRoot2.get(attribute).in(sq));
return sq2;
}
Here is the code that uses the utility method
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Demographic> c = cb.createQuery(Demographic.class);
Root<Demographic> root = c.from(Demographic.class);
joinType = JoinType.INNER;
//use fetch instead of join to prevent duplicates in Lists
root.fetch(Demographic_.user, joinType);
Subquery<ApplicationUser> sq = JpaUtil.getLatestSubelement(cb, c, ApplicationUser.class, ApplicationUser_.lastWrite);
c.where(root.get(Demographic_.user).in(sq));
TypedQuery<Demographic> q = em.createQuery(c);
Stream<Demographic> stream = q.getResultStream();

How to do a search with multiple ElementCollections

Given the following entity:
#Entity
#Table(name = "subscription")
public class Subscription implements Serializable {
private static final long serialVersionUID = 1L;
#ElementCollection
#CollectionTable(joinColumns= #JoinColumn(name="subscription"))
private Set<Code> mainCodes = new HashSet<>();
#ElementCollection
#CollectionTable(joinColumns= #JoinColumn(name="subscription"))
private Set<Code> otherCodes = new HashSet<>();
}
So a Subscription can have zero or more mainCodes or otherCodes it's interested in. I can get hold of the mainCode and otherCode of a certain object that passes by. The codes themselves are embeddables with only single String fields.
How do I create a JPA Query (or CriteriaBuilder) which searches in these collections with an "OR" mechanism?
So basically I'm looking for a query like this:
select s from subscription s where :myMainCode IN s.mainCodes OR :otherCode IN s.otherCodes
Is something like this doable with CriteriaBuilder or do I need to use a more explicit query? If so, what does the query look like?
EDIT: Tried this with CriteriaBuilder:
final CriteriaBuilder cb = this.entityManager.getCriteriaBuilder();
final CriteriaQuery<Subscription> cq = cb.createQuery(Subscription.class);
final Root<Subscription> root = cq.from(Subscription.class);
final Expression<Collection<Code>> mainCodes = root.get("mainCodes");
final Predicate containsMainCode = cb.isMember(obj.getClassCode(), mainCodes);
final Expression<Collection<Code>> otherCodes = root.get("otherCodes");
final Predicate containsOtherCode = cb.isMember(obj.getOtherCode(), otherCodes);
final Predicate searchPredicate = cb.or(containsMainCode, containsOtherCode);
cq.select(root).where(searchPredicate);
However, this creates an inner join of both collections involved, meaning that it will return no results if there is a row for mainCode, but not for otherCode in the database, it generates this query:
SELECT t0.ID
FROM Subscription_OTHERCODES t2, Subscription_MAINCODES t1, subscription t0
WHERE ((t1.CODESYSTEM = ?) AND (t1.CODE = ?)) OR ((t2.CODESYSTEM = ?) AND (t2.CODE = ?))) AND ((t1.subscription = t0.ID) AND (t2.subscription = t0.ID))
So even if it finds a matching mainCode, it fails if it doesn't have any otherCode.
It is other way around that in your example.
For example if the code has name property):
select s from Subscription s left join s.mainCodes m left join s.otherCodes o
where m.name IN :myMainCode or o.name IN :myOtherCode

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 criteria query fails after MapJoin

I have the following #Entities
#Entity
public class Configuration{
#OneToMany
protected Map<String, Component> components;
}
and
#Entity
public class Component{
protected String displayName;
}
I do not understand why this works, returning all Configurations
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Configuration> cq = cb.createQuery(Configuration.class);
Root<Configuration> pc = cq.from(Configuration.class);
cq.select(pc);
But if I do a MapJoin, even without setting any conditions, it does not return anything
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Configuration> cq = cb.createQuery(Configuration.class);
Root<Configuration> pc = cq.from(Configuration.class);
MapJoin<Configuration, String, Component> mapJoin = pc.join(Configuration_.components);
cq.select(pc);
What am I missing? I'm at a loss, I've been through the tutorials, but have not found the answers I need. Any help much appreciated.
Because the join type is inner by default, which means that for a configuration to be returned it has to at least have one component. If none of your configurations have a component, nothing is returned.
The first query is equivalent to
select configuration.* from configuration
And the second one is equivalent to
select configuration.* from configuration
inner join component on component.id = configuration.id