JPA Criteria Query over an entity hierarchy using single table inheritance - java

Say I have the following entities:
#Entity
#Inheritance(strategy = SINGLE_TABLE)
#DiscriminatorColumn(name = "type")
public abstract class BaseEntity {
private Date someDate;
private Date otherDate;
private boolean flag;
}
#Entity
#DiscriminatorValue("entity1")
public class Entity1 extends BaseEntity {
private String someProperty;
}
#Entity
#DiscriminatorValue("entity2")
public class Entity2 extends BaseEntity {
private String otherProperty;
}
I'm trying to build a criteria query that returns instances of BaseEntity based on properties in BaseEntity and both of the subclasses. So essentially I'm looking for a criteria query that corresponds to this pseudo-SQL:
SELECT * FROM <BaseEntity table name>
WHERE someDate < ? AND otherDate > ? AND flag = ?
AND someProperty = ? AND otherProperty = ?;
I'd rather not build two separate queries since they have so much overlap (i.e. most of the properties are in the base class). However, I haven't figured out a way to reference the subclass properties in the query if I declare BaseEntity as the root. Is it possible to build a criteria query like this?
UPDATE:
Maybe some code would clarify the question. I'd essentially like to do something like this:
CriteriaBuilder builder = ...;
CriteriaQuery<BaseEntity> query = ...;
Root<BaseEntity> root = ...;
query.select(root).where(builder.and(
builder.lessThan(root.get(BaseEntity_.someDate), new Date()),
builder.greaterThan(root.get(BaseEntity_.otherDate), new Date()),
builder.isTrue(root.get(BaseEntity_.flag)),
builder.equal(root.get(Entity1_.someProperty), "foo"), <-- This won't work
builder.equal(root.get(Entity2_.otherProperty), "bar") <-- Neither will this
));
Now, I understand why the above code sample doesn't work, but I'd like to know if there's a way to get around it.

I managed to solve this by downcasting the BaseEntity root into new roots that correspond to the subclass types with CriteriaBuilder.treat() like this:
CriteriaBuilder builder = ...;
CriteriaQuery<BaseEntity> query = ...;
Root<BaseEntity> root = ...;
Root<Entity1> entity1 = builder.treat(root, Entity1.class);
Root<Entity2> entity2 = builder.treat(root, Entity2.class);
query.select(root).where(builder.and(
builder.lessThan(root.get(BaseEntity_.someDate), new Date()),
builder.greaterThan(root.get(BaseEntity_.otherDate), new Date()),
builder.isTrue(root.get(BaseEntity_.flag)),
builder.equal(entity1.get(Entity1_.someProperty), "foo"),
builder.equal(entity2.get(Entity2_.otherProperty), "bar")
));

Try Junction. You can always do something like this:
Criteria crit = session.createCriteria(YourClassName.class);
Criterion cr1 = Restrictions.lt("propertyname", propertyvalue);
Criterion cr2 = Restrictions.gt("propertyname", propertyvalue);
Criterion cr3 = Restrictions.eq("propertyname", propertyvalue);
Criterion cr4 = Restrictions.eq("propertyname", propertyvalue);
Junction cond = Restrictions.conjunction();
cond.add(cr1).add(cr2).add(cr3).add(cr4)
crit.add(cond);
I hope I got your question Correctly.

Related

Spring boot JPA & Criteria API - Select single column

