I have two classes, Account and Admin, with many to many mapping.
The Admin class has a collection of Account class and vise versa.
I want to write a query, that given the account id, will return all the account admins.
Here is the relevant fields of the Account class:
#Entity
public class Account {
#Id
public Long id;
#ManyToMany(mappedBy = "account", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
public List<Admin> users = new ArrayList<>();
}
I have tried a regular query for Admin.class with multiselect as each account has a collection of admins, but trying to get a TypedQuery<Admin> out of my CriteriaQuery<Admin> I got an IllegalArgumentException with the message "org.hibernate.hql.internal.ast.QuerySyntaxException: Unable to locate appropriate constructor on class [models.Admin]. Expected arguments are: java.util.Collection [select new models.Admin(generatedAlias0.users) from models.Account as generatedAlias0 where generatedAlias0.id=1L]" (1L here probably since I called this function with 1 as accountId), caused by QuerySyntaxException with the message "Unable to locate appropriate constructor on class [models.Admin]. Expected arguments are: java.util.Collection".
Code:
private static List<Admin> readAccountAdmins(Long accountId) {
CriteriaBuilder cb = JPA.em().getCriteriaBuilder();
CriteriaQuery<Admin> cq = cb.createQuery(Admin.class);
Root<Account> root = cq.from(Account.class);
Predicate idPredicate = cb.equal(root.get(Account_.id), accountId);
cq.multiselect(root.get(Account_.users)).where(idPredicate);
TypedQuery<Admin> typedQuery = JPA.em().createQuery(cq); // exception thrown here
return typedQuery.getResultList();
}
After that I tried running a TypedQuery<List<Admin>>, as I am trying to read a list. This is the first iteration of trying a query of list:
private static List<Admin> readAccountAdmins(Long accountId) {
CriteriaBuilder cb = JPA.em().getCriteriaBuilder();
CriteriaQuery<List<Admin>> cq = cb.createQuery((Class<List<Admin>>)(Class<?>)(Collection.class));
Root<Account> root = cq.from(Account.class);
Predicate idPredicate = cb.equal(root.get(Account_.id), accountId);
cq.select(root.get(Account_.users)).where(idPredicate);
TypedQuery<List<Admin>> typedQuery = JPA.em().createQuery(cq);
return typedQuery.getSingleResult(); // exception thrown here
}
I used getSingleResult as getResultList caused a compilation error, saying the actual return value is List<List<Admin>>> and doesn't match the signature.
This method threw a different exception, a NonUniqueResultException with the message: "result returns more than one elements".
While debugging, I tried to evaluate the expression typedQuery.getResultList() and saw that it actually returns List<Admin> and not List<List<Admin>>, so I got to my final iteration of this function:
private static List<Admin> readAccountAdmins(Long accountId) {
CriteriaBuilder cb = JPA.em().getCriteriaBuilder();
CriteriaQuery<List<Admin>> cq = cb.createQuery((Class<List<Admin>>)(Class<?>)(Collection.class));
Root<Account> root = cq.from(Account.class);
Predicate idPredicate = cb.equal(root.get(Account_.id), accountId);
cq.select(root.get(Account_.users)).where(idPredicate);
TypedQuery<List<Admin>> typedQuery = JPA.em().createQuery(cq);
return (List) typedQuery.getResultList();
}
Now, this function works, but my question is why?
Why did the compiler decide that getResultList returns a different value than the actual return value?
Maybe it makes sense when you take a closer look at your database. A TypeQuery returns entities, so basically rows from tables. List<Admin> is a collection of Entities, so eventhough your Account has a List<Admin> as a field, the Query will still return List<Admin> entities, not List<List<Admin>> as List<Admin> is not an entity.
I hope that makes sense.
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();
i am trying to reuse a dynamic query as a named query as described here:
https://wiki.eclipse.org/EclipseLink/Release/2.5/JPA21#Add_Named_Query
the goal is to build the criteria-query only once and then reuse it as a namedquery if parameter did not change.
public static List<User>getUserByParameter(ParameterMap parameter){
EntityManager em = getEntityManager();
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<User> criteriaQuery = builder.createQuery(User.class);
Root<User> user = criteriaQuery.from(User.class);
List<Predicate> predicates = new ArrayList<Predicate>();
//...build up the query depending on parameter
if (null != parameter.getStatus()){
predicates.add(builder.equal(user.<Integer>get("status"), parameter.getStatus()));
}
//etc.
criteriaQuery.select(user).where(predicates.toArray(new Predicate[]{}));
Query query = em.createQuery(criteriaQuery);
//now register this query as a namedQuery
em.getEntityManagerFactory().addNamedQuery("userByParameter", query);
return query.getResultList();
}
i thought about something like:
public static List<User>getUserByParameter(ParameterMap parameter){
Query userByParameter = em.createNamedQuery("userByParameter");
if (null != userByParameter){
return userByParameter.getResultList();
}else {
//build the dynamic query as above
}
}
this results in a nullpointer as the namedQuery doesn't exist the first time.
how can i reuse the query in the same method or in other words, how can i check in a clean way (without using try-catch) if a namedquery exists?
I'm not sure I understand the problem you are looking to solve. The getUserByParameter method is something that should be built on the EntityManagerFactory, when it is first initialized or obtained. Feel free to add properties to your factory if you wish to keep track of what you have added already, but these should be done only once, upfront during initialization.
What is confusing is that you are expecting the query results to be reused - named queries are designed to help reduce the cost of parsing and preparing queries. EclipseLink has a query cache feature that can return the results for you if the same parameters are used, without you needing to cache the query, its parameters and the results yourself.
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;
}
}
I am trying to use predicateBuilder with next expression definition but I always got the message
"LINQ to Entities does not recognize the method 'puedeConsultar' method, and this method cannot be translated into a store expression."
I think i understand more less this problem, but i don´t know how to solve it.
private static readonly IDictionary<int, List<string>> permisosAccesoSolicitudesEstado = new Dictionary<int, List<string>>(){{0, new List<string>(){"A"}}, {1, new List<string>(){"B"}}};
private static bool esPermisoConcedido(List<string> usuariosPermitidos, string erfilUsuario)
{
return usuariosPermitidos.Any(x => x.Equals(perfilUsuario) || perfilUsuario.StartsWith(x + "|") || perfilUsuario.EndsWith("|" + x));
}
public static bool puedeConsultar(int estadoActual, string perfilUsuario)
{
List<string> usuariosPermitidos = permisosAccesoSolicitudesEstado[estadoActual];
return esPermisoConcedido(usuariosPermitidos, perfilUsuario);
}
public static bool puedeConsultar(string estadoActual, string tipoUsuario)
{
return puedeConsultar(Convert.ToInt32(estadoActual), tipoUsuario);
}
public Expression<Func<Solicitud, Boolean>> predicadoEstadoCorrectoSolicitud(string perfil)
{
return x=> EstadosSolicitud.puedeConsultar(x.estado, perfil);
}
//Instantiated by reflection, this works fine
MethodInfo method = .....
Expression<Func<T, bool>> resultado = ConstructorPredicados.True<T>();
resultado = ConstructorPredicados.And(resultado, method);
objectSet.Where(resultado).ToList();
Note:
ConstructorPredicados is based in Monty´s Gush "A universal PredicateBuilder" on http://petemontgomery.wordpress.com/2011/02/10/a-universal-predicatebuilder/
Thanks in advance
You cannot do that. Your puedeConsultar is .NET function. You cannot execute .NET functions in Linq-to-entities query. When you use method in Linq-to-entities you can use only methods which has direct mapping to SQL. It means that method in the query is only placeholder which is translated to execution of some SQL function. There is set of predefined method mappings called cannonical functions and you can map your own SQL function when using EDMX but in your case you will most probably have to first load data to application by using ToList and after that execute predicadoEstadoCorrectoSolicitud on materialized result.