I have an SQL query that gets me exactly the data I need. The problem is that we are trying to express all queries in JPA Criteria API to maintain portability, and I can't figure out how to map this particular query.
The problem is that the JPA Criteria API Subquery class lacks the multiselect() method that CriteriaQuery class has. As you can see in the SQL query, I have computed fields in the sub-query which don't exist in the entity. Thus, I have no way to retrieve these fields.
I would be quite appreciative if anyone knows a solution or could offer guidance, or even if someone could validate that what I am trying to achieve in JPA Criteria API is not possible.
The SQL:
SELECT w.NAME AS 'wave_name',
Count(*) AS 'num_lines',
Sum(qty_ordered) AS 'num_units',
Count(DISTINCT unit_of_work_id) AS 'num_units_of_work',
Sum(completed_units) AS 'completed_units',
( Sum(completed_units) + Sum(qty_scratched) ) / Sum(qty_ordered) AS 'perc_completed_units'
FROM (SELECT t.id,
t.wave_id,
t.quantity_requested AS 'qty_ordered',
t.quantity_scratched AS 'qty_scratched',
t.unit_of_work_id AS 'unit_of_work_id',
Ifnull(m.quantity, 0) AS 'qty_picked',
CASE
WHEN Ifnull(m.quantity, 0) > quantity_requested THEN
quantity_requested
ELSE Ifnull(m.quantity, 0)
END AS 'completed_units'
FROM task t
LEFT OUTER JOIN (SELECT move.task_id,
Sum(quantity) AS 'quantity'
FROM move
GROUP BY task_id) m
ON m.task_id = t.id) s
JOIN wave w
ON w.id = s.wave_id
GROUP BY w.name;
The entities:
#Entity
#Table(name = "task")
public class Task {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="id")
private Long id;
#ManyToOne (cascade = CascadeType.ALL)
#JoinColumn (name = "wave_id", nullable = false)
private Wave wave;
#ManyToOne (cascade = CascadeType.ALL)
#JoinColumn (name = "unit_of_work_id", nullable = false)
private UnitOfWork unitOfWork;
#OneToMany (cascade = CascadeType.ALL, mappedBy = "task")
private Set<Move> moves = new HashSet<Move>();
#Column (name = "quantity_requested")
private Long quantityRequested;
#Column (name = "quantity_scratched")
private Long quantityScratched;
}
#Entity
#Table(name = "wave")
public class Wave {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#Column(name = "name")
private String name;
#OneToMany(mappedBy = "wave", cascade = CascadeType.ALL)
private Set<Task> tasks = new HashSet<Task>();
}
#Entity
#Table(name = "unit_of_work")
public class UnitOfWork {
#Id
#GeneratedValue (strategy = GenerationType.IDENTITY)
#Column (name = "id")
private Long id;
#OneToMany(mappedBy = "unitOfWork", cascade = CascadeType.ALL)
private Set<Task> tasks = new HashSet<Task>();
}
#Entity
#Table(name = "move")
public class Move {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="id")
private Long id;
#ManyToOne (cascade = CascadeType.ALL)
#JoinColumn (name = "task_id", nullable = false)
private Task task;
#Column (name = "quantity")
private Long quantity;
}
I would say use named parameters or native query approach for this.
For example:
Named parameters:
public interface Repo extends JpaRepository<AEntity, String> {
#Query("select a from AEntity a where a.BEntity.name = :name")
public aMethod( #Param("name") String name)
}
OR
Native query approach:
public interface Repo extends JpaRepository<AEntity, String> {
#Query(value = "select * from Tablename t where t.name = :name", nativeQuery=true)
public aMethod(#Param("name") String name)
}
Check this link if you are using spring jpa
http://docs.spring.io/spring-data/data-jpa/docs/1.4.x/reference/htmlsingle/#jpa.named-parameters
Related
I have problem with N+1 Hibernate.
I have followings entities :
public class Coupon {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "ID")
private Long id;
#Builder.Default
#OneToMany(fetch = FetchType.LAZY,
targetEntity = CouponType.class,
cascade = CascadeType.ALL,
mappedBy = "coupon")
private List<CouponType> couponTypeList = new ArrayList<>();
public class CouponType {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#ManyToOne
private Match match;
#ManyToOne
private Coupon coupon;
public class Match {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "ID")
private Long id;
#Builder.Default
#OneToMany(fetch = FetchType.LAZY,
targetEntity = CouponType.class,
mappedBy = "match")
private List<CouponType> couponTypeList = new ArrayList<>();
And I want to avoid N+1 Problem in hibernate. How can I wrtie properly query in JPQL ?
Solution :
#Query("select c from Coupon c join fetch c.couponTypeList t join fetch t.match where c.id = ?1")
Optional<Coupon> getCoupon(Long couponId)
Try to use the following query:
#Query("select c from Coupon c join fetch c.couponTypeList where c.id = ?1")
Optional<Coupon> getCoupon(Long couponId);
I'm new with Criteria API and struggling with building the CriteriaQuery from this simple SQL:
SELECT *
FROM user_acc u
WHERE (SELECT count(b) FROM bonus b WHERE b.employee_id = u.id) > 8;
Could anybody help me with it? I'm confused...
You can list all wanted non-aggregated columns within SELECT and GROUP BY list containing a HAVING clause :
SELECT b.br, b.employee_id
FROM user_acc u
JOIN bonus b ON b.employee_id = u.id
GROUP BY b.br, b.employee_id
HAVING count(b.br) > 8;
You need entities to use Criteria API
#Entity
#Table(name = "user_acc")
public class UserAcc{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
// getters setters
}
#Entity
#Table(name = "employee")
public class Employee{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
// getters setters
}
#Entity
#Table(name = "bonus")
public class Bonus{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#ManyToOne
#JoinColumn(name = "employee_id")
Employee employee;
// getters setters
}
then you can use this
public List<UserAcc> getUserAccs(EntityManager em) {
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<UserAcc> query = builder.createQuery(UserAcc.class);
Root<UserAcc> user = query.from(UserAcc.class);
Subquery<Bonus> subquery = query.subquery(Bonus.class);
Root<Bonus> bonus = subquery.from(Bonus.class);
Path<Long> employeeId = bonus.get("employee").get("id");
Predicate subqueryPredicate = builder.equal(user.get("id"), employeeId);
Expression<Long> bonusCount = builder.count(bonus);
subquery.select(bonusCount)
.where(subqueryPredicate)
.groupBy(employeeId)
.having(builder.greaterThan(bonusCount, 8L);
Predicate predicate = builder.exists(subquery);
query.select(user).where(predicate);
return em.createQuery(query).getResultList();
}
Final query is a bit different but result should be as expected.
I wrote this code using text editor so it needs to be tested.
I'm trying to build a multi-language database, so I've used this database design as a approach for mine.
Now I've two problems/questions:
I want to retrieve all LocalizedEvent for a given language and given categoryId. How can I make a inner join over the LocalizedCategory table with Hibernate Criteria API?
With SQL I would make this statement to get all LocalizedEvent + LocalizedCategory:
SELECT * FROM event e
INNER JOIN
localized_event le ON (le.event_id = e.event_id)
INNER JOIN
localized_category lc ON (lc.category_id = e.category_id)
WHERE
le.locale = 'de' AND lc.locale = 'de'
My current approach looks like this without getting the LocalizedCategory (with Criteria API):
Criteria c = session.createCriteria(LocalizedEvent.class, "localizedEvent");
c.createAlias("localizedEvent.event", "event");
c.createAlias("event.category", "category");
c.add(Restrictions.eq("category.categoryId", categoryId));
c.add(Restrictions.eq("localizedEvent.locale", language));
I think my mapping is not 100% correct. The entity LocalizedEvent should have a property localizedCategory, but I don't want to save the ID of this localizedCategory (therefore I'm using the #Transient annotation) in the LocalizedEvent table, e.g. using a ManyToOne relation (joining LOC_CATEGORY_ID). But I think it's not possible to do this, isn't it? I would have to map this transient field to LocalizedEvent "manually", because Hibernate is not supporting this mapping (if I'm right).
(Using JDBC this property/mapping would cause no problems, because I can easily make my inner joins and assign the property localizedCategory to the LocalizedEvent in a RowMapper or so).
My entities looks like this:
Event
#Entity
#Table(name = "EVENT")
public class Event {
#Id
#GeneratedValue
#Column(name = "EVENT_ID", unique = true)
private Long eventId;
#Column(name = "DATE")
private Date date;
#OneToMany(mappedBy = "event")
private Set<LocalizedEvent> localizedEvents;
#ManyToOne
#JoinColumn(name = "CATEGORY_ID")
private Category category;
}
LocalizedEvent
#Entity
#Table(name = "LOCALIZED_EVENT")
public class LocalizedEvent {
#Id
#GeneratedValue
#Column(name = "LOC_EVENT_ID")
private Long locEventId;
#ManyToOne
#JoinColumn(name = "EVENT_ID")
private Event event;
#Transient
private LocalizedCategory localizedCategory;
#Column(name = "DESCRIPTION")
private String description;
#Column(name = "LOCALE")
private String locale;
}
Category
#Entity
#Table(name = "CATEGORY")
public class Category {
#Id
#GeneratedValue
#Column(name = "CATEGORY_ID", unique = true)
private Long categoryId;
#OneToMany(mappedBy = "category")
private Set<LocalizedCategory> localizedCategories;
#OneToMany(mappedBy = "category")
private Set<Event> events;
}
LocalizedCategory
#Entity
#Table(name = "LOCALIZED_CATEGORY")
public class LocalizedCategory {
#Id
#GeneratedValue
#Column(name = "LOC_CATEGORY_ID")
private Long locCategoryId;
#ManyToOne
#JoinColumn(name = "CATEGORY_ID")
private Category category;
#Column(name = "NAME")
private String name;
#Column(name = "LOCALE")
private String locale;
}
I have two entities:
UnsubscribedPartner for unsubscribed from mailing partners
#Entity
#Table(schema = "mailing", name = "unsubscribed_partner")
public class UnsubscribedPartner {
#Id
#Column(name = "partner_id")
private int partnerId;
#Column(name = "unsubscription_date")
private Date date;
#OneToOne(targetEntity = Partner.class, fetch = FetchType.EAGER)
#JoinColumn(name = "partner_id")
private Partner partner;
//GET, SET
}
Partner partner's class
#Entity
#Table(schema = "partner", name = "partner")
public class Partner {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private int id;
#Column(name = "email")
private String email;
#OneToOne(fetch = FetchType.EAGER, mappedBy = "partner")
private UnsubscribedPartner unsubscribedPartner;
//GET, SET
}
I constructed the following criteria query:
String email;
//...
Criteria criteria = getSession().createCriteria(Partner.class);
if(!(email == null)){
criteria.add(Restrictions.eq("email", email));
}
Criteria unsubscribedCrieria = criteria.createCriteria("unsubscribedPartner", "unsbcr");
unsubscribedCrieria.add(Restrictions.isNull("unsbcr.reason"));
But the result SQL query is
select
count(*) as y0_
from
partner.partner this_
inner join
mailing.unsubscribed_partner unsbcr1_
on this_.id=unsbcr1_.partner_id
where
unsbcr1_.unsubscription_reason_id is null
Inner join is not appropriate here, because the unsubscribed_partner tables may not any partner from the partner table, therefore I need LEFT OUTER JOIN instead. How can I fix that?
The documentation states that createCriteria(String, String) is functionally equivalent to createCriteria(String, String, int) using CriteriaSpecification.INNER_JOIN for the joinType.
So, try with createCriteria("unsubscribedPartner", "unsbcr", CriteriaSpecification.LEFT_JOIN) instead.
I'm using hibernate-core:3.3.1.GA
I have three mappings:
class PlayerAccount:
#Entity
#Table(name = "player_account")
public class PlayerAccount {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private int id;
#ManyToOne(targetEntity = Player.class, fetch = FetchType.EAGER)
#JoinColumn(name="player_id")
private Player player;
//GET, SET
}
class PlayerAchievements:
#Entity
#Table(name="player_achievement")
public class PlayerAchievement {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private int id;
#ManyToOne(targetEntity = Player.class, fetch = FetchType.EAGER)
#JoinColumn(name = "player_id")
private Player player;
//GET, SET
}
and class Player
#Entity
#Table(name = "player")
#Inheritance(strategy = InheritanceType.JOINED)
public class Player {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private int id;
#Column(name = "ps_id")
private String psId;
#Column(name = "login")
private String login;
#Column(name = "password")
private String password;
#Column(name = "email")
private String email;
//GET, SET
}
Issue:
When I try to write a simple criteria query like the following:
Criteria criteria = getSession().createCriteria(PlayerAccount.class);
criteria.add(Restrictions.eq("playerId", playerId));
criteria.list();
The resulting sql-query contains joins have the following view:
SELECT
-- columns to select
FROM player_account this_
LEFT OUTER JOIN player player2_
ON this_.player_id=player2_.id
LEFT OUTER JOIN external_partner_player player2_1_
ON player2_.id=player2_1_.player_id
LEFT OUTER JOIN player_achievement achievemen3_
ON player2_.id=achievemen3_.player_id
WHERE player_id = 2362189
The thing is player_achievements may contain more than one row with the same player_id value. Since we probably have a duplicated result in that criteria query. How to fix that using hibernate whithout the writing an sql-query?
Because the Fetch Types are eager. That means that you always want the entities loaded when you load the main entity.
You can use FetchType.LAZY for best perform. Here explains better: https://howtoprogramwithjava.com/hibernate-eager-vs-lazy-fetch-type/
You need to do SELECT DISTINCT, like this:
criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);