I am trying to retrieve a single column from the table, but I am getting compilation error about return type.
SQL
select oComment from comment where oNote = note and version > 0;
I have Comment table and Note table. Comment table has comment, note and version columns. The comment itself is a note. Now I want to retrieve all comments of the note which has version greater than 0. But here I want only comment column which of note type.
Comment.java
#Entity
#Table(name="comment")
#Cache(usage=CacheConcurrencyStrategy.READ_WRITE, region="comments")
public class Comment implements Serializable {
private static final long serialVersionUID = -4420192568334549165L;
public Comment() {
}
#Id
#OneToOne
#JoinColumn(name="commentuuid",referencedColumnName="noteuuid")
private Note oComment;
#Id
#OneToOne
#JoinColumn(name="noteuuid",referencedColumnName="noteuuid")
private Note oNote;
}
Note.java
#Entity
#Table(name = "note")
#Cache(usage=CacheConcurrencyStrategy.READ_WRITE, region="notes")
public class Note implements Serializable{
private static final long serialVersionUID = 4089174391962234433L;
#Column(name="title")
private String m_szTitle;
#Column(name="htmlcontent")
private String m_sbHtmlContent;
#Column(name="textcontent")
private String m_sbTextContent;
#Id
#Column(name="noteuuid", columnDefinition="varchar(36)")
private String noteUuid;
}
CustomRepositoryMethod
public List<Note> findAllByNote(Note oNote, int iOffset, int iResultSize) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Comment> cq = cb.createQuery(Comment.class);
Root<Comment> oComment = cq.from(Comment.class);
List<Predicate> predicates = new ArrayList<>();
predicates.add(cb.equal(oComment.get("oNote"), oNote));
predicates.add(cb.greaterThan(oComment.get("version"), 0));
Subquery<Note> subquery = cq.subquery(Note.class);
Root<Note> note = subquery.from(Note.class);
cb.desc(note.get("m_dtCreationDate"));
cq.where(predicates.toArray(new Predicate[0]));
cq.multiselect(oComment.<Note>get("oComment"));
return (List<Note>)em.createQuery(cq).setFirstResult(iOffset).setMaxResults(iResultSize).getResultList();
}
error
Error at return statement,
Cannot cast from List<Comment> to List<Note>
in CustomRepositoryMethod replace first
line CriteriaQuery<Comment> cq = cb.createQuery(Comment.class); to CriteriaQuery<Note> cq = cb.createQuery(Note.class)
cb.createQuery parameter accept result Class in docs you can see.
update
// assuming query like
// select oComment from comment inner join Note on comment.noteuuid=Note.noteuuid where Note.noteUuid = 1 and version > 0;
CriteriaBuilder cb = em.getCriteriaBuilder();
// data type of oComment
CriteriaQuery<Note> query = cb.createQuery(Note.class);
// from comment
Root<Comment> comment = query.from(Comment.class);
//join
Join<Comment, Note> note = comment.join(comment.get("oNote"));
//version Condition
Predicate version=cb.greaterThan(comment.get("version"),0 );
//Note condition
predicate note=cb.equal(note.get("noteuuid"),note.getNoteUuid());
// get oComment and where condtion
query.select(comment.get("oComment")).where(cb.and(version,note));
return em.createQuery(query).setFirstResult(iOffset).setMaxResults(iResultSize).getResultList();
Your criteria query's root is Comment not Note
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Comment> cq = cb.createQuery(Comment.class);
Root<Comment> oComment = cq.from(Comment.class);
and you are trying to do
return (List<Note>)em.createQuery(cq).setFirstResult(iOffset)
.setMaxResults(iResultSize).getResultList();
the compilation error is inevitable in this scenario, because em.createQuery(cq).getResultList() will return List<Comment> not List<Note>
It is not necessary to write a custom repository method since the one you are creating is already generated in spring-data.
If your repository extends the CrudRepository you will be given that method you are looking for for free.
The pattern is findAllBy[propertyOfClass].
But please be aware of that you actually have no Collection of NOTE in your entity.
Perhaps you should first change the OneToOne association into a OneToMany.
Can be built as a criteria query as follows:
CriteriaQuery<Country> q = cb.createQuery(Country.class);
Root<Country> c = q.from(Country.class);
q.select(c.get("currency")).distinct(true);
The select method takes one argument of type Selection and sets it as the SELECT clause content.

Hibernate criteria join table issue

