When attempting to generate dynamic queries using CriteriaBuilder, Hibernate is not creating the proper SQL with regards to an Entities member variable associated with #ElementCollection.
Sample Entity:
#Entity
#Table(name = "myclass")
public class MyClass {
...
#ElementCollection
#CollectionTable(
name = "myclass_mysubclass",
joinColumns = #JoinColumn(name = "myclass_id")
)
#Column(name = "mysubclass_id")
private List<Integer> mySubClassIDs;
...
}
CriteriaBuilder code:
CriteriaBuilder criteriaBuilder = getEntityManager().getCriteriaBuilder();
CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(MyClass.class);
Root<T> root = criteriaQuery.from(MyClass.class);
Expression<Object> e = root.get("mySubClassIDs");
List<Object> o = (List<Object>) entry.getValue();
criteriaQuery.where(e.in(o));
where entry.getValue() will return an ArrayList<Integer> of [1]
Produces:
SELECT distinct count(myclass0_.id) as col_0_0_
FROM hotel myclass0_
cross join myclass_mysubclass mySubClassids1_
where myclass0_.id=mySubClassids1_.myclass_id and (. in (1))
Why is Hibernate not generating the "in" clause properly? the "." should be mySubClassids1_.mysubclass_id
Am I missing something in the annotation of the member variable? Doesn't seem so, as it is enough to generate the cross join.
The env is Jboss AS 7 with Hibernate 4.2.7.SP1-redhat-3 on jdk-6
Your schema is creating two separate tables:
create table myclass (
id int8 not null,
primary key (id)
);
create table myclass_mysubclass (
myclass_id int8 not null,
mysubclass_id int4
);
So, it seems you need to do a join instead of a get:
Expression<Object> e = root.join("mySubClassIDs");
Worked for me at any rate.
Related
I modeled a database relationship in JPA like this:
#Entity
#Data
#NoArgsConstructor(access = AccessLevel.PROTECTED) // Required for JPA
#AllArgsConstructor
public class OuterEntity {
#Id
private String outerId;
#JoinColumn(nullable = true)
#ManyToOne(fetch = FetchType.LAZY, optional = true)
private InnerEntity inner;
}
#Entity
#Data
#NoArgsConstructor(access = AccessLevel.PROTECTED) // Required for JPA
#AllArgsConstructor
public class InnerEntity {
#Id
private String innerId;
#Column
private boolean deleted;
}
And persisted some test data as follows:
InnerEntity inner = new InnerEntity("inner", false);
OuterEntity outerWithInner = new OuterEntity("outerWithInner", inner);
OuterEntity outerWithoutInner = new OuterEntity("outerWithoutInner", null);
em.persist(inner);
em.persist(outerWithInner);
em.persist(outerWithoutInner);
Performing a very simple query using the criteria API successfully gives two results:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<OuterEntity> query = cb.createQuery(OuterEntity.class);
query.from(OuterEntity.class);
System.out.println(em.createQuery(query).getResultList());
// [OuterEntity(outerId=outerWithInner, inner=InnerEntity(innerId=inner, deleted=false)), OuterEntity(outerId=outerWithoutInner, inner=null)]
But once I add a filter on the inner entity like this:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<OuterEntity> query = cb.createQuery(OuterEntity.class);
Root<OuterEntity> root = query.from(OuterEntity.class);
query.where(cb.or(cb.isNull(root.get("inner")), cb.isFalse(root.get("inner").get("deleted"))));
hibernate generates a cross join with where outerentit0_.innerId_innerId=innerentit1_.innerId in the resulting SQL in order to access the inner entity's table:
select
outerentit0_.outerId as outerid1_1_,
outerentit0_.inner_innerId as inner_in2_1_
from OuterEntity outerentit0_
cross join InnerEntity innerentit1_
where
outerentit0_.inner_innerId=innerentit1_.innerId
and (
outerentit0_.inner_innerId is null
or innerentit1_.deleted=0
)
This now only returns one entity:
[OuterEntity(outerId=outerWithInner, inner=InnerEntity(innerId=inner, deleted=false))]
Which is not the result I desire. I expected there to still be two results.
I believe this happens because the filter outerentit0_.inner_innerId=innerentit1_.innerId removes the outer entity that has null for its inner, because using = on nulls always evaluates to false.
Note that I explicitly set the join column to nullable and the many-to-one relationship to optional in the JPA-annotations, so I expected JPA to properly handle the null-case instead.
If I add either root.join("inner", JoinType.LEFT); or root.join("inner", JoinType.LEFT);, I get the correct result because the generated query is using a left outer join:
select
outerentit0_.outerId as outerid1_1_,
outerentit0_.inner_innerId as inner_in2_1_
from OuterEntity outerentit0_
left outer join InnerEntity innerentit1_
on outerentit0_.inner_innerId=innerentit1_.innerId
where
outerentit0_.inner_innerId is null
or innerentit1_.deleted=0
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<OuterEntity> query = cb.createQuery(OuterEntity.class);
Root<OuterEntity> root = query.from(OuterEntity.class);
root.join("inner", JoinType.LEFT);
cb.isFalse(root.get("inner").get("deleted"))));
List<OuterEntity> result = em.createQuery(query).getResultList();
System.out.println(result);
// [OuterEntity(outerId=outerWithInner, inner=InnerEntity(innerId=inner, deleted=false)), OuterEntity(outerId=outerWithoutInner, inner=null)]
Which is a workable solution I suppose, but I am very confused why hibernate would emit a cross join, which is subtly wrong for my expectations, the worst kind of wrong. What did I do to cause this behaviour? Am I required to always explicitly perform a left outer join when dealing with nullable relations like this?
Our in-house framework built with Java 11, Spring Boot, Hibernate 5 and QueryDSL does a lot of auto-generation of queries. I try to keep everything efficient and load associations only when needed.
When loading full entities, the programmer can declare a NamedEntityGraph to be used. Now there is one case where a query like this is generated:
select user.groups
from User user
where user.id = ?1
Where the Entities in question look like this:
#Entity
#NamedEntityGraph(name = User.ENTITY_GRAPH,
attributeNodes = {
#NamedAttributeNode(User.Fields.permissions),
#NamedAttributeNode(value = User.Fields.groups, subgraph = "user-groups-subgraph")
},
subgraphs = #NamedSubgraph(
name = "user-groups-subgraph",
attributeNodes = {
#NamedAttributeNode(Group.Fields.permissions)
}
))
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Enumerated(EnumType.STRING)
#ElementCollection(targetClass = Permission.class)
#CollectionTable(name = "USERS_PERMISSIONS", joinColumns = #JoinColumn(name = "uid"))
private Set<Permission> permissions = EnumSet.of(Permission.ROLE_USER);
#ManyToMany(fetch = LAZY)
private Set<Group> groups = new HashSet<>();
}
#Entity
#NamedEntityGraph(name = Group.ENTITY_GRAPH,
attributeNodes = {
#NamedAttributeNode(value = Group.Fields.permissions)
})
public class Group {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
#Enumerated(EnumType.STRING)
#ElementCollection(targetClass = Permission.class)
#CollectionTable(
name = "GROUPS_PERMISSIONS",
joinColumns = #JoinColumn(name = "gid")
)
#NonNull
private Set<Permission> permissions = EnumSet.noneOf(Permission.class);
}
When selecting either User or Group directly, the generated query simply applies the provided NamedEntityGraphs. But for the above query the exception is:
org.hibernate.QueryException:
query specified join fetching, but the owner of the fetched association was not present in the select list
[FromElement{explicit,collection join,fetch join,fetch non-lazy properties,classAlias=user,role=foo.bar.User.permissions,tableName={none},tableAlias=permission3_,origin=null,columns={,className=null}}]
I first tried the User graph, but since we are fetching Groups, I tried the Group graph. Same Exception.
Problem is, there is no easy way to add a FETCH JOIN to the generated query, since I don't know which properties of the association should be joined in anyway. I would have to load the Entitygraph, walk it and any subgraph and generated the right join clauses.
Some more details on Query generation:
// QueryDsl 4.3.x Expressions, where propType=Group.class, entityPath=User, assocProperty=groups
final Path<?> expression = Expressions.path(propType, entityPath, assocProperty);
// user.id = ?1
final BooleanExpression predicate = Expressions.predicate(Ops.EQ, idPath, Expressions.constant(rootId));
// QuerydslJpaPredicateExecutor#createQuery from Spring Data JPA
final JPQLQuery<P> query = createQuery(predicate).select(expression).from(path);
// Add Fetch Graph
((AbstractJPAQuery<?, ?>) query).setHint(GraphSemantic.FETCH.getJpaHintName(), entityManager.getEntityGraph(fetchGraph));
EDIT:
I can reproduce this with a simple JPQL Query. It's very strange, if I try to make a typed query, it will select a List of Sets of Group and untyped just a List of Group.
Maybe there is something conceptually wrong - I'm selecting a Collection and I'm trying to apply a fetch join on it. But JPQL doesn't allow a SELECT from a subquery, so I'm not sure what to change..
// em is EntityManager
List gs = em
.createQuery("SELECT u.groups FROM User u WHERE u.id = ?1")
.setParameter(1, user.getId())
.setHint(GraphSemantic.FETCH.getJpaHintName(), em.getEntityGraph(Group.ENTITY_GRAPH))
.getResultList();
Same Exception:
org.hibernate.QueryException: query specified join fetching, but the owner of the fetched association was not present in the select list
So the problem can be distilled down to a resolution problem of the Entit Graphs attributes:
select user.groups
from User user
where user.id = ?1
With the Entity Graph
EntityGraph<Group> eg = em.createEntityGraph(Group.class);
eg.addAttributeNodes(Group.Fields.permissions);
Gives an Exception that shows that Hibernate tries to fetch User.permissions instead of Group.permissions. This is the bug report.
And there is another bug regarding the use of #ElementCollection here.
I use Hibernate OGM for MongoDB, and mapping simple ShopItem entity with Map:
#NamedQueries({
#NamedQuery(name = ShopItem.GET_BY_NAME,
query = "select items from ShopItem items where
items.name=:name"),
#NamedQuery(name = ShopItem.GET_BY_OPTION,
query = "select items from ShopItem items
join items.options map
where ( KEY(map) = :optionKey and map = :optionValue )")
})
#Entity
public class ShopItem {
public static final String GET_BY_NAME = "ShopItem.getByName";
public static final String GET_BY_OPTION = "ShopItem.getByOption";
#Id
#GeneratedValue(generator = "uuid")
#GenericGenerator(name = "uuid", strategy = "uuid2")
private String id;
private String name;
#ElementCollection(fetch = FetchType.EAGER)
private Map<String, String> options = new HashMap<>();
//...etc
}
This mapped good. And I tried write JPQL named queries for my ShopItemRepository. I've searched NamedQuery by field 'name' and it works fine. But I need to search by both fields 'key' and 'value' and I can't write correct JPQL for this.
For example i have entity in db:
{
"_id":"df111b8a-b831-4200-95b3-f80576cd7273",
"name":"Example",
"description":"My description",
"options":
{
"weight":"60 kg",
"age":"22"
}
}
And i want find all entities where options have key 'age' and have value '22'.
I try to write:
select items from ShopItem items join items.options map where ( KEY(map) = :optionKey and map = :optionValue )
or
select items from ShopItem items join items.options map where ( KEY(map) = :optionKey and :optionValue in (VALUE(map))
or
select items from ShopItem items where ( KEY(items.options) = :optionKey and items.options = :optionValue )
or
select items from ShopItem items where ( KEY(items.options) = :optionKey and :optionValue in (VALUE(items.options))
And have this error
org/hibernate/hql/ast/origin/hql/resolve/GeneratedHQLResolver.g: node from line 1:85 no viable alternative at input '('
can't look backwards more than one token in this stream
My code in repo:
List<ShopItem> result = entityManager.createNamedQuery(ShopItem.GET_BY_OPTION, ShopItem.class)
.setParameter("optionKey", key)
.setParameter("optionValue", value)
.getResultList();
Can you help me write correct JPQL query? And if this is not possible, then suggest how to do it another way.
I can't use Spring Data JPA.
Right now, the only way to query this case is using a native query:
List<ShopItem> results = session
.createNativeQuery( "{ 'options.Weight': '60 Kg' }", ShopItem.class )
.getResultList();
You can still declare the queries using #NamedNativeQuery (instead of #NamedQuery) but parameters aren't supported for MongoDB.
There are the following structure of tables:
CREATE TABLE Train
(
idTrain INT NOT NULL AUTO_INCREMENT,
nameTrain CHAR(20) NOT NULL,
PRIMARY KEY (idTrain)
);
CREATE TABLE Station
(
idStation INT NOT NULL AUTO_INCREMENT,
nameStation CHAR(50) NOT NULL,
PRIMARY KEY (idStation)
);
CREATE TABLE Schedule
(
idStation INT NOT NULL,
idTrain INT NOT NULL,
arrivalTime TIME NOT NULL,
departureTime TIME NOT NULL,
nextStation INT NOT NULL,
kmToNextStation INT NOT NULL,
PRIMARY KEY (idStation, idTrain, nextStation),
FOREIGN KEY (idStation) REFERENCES Station(idStation),
FOREIGN KEY (idTrain) REFERENCES Train(idTrain),
FOREIGN KEY (nextStation) REFERENCES Station(idStation)
);
Necessary to implement the following sql-query using JPA Criteria:
SELECT Station.nameStation, Schedule.arrivalTime, Schedule.departureTime, Schedule.kmToNextStation
FROM Schedule
JOIN Station
ON Station.idStation = Schedule.idStation
JOIN Train
ON Schedule.idTrain = Train.idTrain
WHERE Train.nameTrain = "268A";
Here is my attempt:
EntityManager em = EntitySupport.getEntityManager();
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<ScheduleEntity> cq = builder.createQuery(ScheduleEntity.class);
Root<ScheduleEntity> root = cq.from(ScheduleEntity.class);
Join<ScheduleEntity, TrainEntity> idTrain = root.join("idTrain");
Join<ScheduleEntity, StationEntity> idStation = root.join("idStation");
cq.multiselect(root.get("arrivalTime"),
root.get("departureTime"),
idTrain.get("nameTrain"));
Query query = em.createQuery(cq);
List res = query.getResultList();
System.out.println("Result query: " + res.toString());
Apparently, I'm doing something wrong, because I get the following error:
Exception in thread "main" java.lang.IllegalArgumentException:
org.hibernate.hql.internal.ast.QuerySyntaxException: Unable to locate
appropriate constructor on class [srt.entity.ScheduleEntity]. Expected
arguments are: java.util.Date, java.util.Date, java.lang.String
[select new srt.entity.ScheduleEntity(generatedAlias0.arrivalTime,
generatedAlias0.departureTime, generatedAlias1.nameTrain) from
srt.entity.ScheduleEntity as generatedAlias0 inner join
generatedAlias0.idTrain as generatedAlias1 inner join
generatedAlias0.idStation as generatedAlias2]
Help me make the correct code for the above sql-query using JPA Criteria.
I was wrong in the concept of JPA Criteria. Now I completely understood, the answer would be as follows:
EntityManager em = EntitySupport.getEntityManager();
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<ScheduleEntity> cq = builder.createQuery(ScheduleEntity.class);
Root<ScheduleEntity> from = cq.from(ScheduleEntity.class);
Join<ScheduleEntity, StationEntity> idStation = from.join("idStation");
Join<ScheduleEntity, TrainEntity> idTrain = from.join("idTrain");
Predicate where = builder.equal(idTrain.get("nameTrain"), "268A");
cq.where(where);
List<ScheduleEntity> schedule = em.createQuery(cq).getResultList();
for(ScheduleEntity s : schedule) {
System.out.println(s.toString());
}
I have two entities, ShoppingCart and ShoppingCartLine. ShoppingCart has a collection of ShoppingCartLines. I am trying to create a JPA query using criteria to get a list of ShoppingCart ids and the number of ShoppingCartLines each ShoppingCart has.
Here is the mapping in the ShoppingCart class:
#OneToMany(mappedBy = "shoppingCart", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval=true)
private Set<ShoppingCartLine> shoppingCartLines = new TreeSet<ShoppingCartLine>();
Here is the code I am attempting to create my JPA query with:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Tuple> cq = cb.createTupleQuery();
Root<ShoppingCart> carts = cq.from( ShoppingCart.class );
Join<ShoppingCart, ShoppingCartLine> lines = carts.join( "shoppingCartLines", JoinType.LEFT);
cq.multiselect( carts.<Long>get( "id" ), cb.count( lines ));
cq.groupBy( carts.<Long>get("id") );
Query tq = em.createQuery( cq );
return tq.getResultList();
When I run this I get an SQLGrammerException, the SQL produced does not look correct to me.
select
shoppingca0_.id as col_0_0_,
count(.) as col_1_0_
from
SHOPPINGCART shoppingca0_
left outer join
SHOPPINGCARTLINE shoppingca1_
on shoppingca0_.id=shoppingca1_.shoppingCart_id,
SHOPPINGCARTLINE shoppingca2_
where
shoppingca0_.id=shoppingca2_.shoppingCart_id
group by
shoppingca0_.id
I should mention I am using Hibernate 3.5.4 with MySQL5Dialect
This is the query I am wanting to generate:
select
sc.id,
count(scl.id)
from
shoppingcart sc
left join
shoppingcartline scl
on
scl.shoppingCart_id = sc.id
group by
sc.id
Any idea what I'm doing wrong? Thanks.
Try to replace join and multiselect lines with
SetJoin<ShoppingCart, ShoppingCartLine> lines =
carts.joinSet( "shoppingCartLines", JoinType.LEFT);
cq.multiselect( carts.<Long>get( "id" ), cb.count( lines.<Long>get( "id" ) ));