Have been learning Hibernate, Spring and JPA the last week and got stuck on trying to create a Criteria for the following scenario:
Let's say I have 2 tables:
Game
id
PlayedGame
id
account_ref -> reference to some account table
game_id -> reference to the game
Entity mapping:
Game {
#id
Long id;
#OneToMany(mappedBy = "game")
Set<Player> players;
}
PlayedGame {
#id
Long id;
Long account_ref;
#ManyToOne
#JoinColumn(name = "game_id", referencedColumnName = "id")
Game game;
}
Now I want to query for the following scenario:
- I want to find all games a specific player (P) has played where player (A) was a part of it. More specifically that two people belonging to the same game
In SQL this could be done with something like (which the query should be):
SELECT DISTINCT p1.* FROM Player as p1
INNER JOIN Player as p2 ON p1.game_id=p2.game_id
WHERE p1.account_ref=P AND p2.account_ref=A
Can this be done neatly with Criteria in Hibernate?
Maybe possible with Hibernate's Criteria API, but not straight forward.
A simple case would require the same association path to be join twice (one for A and one for P):
Criteria gameCriteria = ((HibernateEntityManager) em).getSession().createCriteria(Game.class);
Criteria playedGamesOfACriteria = gameCriteria.createCriteria("playedGames", "pga");
Criteria accountOfACriteria = playedGamesOfACriteria.createCriteria("account", "a");
accountOfACriteria.add(Restrictions.idEq(a.id));
Criteria playedGamesOfPCriteria = gameCriteria.createCriteria("playedGames", "pgp");
Criteria accountOfPCriteria = playedGamesOfPCriteria.createCriteria("account", "p");
accountOfPCriteria.add(Restrictions.idEq(p.id));
return gameCriteria.list();
This won't work due to HHH-879.
But you can use a JPA query:
Query q = em.createQuery(
"select g "
+ "from Game g "
+ "join g.playedGames pga "
+ "join pga.account a "
+ "join g.playedGames pgp "
+ "join pgp.account p "
+ "where a = ?1 and p = ?2"
);
q.setParameter(1, a);
q.setParameter(2, p);
return q.getResultList();
This is even less code.
Also: Some consider the Criteria API to be deprecated.
Related
I have a Model called CommunityProfile. This model contains two child relationships; player (type User), and rank (type Rank).
The default spring boot JPA-generated query is taking approximately 9s to fetch 200 records, which is rather slow. By using the following MySQL query, I can return the data I need rather quickly:
SELECT cp.*, r.*, u.* FROM community_profiles cp
LEFT JOIN users u ON cp.player_id = u.id
LEFT JOIN ranks r ON cp.rank_id = r.id
WHERE cp.community_id = 1
How can I make my repository map the results to their correct Objects/Models?
I have tried using a non-native query, like this:
#Query("SELECT cp FROM CommunityProfile cp " +
"LEFT JOIN FETCH cp.player u " +
"LEFT JOIN FETCH cp.rank r " +
"WHERE cp.communityId = :communityId")
List<CommunityProfile> findByCommunityIdWithJoin(#Param("communityId") Integer communityId);
However, this is still quite slow in comparison, resulting in an 800-900ms response. For comparison, my current Laravel application can return the same data in a 400-ms cold start.
Any tips are appreciated, thank you
==UPDATE==
After trying the suggested #Index annotation, I still don't really see any performance gains. Did I implement correctly?
#Entity
#Table(name = "community_profiles", indexes = #Index(name = "cp_ci_idx", columnList = "community_id"))
public class CommunityProfile {
If your JPA query is working, and you are just asking about performance, you may add the following index:
CREATE INDEX idx ON community_profiles(community_id);
This index should allow MySQL to filter off records which are not part of the result set.
From JPA itself you may use:
#Table(indexes = #Index(name = "idx", columnList = "community_id"))
public class CommunityProfile {
// ...
}
Have you tried EntityManager
#PersistenceContext(type = PersistenceContextType.TRANSACTION)
private EntityManager entityManager;
List<CommunityProfile> findByCommunityIdWithJoin(Integer communityId){
String query = ""SELECT cp FROM CommunityProfile cp " +
"LEFT JOIN FETCH cp.player u " +
"LEFT JOIN FETCH cp.rank r " +
"WHERE cp.communityId = :communityId"
List<CommunityProfile> list = entityManager.createNativeQuery(query, CommunityProfile.class)
.setParameter("communityId",communityId)
.getResultList();
entityManager.clear();
return list
}
Once I used this kind of native query inside loop and it constantly returned cash values bu entityManagaer.clear() clears cash. This is for info only)
Or create an Index on specific columns when you are defining entity classes like:
#Table(indexes = {
#Index(columnList = "firstName"),
#Index(name = "fn_index", columnList = "firstName"),
#Index(name = "mulitIndex1", columnList = "firstName, lastName")
...
}
For Non-entity #Index you can check documentation
I have a JPA query written like this:
public interface MyDataRepository extends CrudRepository<MyData, String> {
#Query("select md " +
"from MyData md " +
"join fetch md.child c " +
"where c.date = :date")
List<MyData> getMyDataOfDate(#NotNull LocalDate date);
#Query("select md " +
"from MyData md " +
"join fetch md.child c " +
"where c.name = :name")
List<MyData> getMyDataOfName(#NotNull String name);
#Query("select md " +
"from MyData md " +
"join fetch md.child c " +
"where md.type = :type")
List<MyData> getMyDataOfType(#NotNull String type);
}
Class MyData and Child are defined as:
class MyData {
String id;
String type;
#ManyToOne
#JoinColumn(name = "CHILD_ID", referencedColumnName = "ID", nullable = false, updatable = false)
Child child;
}
class Child {
String id;
String name;
LocalDate date;
}
The problem is that whenever I call the getMyDataOfDate method or getMyDataOfName method, they always return ALL rows rather than the rows that matches the where condition, as if the where clause never exists.
However, the getMyDataOfType method works fine. The difference of this method is that the where condition is on a property of md, not c.
What did I do wrong?
JPA does not allow filtering on join fetches. Reasons being is that when you specify a join fetch, you are telling JPA to fetch the parent and all its children defined by that relationship in the managed entities it returns. If filtering were allowed, the list of children, the relationship in the parent, might not reflect what is actually in the database. Take the case of Parent with many children
"Select parents from Parent p fetch join p.children c where c.firstName = 'Bob'"
For such a query, when you get a list of parents and calling getChildren on them, do you expect to see all their children or a list that only contains children named Bob? If the later (which is the only way to do so), how should JPA handle changes to a parents children list, and know what to do with the not-fetched children?
This is why JPA doesn't allow filtering over fetch joins, and they restrict it across all relationships to be consistent. If you want the parents who have children with the firstName of 'Bob', it would look like:
"Select parents from Parent p join p.children c where c.firstName = 'Bob'"
Every parent returned will be a complete representation of its state in the database based on its mappings; so accessing parent.getChildren will return the current state of its children list and not something affected by the way it was fetched.
I'm using Quarkus and Hibernate / Panache.
For this example, I have 3 tables (table_a, table_b, table_c) that I am joining together using a native query. In the project I'm working on, it's around 5 JOIN tables to retrieve the information I'm looking for.
table_b is purely a mapping / join table for table_a and table_c:
SELECT
a.id,
a.name,
c.login_date
FROM
table_a a
JOIN table_b b ON b.a_id = a.id
JOIN table_c c ON b.c_id = c.id
WHERE
c.login_date > '01-MAY-21'
I'm porting the above to HQL. I've mapped all my #Entity classes with their respective #Table, along with their #Column names. We're good in that department.
SELECT
a.id,
a.name,
c.loginDate
FROM
TableA a
JOIN TableA b ON b.aId = a.id
JOIN TableB c ON b.cId = c.id
WHERE
c.loginDate > '01-MAY-21'
I'm only looking for name and login_date. There is a bunch of other information stored in table_a and table_c that I don't want for this specific query. So I created an entity for this call:
#Entity
#IdClass(LoginDetailsPk.class)
#NamedQuery(
name = "LoginDetails.findFromDate",
query = "FROM TableA a " +
"JOIN TableA b ON b.aId = a.id " +
"JOIN TableB c ON b.cId = c.id " +
"WHERE c.loginDate > '01-MAY-21'"
)
public class LoginDetails extends PanacheEntityBase {
#Id
private int id;
#Id
private String name;
#Id
private String loginDate;
public static List<LoginDetails> findFromDate(String fromDate) {
// Eventually pass fromDate into find()
return find("#LoginDetails.findFromDate").list();
}
}
I'm having a hard time trying to understand why the return even works. When I invoke LoginDetails.findFromDate(...) and store it in a List<LoginDetails>, it works fine. However, when I try to access the list, I get a ClassCastException error.
List<LoginDetails> details = LoginDetails.findFromDate(null);
for(LoginDetails detail : details) { // <------ Throws a class cast exception
//...
}
After debugging, I'm noticing that generic type stored in my List isn't even my LoginDetails class; rather, it's an array of objects (List<Object[]>) with all my #Entities and the irrelevant information I'm not looking for.
I'm lost. Would it make more sense to move back to a native query?
Your HQL is creating a Object[] for every row in the result, because you are not specifying any SELECT, and by default all the objects in the FROM clause are included in that Object array. If you want to return a LoginDetails object you need to create a constructor with all the attributes:
public LoginDetails(int id, String name, String loginDate) {
this.id = id;
this.name = name;
this.loginDate = loginDate;
}
And then change the query to:
query = "SELECT new LoginDetails(a.id, a.name, c.loginDate) "
"FROM TableA a " +
"JOIN TableA b ON b.aId = a.id " +
"JOIN TableB c ON b.cId = c.id " +
"WHERE c.loginDate > '01-MAY-21'"
See https://docs.jboss.org/hibernate/core/3.5/reference/en/html/queryhql.html#queryhql-select
I have a HQL query with a JOIN but the where clause (instrPrice.date BETWEEN :dateFrom AND :dateTo ) on the joined entity doesn't work. The query always returns all the records of instrumentPrice instead of limiting the result by the dates.
NamedQuery
#NamedQuery(name = "findAllPrices",
query = "SELECT DISTINCT taPat FROM TaPatternInstrument taPat "
+ "LEFT JOIN FETCH taPat.instrument instr "
+ "LEFT JOIN instr.instrumentPriceList instrPrice "
+ "WHERE taPat.id = :taPatternInstrumentId "
+ "AND instrPrice.date BETWEEN :dateFrom AND :dateTo ")
Service which calls the Query
public TaPatternInstrument findAllPrices(int taPatternInstrumentId, LocalDate dateFrom, LocalDate dateTo) {
TypedQuery<TaPatternInstrument> typedQuery = createNamedQuery("findAllPrices",
TaPatternInstrument.class);
typedQuery.setParameter("taPatternInstrumentId", taPatternInstrumentId);
typedQuery.setParameter("dateFrom", dateFrom);
typedQuery.setParameter("dateTo", dateTo);
return typedQuery.getSingleResult();
}
Entities
public abstract class BaseEntity implements Serializable {
#Id
#Column(name = "id")
#GeneratedValue(strategy =
GenerationType.IDENTITY)
protected int id; ...
}
public class TaPatternInstrument extends BaseEntity {
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "instrument_id", nullable = false, foreignKey = #ForeignKey(name =
"tapatterninstrument_instrument_fk"))
private Instrument instrument;
}
public class Instrument extends BaseEntity {
#OneToMany(mappedBy = "instrument", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private List<InstrumentPrice> instrumentPriceList;
}
Generated SQL
SELECT DISTINCT tapatterni0_.id AS id1_34_0_,
...
FROM tapatterninstrument tapatterni0_
LEFT OUTER JOIN instrument instrument1_
ON tapatterni0_.instrument_id = instrument1_.id
LEFT OUTER JOIN instrumentprice instrument2_
ON instrument1_.id = instrument2_.instrument_id
WHERE tapatterni0_.id = ?
AND ( instrument2_.date BETWEEN ? AND ? )
The solution is to add a FETCH on instrumentPriceList : LEFT JOIN FETCH instr.instrumentPriceList instrPrice
#NamedQuery(name = "findAllPrices",
query = "SELECT DISTINCT taPat FROM TaPatternInstrument taPat "
+ "LEFT JOIN FETCH taPat.instrument instr "
+ "LEFT JOIN FETCH instr.instrumentPriceList instrPrice "
+ "LEFT JOIN taPat.taPatternInstrumentPriceList taPatpr "
+ "WHERE taPat.id = :taPatternInstrumentId "
+ "AND instrPrice.date BETWEEN :dateFrom AND :dateTo ")
The FETCH forces Hibernate to retrieve the Entity (InstrumentPrice) immediately at the first DB request. And thus the where clause is taken into account.
Without FETCH, the Entity InstrumentPrice is only retrieved from the DB when the method getInstrumentPriceList of the Entity Instrument is called (an additional call to the DB is performed). And with this additional call to the DB, the where clause is not taken into account anymore, thus retrieving all records from Entity instrumentPrice.
I have an #Entity class Company with several attributes, referencing a companies Table in my db. One of them represents a Map companyProperties where the companies table is extended by a company_properties table, and the properties are saved in key-value format.
#Entity
#Table(name = "companies")
public class Company extends AbstractEntity {
private static final String TABLE_NAME = "companies";
#Id
#GeneratedValue(generator = TABLE_NAME + SEQUENCE_SUFFIX)
#SequenceGenerator(name = TABLE_NAME + SEQUENCE_SUFFIX, sequenceName = TABLE_NAME + SEQUENCE_SUFFIX, allocationSize = SEQUENCE_ALLOCATION_SIZE)
private Long id;
//some attributes
#ElementCollection
#CollectionTable(name = "company_properties", joinColumns = #JoinColumn(name = "companyid"))
#MapKeyColumn(name = "propname")
#Column(name = "propvalue")
private Map<String, String> companyProperties;
//getters and setters
}
The entity manager is able to perform properly find clauses
Company company = entityManager.find(Company.class, companyId);
However, I am not able to perform JPQL Queries in this entity and retrieve the Map accordingly. Since the object is big, I just need to select some of the attributes in my entity class. I also do not want to filter by companyProperties but to retrieve all of them coming with the proper assigned companyid Foreign Key. What I have tried to do is the following:
TypedQuery<Company> query = entityManager.createQuery("SELECT c.id, c.name, c.companyProperties " +
"FROM Company as c where c.id = :id", Company.class);
query.setParameter("id", companyId);
Company result = query.getSingleResult();
The error I get is:
java.lang.IllegalArgumentException: An exception occurred while creating a query in EntityManager:
Exception Description: Problem compiling [SELECT c.id, c.name, c.companyProperties FROM Company as c where c.id = :id]. [21, 40] The state field path 'c.companyProperties' cannot be resolved to a collection type.
org.eclipse.persistence.internal.jpa.EntityManagerImpl.createQuery(EntityManagerImpl.java:1616)
org.eclipse.persistence.internal.jpa.EntityManagerImpl.createQuery(EntityManagerImpl.java:1636)
com.sun.enterprise.container.common.impl.EntityManagerWrapper.createQuery(EntityManagerWrapper.java:476)
Trying to do it with joins (the furthest point I got was with
Query query = entityManager.createQuery("SELECT c.id, c.name, p " +
"FROM Company c LEFT JOIN c.companyProperties p where c.id = :id");
does not give me either the correct results (it only returns the value of the property and not a list of them with key-value).
How can I define the right query to do this?
Your JPA syntax looks off to me. In your first query you were selecting individual fields in the Company entity. But this isn't how JPA works; when you query you get back the entire object, with which you can access any field you want. I propose the following code instead:
TypedQuery<Company> query = entityManager.createQuery("from Company as c where c.id = :id", Company.class);
query.setParameter("id", companyId);
Company result = query.getSingleResult();
Similarly, for the second join query I suggest the following code:
Query query = entityManager.createQuery("SELECT c" +
"FROM Company c LEFT JOIN c.companyProperties p WHERE c.id = :id");
query.setParameter("id", companyId);
List<Company> companies = query.getResultList();
The reason why only select a Company and not a property entity is that properties would appear as a collection inside the Company class. Assuming a one to many exists between companies and properties, you could access the propeties from each Company entity.
You are expecting to get a complete Company object when doing select only on particular fields, which is not possible. If you really want to save some memory (which in most cases would not be that much of a success) and select only some field, then you should expect a List<Object[]>:
List<Object[]> results = entityManager.createQuery("SELECT c.id, c.name, p " +
"FROM Company c LEFT JOIN c.companyProperties p where c.id = :id")
.setParameter("id", companyId)
.getResultList();
Here the results will contain a single array of the selected fields. You can use getSingleResult, but be aware that it will throw an exception if no results were found.