JPA named query gets turned into incorrect SQL - jpa

I am building a JSF application and want to list some entities on a page. I three entities: Panel, PanelDefinition and Protein. One page is a kind of details page for one Protein entity. One thing I want to do on that details page is to list all Panel entities for that Protein.
To do this I use a method in my PanelController class (JSF managed session scoped bean), which use my PanelService (stateless EJB) to call the named query Panel.byProtein. When I do this I get the error:
Internal Exception: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '.panel_id) AND (t0.target = 5)) AND (t0.panel = t1.panel_id))' at line 1
Error Code: 1064
Call: SELECT t1.panel_id, t1.comment, t1.insert_date, t1.name, t1.panel_version, t1.status, t1.inserted_by, t1.previous_version FROM panel_definition t0, panel t1 WHERE (((t0.panel = t1.panel_id.t1.panel_id) AND (t0.target = ?)) AND (t0.panel = t1.panel_id))
bind => [1 parameter bound]
Query: ReadAllQuery(name="Panel.byProtein" referenceClass=Panel sql="SELECT t1.panel_id, t1.comment, t1.insert_date, t1.name, t1.panel_version, t1.status, t1.inserted_by, t1.previous_version FROM panel_definition t0, panel t1 WHERE (((t0.panel = t1.panel_id.t1.panel_id) AND (t0.target = ?)) AND (t0.panel = t1.panel_id))")
at org.eclipse.persistence.exceptions.DatabaseException.sqlException(DatabaseException.java:340)
at org.eclipse.persistence.internal.databaseaccess.DatabaseAccessor.basicExecuteCall(DatabaseAccessor.java:679)
at org.eclipse.persistence.internal.databaseaccess.DatabaseAccessor.executeCall(DatabaseAccessor.java:558)
at org.eclipse.persistence.internal.sessions.AbstractSession.basicExecuteCall(AbstractSession.java:1995)
at org.eclipse.persistence.sessions.server.ServerSession.executeCall(ServerSession.java:570)
at org.eclipse.persistence.internal.queries.DatasourceCallQueryMechanism.executeCall(DatasourceCallQueryMechanism.java:242)
at org.eclipse.persistence.internal.queries.DatasourceCallQueryMechanism.executeCall(DatasourceCallQueryMechanism.java:228)
at org.eclipse.persistence.internal.queries.DatasourceCallQueryMechanism.executeSelectCall(DatasourceCallQueryMechanism.java:299)
at org.eclipse.persistence.internal.queries.DatasourceCallQueryMechanism.selectAllRows(DatasourceCallQueryMechanism.java:694)
at org.eclipse.persistence.internal.queries.ExpressionQueryMechanism.selectAllRowsFromTable(ExpressionQueryMechanism.java:2714)
at org.eclipse.persistence.internal.queries.ExpressionQueryMechanism.selectAllRows(ExpressionQueryMechanism.java:2667)
at org.eclipse.persistence.queries.ReadAllQuery.executeObjectLevelReadQuery(ReadAllQuery.java:477)
at org.eclipse.persistence.queries.ObjectLevelReadQuery.executeDatabaseQuery(ObjectLevelReadQuery.java:1155)
at org.eclipse.persistence.queries.DatabaseQuery.execute(DatabaseQuery.java:899)
at org.eclipse.persistence.queries.ObjectLevelReadQuery.execute(ObjectLevelReadQuery.java:1114)
at org.eclipse.persistence.queries.ReadAllQuery.execute(ReadAllQuery.java:402)
at org.eclipse.persistence.queries.ObjectLevelReadQuery.executeInUnitOfWork(ObjectLevelReadQuery.java:1202)
at org.eclipse.persistence.internal.sessions.UnitOfWorkImpl.internalExecuteQuery(UnitOfWorkImpl.java:2894)
at org.eclipse.persistence.internal.sessions.AbstractSession.executeQuery(AbstractSession.java:1797)
at org.eclipse.persistence.internal.sessions.AbstractSession.executeQuery(AbstractSession.java:1779)
at org.eclipse.persistence.internal.sessions.AbstractSession.executeQuery(AbstractSession.java:1744)
at org.eclipse.persistence.internal.jpa.QueryImpl.executeReadQuery(QueryImpl.java:258)
And t0.panel = t1.panel_id.t1.panel_id does indeed not look proper. How and why do I get incorrect sql?
Here are a simplified version of my entity classes:
#Entity
#NamedQueries({
#NamedQuery(name = "Panel.findAll", query = "SELECT p FROM Panel p"),
#NamedQuery(name = "Panel.byProtein", query = "SELECT p FROM Panel p JOIN p.panelDefinition pd WHERE pd.pdId = p.panId AND pd.protein.ID = :pid"),
})
public Class Panel {
#Id
private Integer panId;
#OneToMany(mappedBy = "panel")
private List<PanelDefinition> panelDefinition;
//More attributes + getters and setter
}
#Entity
public Class PanelDefinition {
#Id
private Integer pdId;
#JoinColumn(name = "protein", referencedColumnName = "ID")
#ManyToOne
private Protein protein;
#JoinColumn(name = "panel", referencedColumnName = "panId")
#ManyToOne
private Panel panel;
//More attributes + getters and setter
}
#Entity
public Class Protein {
#Id
private Integer ID;
//More attributes + getters and setter
}