I have 3 entities as you can see below. I want to write a query that fetches products. In this query the parameter is a list of optionValues id.
now my question is how to join these entities?
Product:
public class Product{
//other col
#OneToMany(mappedBy = "product")
private Set<Attribute> attributeSet = new HashSet<>();
}
Attribute:
public class Attribute{
#OneToOne
#JoinColumn(name = "OPTION_VALUE_ID")
private OptionValue optionValue;
#ManyToOne
#JoinColumn(name="PRODUCT_ID",referencedColumnName="id")
private Product product;
}
optionValue:
public class OptionValue{
#Column(name = "id")
private Long id;
#Column(name = "value",updatable = true)
private String value;
}
I wrote a query but I think my code is not a good solution.
Criteria aCriteria = null;
if (!optionValueList.isEmpty()) {
aCriteria = currentSession().createCriteria(Attribute.class, "attribute");
aCriteria.createAlias("attribute.optionValue", "optionValue");
aCriteria.add(Restrictions.in("optionValue.id", optionValueList));
attributes = aCriteria.list();
}
PagingData<Product> pagingData = new PagingData<>();
Criteria criteria = currentSession().createCriteria(Product.class, "product");
if (!attributes.isEmpty()) {
for (Attribute attribute:attributes){
longList.add(attribute.getId());
}
criteria.createAlias("product.attributeSet", "attribute");
criteria.add(Restrictions.in("attribute.id", longList));
criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
}
The general idea is to start with creating criteria of objects you want to return, and travel further by adding criteria which is joined. So I start with Parent class, add qualifiers and end up with most nested element, OptionValue.
Code below is untested, but you should get the idea:
Criteria criteria = currentSession()
.createCriteria(Product.class)
.createCriteria("attributeSet", "join_between_product_and_attribute");
if (!attributes.isEmpty()) {
Set<String> attributeIds = new HashSet<>();
for (Attribute attribute : attributeList) {
attributeIds.add(attribute.getId());
}
criteria.add(Restrictions.in("id", attributeIds));
}
criteria = criteria.createCriteria("optionValue", "join_between_attribute_optionvalue");
if (!optionValueList.isEmpty()) {
criteria.add(Restrictions.in("id", optionValueList));
}
an even easier solution would be to use a CriteriaQuery. i did not test the following code, but i think it should work correctly. it requires hibernate 5, but also works with some modifications in hibernate 4:
CriteriaBuilder cb = sessionFactory.getCriteriaBuilder();
CriteriaQuery<Product> query = cb.createQuery(Product.class);
Root<Product> r = query.from(Product.class);
In<Object> in = cb.in(r.join("attributeSet ").join("optionValue").get("id"));
for(Object optionValue : optionValueList){
in.value(optionValue);
}
query.select(r).where(in);
return sessionFactory.getCurrentSession().createQuery(query).getResultList();
i am assuming, that you can access the optionValueList since you posted it in your question.
For the solution with EntityManager i am assuming you already were able to instantiate one.
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Product> query = cb.createQuery(Product.class);
Root<Product> r = query.from(Product.class);
In<Object> in = cb.in(r.join("attributeSet ").join("optionValue").get("id"));
for(Object optionValue : optionValueList){
in.value(optionValue);
}
query.select(r).where(in);
return entityManager.createQuery(query).getResultList();
if you have an EntityManagerFactory, replace the first entityManager with it and the second one with entityManagerFactory.createEntityManager()

#Filter on two entities in one QueryDSL query result in one whereclause

I have multiple entitys that need to be uniformly filtered by one Row. To keep it simple, i want to use #Filter on each instead of changing every query involved.
Now when i activate two #Filters, only one (the one in the from entity) is applied, the #Filter of the joined entity is ignored without error message.
Minimal example:
Entity A:
#Entity
#FilterDef(name="ExampleFilterA", parameters=#ParamDef( name = "version", type = "integer" ) )
#Filters( {
#Filter(name="ExampleFilterA", condition=":version = MY_VERSION") } )
#Table(name = "TABLE_A")
public class EntityA implements Serializable {
private Integer myVersion;
#Column(name = "MY_VERSION")
public Integer getMyVersion() {
return myVersion;
}
public void setMyVersion(Integer myVersion) {
this.myVersion = myVersion;
}
...other columns...
}
EntityB is similar
Loading method:
EntityManager em = getEntityManager();
Session session = em.unwrap(org.hibernate.Session.class);
Filter filterA = session.enableFilter("ExampleFilterA");
filterA.setParameter( "version", 1 );
Filter filterB = session.enableFilter("ExampleFilterB");
filterB.setParameter( "version", 1 );
JPAQuery exampleQuery = query()
.from(A)
.join(A.entityB, B);
Now this generates a query (taken from server log) that has only a WHERE ? = TABLE_A.MY_VERSION, no where for TABLE_B. Result: expected Data plus wrong version entrys from table B.
Actually solved it myself: The collection od EntityB's JoinColumns were broken.
With correct joinColumns, no Filter is needed.

Resolving Criteria on Polymorphic child class attribute jpa hibernate query

Using hibernate 3.6.10 with hibernate jpa 2.0.
My problem boils down to needing to set some criteria on a column of a child object during a somewhat complex joining query.
I have a set of objects similar to:
#Entity
#Inheritance(strategy = InheritanceType.JOINED)
public class Ball
{
private String name;
//...getter and setter crud...
}
#Entity
public class BeachBall extend ball
{
private boolean atTheBeach;
//...getter and setter crud...
}
#Entity
public class SoccerBall extend ball
{
private int numberOfKicks;
//...getter and setter crud...
}
#Entity
public class Trunk
{
private Set<Ball> balls;
#OneToMany(mappedBy = "trunk", cascade = CascadeType.ALL, orphanRemoval = true)
public Set<Ball> getBalls()
{
return balls;
}
}
#Entity
public class Car
{
private Trunk trunk;
private String carModel;
//...getter and setter crud...
}
Now i need to query how many soccer balls have 20 kicks in a car with a specific model.
Using JPA I tried to do something like:
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Car> criteriaQuery = criteriaBuilder.createQuery(Car.class);
Root<Car> car= criteriaQuery.from(Car.class);
Join<Car, Trunk> trunkJoin = car.join(Car_.trunk);
Join<Trunk, Ball> ballJoin = trunkJoin.join(Trunk_.Balls);
criteriaQuery.select(trunk);
Predicate [] restrictions = new Predicate[]{ criteriaBuiler.equal(car.get(carModel), "Civic"), criteriaBuilder.equal(ballJoin.get("numberOfKicks"), 20)};
criteriaQuery.where(restrictions);
TypedQuery<Car> typedQuery = entityManager.createQuery(criteriaQuery);
Car carWithSoccerBalls = typedQuery.getSingleResult();
At runtime the above code dies because numberOfKicks is only on soccerballs and due to how its typed in Trunk it only knows about ball. If i manually create a from on the soccerballs and setup criteria to join it i can query numberOfKicks, however i feel like there must be a way to inform the query that the set is in fact a set.
Please note i cannot post any of the actual code so all above examples are just examples.
Using JPA and hibernate like above is there a way to force hibernate to know that the set< ball > is actually set< soccerball >?
Due to time restrictions i'm taking the easy way out :(. If anyone can answer better then what i have i'll gladly choose their answer over mine.
To make the criteria api recognize that i'm looking for the inherited table i changed my query code to be:
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Car> criteriaQuery = criteriaBuilder.createQuery(Car.class);
Root<Car> car= criteriaQuery.from(Car.class);
Root<Soccerball> soccerballs = criteriaQuery.from(SoccerBall.class);
Join<Car, Trunk> trunkJoin = car.join(Car_.trunk);
Join<Trunk, Ball> ballJoin = trunkJoin.join(Trunk_.Balls);
criteriaQuery.select(trunk);
Predicate [] restrictions = new Predicate[]{ criteriaBuiler.equal(car.get(carModel), "Civic"), criteriaBuilder.equal(soccerball.get("numberOfKicks"),20), criteriaBuilder.equal(soccerball.get(SoccerBall_.id),car.get(Car_.id))};
criteriaQuery.where(restrictions);
TypedQuery<Car> typedQuery = entityManager.createQuery(criteriaQuery);
Car carWithSoccerBalls = typedQuery.getSingleResult();
The following retrieves all Cars with nested list attributes satisfying equality criteria for subclass type in a collection and equality on root element.
I've modified the query to work with the datamodel in the original question.
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Car> carQuery = criteriaBuilder.createQuery(Car.class);
Root<Car> carRoot = carQuery.from(Car.class);
Subquery<SoccerBall> ballQuery = carQuery.subquery(SoccerBall.class);
Root<SoccerBall> soccerBall = ballQuery.from(SoccerBall.class);
ballQuery.select(soccerBall);
ballQuery.where(criteriaBuilder.equal(soccerBall.get(SoccerBall_.numberOfKicks), 25));
Join<Car, Trunk> carTrunkJoin = carRoot.join(Car_.trunk);
SetJoin<Trunk, Ball> trunkBallJoin = carTrunkJoin.join(Trunk_.balls);
carQuery.select(carRoot);
carQuery.where(criteriaBuilder.and(
trunkBallJoin.in(ballQuery),
criteriaBuilder.equal(carRoot.get(Car_.carModel), "Civic")));
TypedQuery<?> typedQuery = entityManager.createQuery(carQuery);
List<?> result = typedQuery.getResultList();
The equivalent SQL is:
SELECT * FROM car JOIN trunk JOIN ball WHERE ball.id IN (SELECT soccerball.id FROM soccerball WHERE soccerball.numberOfKicks = 25)