Apparently I needed another JOIN in the query. This change fixed it:
Original:
SELECT p FROM Panel p JOIN p.panelDefinition pd
WHERE pd.pdId = p.panId AND pd.protein.ID = :pid
Updated query:
SELECT p FROM Panel p JOIN p.panelDefinition pd
JOIN pd.protein t
WHERE p.panId = pd.panel.panId AND t.ID = :id

Related

How to Use Values from an #ElementCollection Map in JPQL Where Clause

I have an entity with a Map<String,String> field that is an #ElementCollection. I'd like to select entities whose values for this map field match some pattern. However, I'm getting an error at runtime, in this case, with an H2 database.
Here's the entity:
#Entity
#Table(name = "config")
public class Config {
private String id;
private String name;
#ElementCollection
#CollectionTable(name = "setting",
joinColumns = #JoinColumn(name = "config_id", referencedColumnName = "id"))
#MapKeyColumn(name = "name", columnDefinition = "varchar(1024)")
#Column(name = "value", columnDefinition = "TEXT")
Map<String, String> settings;
}
Then, in my JPA repository, I have a method that fetches configs which have a setting that matches a given prefix:
#Query("select config.id, config.name from Config config " +
"join config.settings setting " +
"where value(setting) like concat(:prefix, '%')")
List<List<String>> findConfigBySettingPrefix(#Param("prefix") String prefix);
This produces an error from H2:
Caused by: org.h2.jdbc.JdbcSQLDataException: Scalar subquery contains more than one row;
The SQL generated by the JPQL shows that the value() function is expanded into a subselect:
select config."id", config."name" from "config" config
inner join "setting" setting on config."id"=setting."config_id"
where (select setting."value" from "setting" setting where config."id"=setting."config_id") like (?||'%')
Thus, when there are multiple matching settings (as expected), this produces an error because they are being passed into an operator that expects one value. When I wrote the JPQL, I was expecting it would generate something more like this:
select config."id", config."name" from "config" config
inner join "setting" setting on config."id"=setting."config_id"
where setting."value" like (?||'%')
It would seem I'm not able to use value() as a where condition, or I'm just not doing it right. Is there a better way to do this?
I've tried similar variants that all compile at bootup, but they produce the same error at runtime:
#Query("select config.id, config.name from Config config " +
"join config.settings setting on value(setting) like concat(:prefix, '%')")
#Query("select config.id, config.name from Config config " +
"where value(config.settings) like concat(:prefix, '%')")
This doesn't precisely answer the question, but it is an alternative to using JPQL. Using Spring Data query semantics in the repository method name produces the results I'm looking for:
public interface ConfigRepository extends JpaRepository<Config, String> {
List<Config> findAllBySettingsStartingWith(String prefix);
}

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 map ALL names directly by JPA?