Pass a object as query parameter and exclude the ID column

I have the following:
#Entity
#NamedQuery(name = "listCarsBySecurity", query = "SELECT c FROM Car c WHERE c.security = :security"
public class Car {
#Id
#GeneratedValue
private Long id;
#NotNull()
#Column(nullable = false)
private String make;
#NotNull()
#Column(nullable = false)
private String model;
// Some more fields
#NotNull()
#OneToOne (fetch = FetchType.LAZY, orphanRemoval=true)
private Security security = new Security();
// Some getters and setters
As you can see, the Car class has a "Security" object which is LAZY fetched. The security class looks like:
#Entity
public class Security {
#Id #GeneratedValue
private Long id;
// Security equipment. Add in alphanumerical order
private boolean abs;
private boolean airbag;
private boolean antispin;
// Some getters and setters
as you can see, the named query list try to list all cars which has a security entity equal to a provided security object.
The persistence method looks like:
#Stateless
public class CarEJB {
#PersistenceContext(unitName = "carcmsPU")
private EntityManager em;
public List<Car> listCarsBySecurity(Security security) {
TypedQuery<Car> query = em.createNamedQuery("listCarsBySecurity", Car.class);
query.setParameter("security", security);
return query.getResultList();
}
And a junit test looks like:
#Test
public void searchCar() throws Exception {
// Looks up the EJBs
carEJB = (CarEJB) ctx.lookup("java:global/classes/CarEJB");
// Create a new Ferrari with security = ABS brakes and Airbag
Car car = new Car();
car.setMake("Ferrari");
car.setModel("Some model");
car.setSubModel("Some sub model");
car.setEngine("Some engine");
car.setYear(1999);
car.getFuel().setGasoline(true);
car.setGearbox(Gearbox.AUTOMATIC);
car.setKilometres(323);
car.setDescription("This is a description");
Security security = new Security();
security.setAbs(true);
security.setAirbag(true);
car.setSecurity(security);
carEJB.createCar(car); // Persist
// Create a new security object and match it to the former one
Security securityObject = new Security();
securityObject.setAbs(true);
securityObject.setAirbag(true);
List<Car> carList = carEJB.listCarsBySecurity(securityObject);
assertTrue("Should contain at least 1 car with ABS and Airbag", carList.size() > 0 );
for (Car carTemporary : carList) {
System.out.println(carTemporary.toString());
}
}
The thing is that the list does not contain any cars at all. And I think I know why; the named query does try to match the security_id with NULL (since I have not define it).
My question is: How can I perform a query by passing a object as a query parameter with no ID and by not specify all fields which shall be compared inside that object? (or how exclude the ID from a search)?
Best regards
You can define a named query using OR and passing each one of the object's attributes. You can also use Criteria API to build a query based on the fields you want to query about. Since you already have a named query I'll leave that one to you.
If you decide to go that way (tough field by field comparation is kind of insane if your entity has way too many attributes). Using criteria you can do something like this:
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Car> query = builder.createQuery(Car.class);
Root<Car> queryRoot = query.from(Car.class);
query.select(queryRoot);
Path<String> pathToYourField = root.get(yourField); //yourField is a variable containing the field.
//You can store all the variables in a list, iterate
//over them and do this for each one.
query.where(builder.and(builder.equal(pathToYourField, "particularValue"))); //You compare the path against a value.
//Rest of the fields / paths
TypedQuery<Car> typedQuery = entityManager.createQuery(query);
List<Car> cars = typedQuery.getResultList();
EDIT: About performance, check this links:
JPA Criteria vs NamedQueries
Another answer regarding Criteria vs HQL
Criteria overhead discussion

Categories