Given a ZIP-code-like hierarchical code/name schema.
For example:
code = 101010
Code:
100000 level 1 code (10....)
101000 level 2 code (..10..)
101010 level 3 code (....10)
Name (short name)
100000 - A
101000 - a
101010 - i
Name (FullQualifiedName)
100000 - A
101000 - A->a
101010 - A-a->i
EDIT
I wanna following code (JPA pseudo code), but CANNOT.
#Entity
public class CodeName{
// ....
String code; // 100101 levels = {100000, 100100, 100101}
String name; //
#HowToMapDirectedToNameOfCode('100000') // #SecondTable ?
String name1;
#HowToMapDirectedToNameOfCode('100100')
String name2;
#HowToMapDirectedToNameOfCode('100101')
String name3;
String getFullQualifiedName(){
return String.format("%s->%s->%s", name1, name2, name3);
}
// getter and setter
}
But it's relatively easier in native SQL:
SELECT (select p1.name from codename p1 where p1.code= concat( substring(p.code,1,2), "0000") ) province,
(select p2.name from codename p2 where p2.code= concat( substring(p.code,1,4), "00") ) city,
(select p3.name from codename p3 where p3.code=p.code) area
FROM codename p WHERE p.code = '100101';
So, I implements it as following snippet.
#Entity
public class CodeName{
// ....
String code; // 100000, 101000, 100101
String name; // province, city , area
#Transient
String name1; // mapping directly?
#Transient
String name2; // mapping directly?
#Transient
String name3; // mapping directly?
String getFullQualifiedName(){
return String.format("%s->%s->%s", name1, name2, name3);
}
// getter and setter
}
public interface CodeNameRepository extends CrudRepository<CodeName, Long>, CodeNameRepositoryCustom {
#Query(" FROM CodeName p " +
" WHERE p.code = CONCAT(SUBSTRING(?1, 1, 2), '0000') " +
" OR p.code = CONCAT(SUBSTRING(?1, 1, 4), '00') " +
" OR p.code = ?1")
List<CodeName> findAllLevelsByCode(String code);
}
#Component
public class CodeNameRepositoryImpl implements CodeNameRepositoryCustom {
#Autowired
private CodeNameRepository codeNameRepository ;
#Override
public CodeName CodeNamefindFullQualifiedNameByCode(String code) {
List<CodeName> codeNames= codeNameRepository .findAllLevelsByCode(code);
CodeName codeName;
// extra name1, name2, name3 from list,
// fill code, name, name1, name2, name3 to codeName and
return codeName;
}
}
But it have SO MANY limitations.
Most likely, I need getFullQualifiedName(), to display it on UI, but every time I must have an extra call to populate all names.
For each entity has CodeName as its children, no matter how deep the codeName is at, I MUST expand to the codeName and reload it with FQN.
Can we mapping all #Transient names directly by JPA?
You could technically model your code repository entity as follows:
public class CodeName {
#Id
#GeneratedValue(GenerationStrategy.AUTO)
#Column
private Long id;
#ManyToOne
private CodeName parent;
#OneToMany(mappedBy = "parent")
private List<CodeName> children;
#Column
private String name;
#Transient
public String getFullyQualifiedName() {
List<String> names = new ArrayList<>();
names.add(name);
CodeName theParent = parent;
while(theParent != null) {
names.add(theParent.getName());
theParent = theParent.parent;
}
Collections.reverse(names);
return StringUtils.join(names, "->");
}
}
Because the parent relationships will be fetched EAGERLY because they mapped as #ManyToOne, you can basically start at any child CodeName entity and traverse up it's parent/child relationship to the root. This basically allows the getFullyQualifiedName method to build the name for you at runtime.
If performance becomes a problem doing this, you can always datamine the names ahead of time in your entity as you described by adding a #Column private String fullyQualifiedName and make sure that field is inserted when you create your codes. Then the transient method I added to my the entity can be dropped since you're caching the names at data insertion.
It is possible to write a JPQL, which is equivalent to your SQL query. The only tricky part is to rewrite nested selects into cross joins, because nested selects are not supported by JPA and you need to join unrelated entities. On the other hand, functions CONCAT and SUBSTRING are supported by JPQL in the same way as in SQL. See the following JPQL query, which should give you the results as the SQL query in the question:
SELECT p1.name // province
, p2.name // city
, p.name // area
FROM CodeName p, CodeName p1, CodeName p2
WHERE p.code = '100101'
AND p1.code = concat( substring(p.code,1,2), "0000")
AND p2.code= concat( substring(p.code,1,4), "00")
The above query will give you 3 values in one row, which cannot be mapped into a single entity. The result of the query will therefore be a list of Object[] arrays. You may also add the original entity into the select clause: SELECT p1.name, p2.name, p.name, p FROM .... This way, you may later process the list of results and assign first three values into the transient fields of the entity:
Object[] rows = query.getResultList();
for (Object row : rows) {
CodeName c = (CodeName)row[3];
c.setName1((String)row[0]);
c.setName2((String)row[1]);
c.setName3((String)row[2]);
}

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

Querying a collection of embeddable elements using eclipselink

I have the following JPA (2.0.2) entities:
Employee
#Entity
#Table(name = "T_EMPLOYEE")
public class Employee {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#ElementCollection
#CollectionTable(name = "T_COMPETENCE")
private Set<Competence> competences;
// Getter and setters
}
and Competence
#Embeddable
public class Competence {
#JoinColumn(nullable = false)
#ManyToOne
private Skill skill;
// Getter and setters
}
(The skill entity shouldn't be important and is therefore omitted, so are various additional attributes.)
I'm using EclipseLink (2.2.0) to query my entities with a DAO. Now I'd like to use the following query:
public List<Employee> findBySkill(Skill skill) {
TypedQuery<Employee> query = getCurrentEntityManager().createQuery(
"SELECT e FROM Employee e JOIN e.competences c WHERE c.skill = :skill",
Employee.class);
query.setParameter("skill", skill);
return query.getResultList();
}
But it keeps throwing the following exception:
Caused by: Exception [EclipseLink-8030] (Eclipse Persistence Services - 2.2.0.v20110202-r8913): org.eclipse.persistence.exceptions.JPQLException
Exception Description: Error compiling the query [SELECT e FROM Employee e JOIN e.competences c WHERE c.skill = :skill], line 1, column 54: unknown state or association field [skill] of class [com.kaio.model.Competence].
at org.eclipse.persistence.exceptions.JPQLException.unknownAttribute(JPQLException.java:457)
at org.eclipse.persistence.internal.jpa.parsing.DotNode.validate(DotNode.java:88)
at org.eclipse.persistence.internal.jpa.parsing.Node.validate(Node.java:91)
at org.eclipse.persistence.internal.jpa.parsing.BinaryOperatorNode.validate(BinaryOperatorNode.java:34)
at org.eclipse.persistence.internal.jpa.parsing.EqualsNode.validate(EqualsNode.java:41)
at org.eclipse.persistence.internal.jpa.parsing.WhereNode.validate(WhereNode.java:34)
at org.eclipse.persistence.internal.jpa.parsing.ParseTree.validate(ParseTree.java:207)
at org.eclipse.persistence.internal.jpa.parsing.ParseTree.validate(ParseTree.java:183)
at org.eclipse.persistence.internal.jpa.parsing.ParseTree.validate(ParseTree.java:173)
at org.eclipse.persistence.internal.jpa.parsing.JPQLParseTree.populateReadQueryInternal(JPQLParseTree.java:110)
at org.eclipse.persistence.internal.jpa.parsing.JPQLParseTree.populateQuery(JPQLParseTree.java:84)
at org.eclipse.persistence.internal.jpa.EJBQueryImpl.buildEJBQLDatabaseQuery(EJBQueryImpl.java:216)
at org.eclipse.persistence.internal.jpa.EJBQueryImpl.buildEJBQLDatabaseQuery(EJBQueryImpl.java:187)
at org.eclipse.persistence.internal.jpa.EJBQueryImpl.<init>(EJBQueryImpl.java:139)
at org.eclipse.persistence.internal.jpa.EJBQueryImpl.<init>(EJBQueryImpl.java:123)
at org.eclipse.persistence.internal.jpa.EntityManagerImpl.createQuery(EntityManagerImpl.java:1376)
... 48 more
The message is pretty clear: It can't find the attribute Skill on my class Competence. But in my opinion there is no reason on that. Or do I have a wrong approach on my problem? How should I query in a List of embeddable objects?
Any help is appreciated.
Okay, this seems to be a bug and it has been solved in a later version of eclipse link. I updated the project to EclipseLink 2.4 and the problem disappeared.
Your Competence table will have a Skill.id field, so try:
"SELECT e FROM Employee e JOIN e.competences c WHERE c.skill.id = :skillId"
query.setParameter("skillId", skill.getId());