I have 3 entities. Customer, Process and Document.
A Customer has many processes and a process has many documents.
I want to sort customers by document's updateDate.
My entities are like below;
Customer-
#Entity
public class Customer {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
#OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Process> processes = new ArrayList<>();
// getter, setter etc.
}
Process-
#Entity
public class Process {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String type;
#ManyToOne(fetch = FetchType.LAZY)
private Customer customer;
#OneToMany(mappedBy = "process", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<Document> documents = new ArrayList<>();
//getter, setter etc.
}
Document-
#Entity
public class Document {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String note;
private LocalDateTime updateDate;
#ManyToOne(fetch = FetchType.LAZY)
private Process process;
}
I have tried the following specification-
public static Specification<Customer> orderByDocumentUploadDate() {
return (root, query, criteriaBuilder) -> {
ListJoin<Customer, Process> processJoin = root.join(Customer_.processes);
ListJoin<Process, Document> documentJoin = processJoin.join(Process_.documents);
query.orderBy(criteriaBuilder.desc(documentJoin.get(Document_.updateDate)));
query.distinct(true);
return null;
};
}
It gives following error-
ERROR: for SELECT DISTINCT, ORDER BY expressions must appear in select
list
Generated SQL-
select distinct customer0_.id as id1_0_,
customer0_.name as name2_0_
from customer customer0_
inner join
process processes1_ on customer0_.id = processes1_.customer_id
inner join
document documents2_ on processes1_.id = documents2_.process_id
order by documents2_.update_date desc
limit ?
I have also tried by grouping, like below-
public static Specification<Customer> orderByDocumentUploadDate() {
return (root, query, criteriaBuilder) -> {
ListJoin<Customer, Process> processJoin = root.join(Customer_.processes);
ListJoin<Process, Document> documentJoin = processJoin.join(Process_.documents);
query.orderBy(criteriaBuilder.desc(documentJoin.get(Document_.updateDate)));
query.groupBy(root.get(Customer_.id));
return null;
};
}
Then it gave a different error-
ERROR: column "documents2_.update_date" must appear in the GROUP BY
clause or be used in an aggregate function
Generated SQL-
select
customer0_.id as id1_0_,
customer0_.name as name2_0_
from
customer customer0_
inner join
process processes1_
on customer0_.id=processes1_.customer_id
inner join
document documents2_
on processes1_.id=documents2_.process_id
group by
customer0_.id
order by
documents2_.update_date desc limit ?
I could do it by the following sql; max() solved it in below sql-
select customer.* from customer
inner join process p on customer.id = p.customer_id
inner join document d on p.id = d.process_id
group by customer.id
order by max(d.update_date);
But I can't do the same, using the criteria API.
Do you have any suggestion?
This is a conceptual misunderstanding.
First, you have to understand how does inner join works. And this portion is okay in this case: [join process table with document table based on document.process_id = process.id]
Second, you need to sort customers based on the document's update date
Unfortunately, you used group by here. GROUP BY only returns column in which it is grouped by. In this case, it will return only customer_id.
You can use aggregate functions like count(), sum() etc. on grouped data.
When you tried to access update_date, it will throw below error:
ERROR: column "documents2_.update_date" must appear in the GROUP BY clause or be used in an aggregate function
Now, how can we get rid of this?
So first we need to do join to get customer id. After getting customer id, we should group the data by the customer id and then use max() to get max_date of each group(if necessary then minimum)
SELECT
customer_id,
max(date) AS max_date
FROM
document
JOIN process ON process.id = document.process_id
GROUP BY customer_id
It will return a temporary table, that looks something like below:
customer_id
max_date
1
2020-10-24
2
2021-03-15
3
2020-09-24
4
2020-03-15
Using the temporary table, you can now sort customer_id by date
SELECT
customer_id,
max_date
FROM
(SELECT
customer_id,
max(date) AS max_date
FROM
document
JOIN process ON process.id = document.process_id
GROUP BY customer_id) AS pd
ORDER BY max_date DESC
Hope this helps.
Related
If I setup a parent/child relationship with both parent and child having additionalcriteria constraints, and then use #JoinFetch then childs additionalcriteria are ignored.
For example:
TableA.java:
#javax.persistence.Entity
#Table(name = "TABLE_A")
#AdditionalCriteria("this.tableAfield2=:propA")
public class TableA {
#Id
#Column(name = "TABLEAFIELD1")
private String tableAfield1;
#Column(name = "TABLEAFIELD2")
private String tableAfield2;
#JoinColumn(name = "TABLEAFIELD2", referencedColumnName = "TABLEBFIELD1", insertable = false, updatable = false)
#OneToOne(fetch = FetchType.EAGER)
// #JoinFetch(JoinFetchType.OUTER)
private TableB tableAtableB;
}
TableB.java:
#javax.persistence.Entity
#Table(name = "TABLE_B")
#AdditionalCriteria("this.tableBfield2=:propB")
public class TableB {
#Id
#Column(name = "TABLEBFIELD1")
private String tableBfield1;
#Column(name = "TABLEBFIELD2")
private String tableBfield2;
public String getTableBfield1() {
return tableBfield1;
}
public String getTableBfield2() {
return tableBfield2;
}
}
Main:
em.setProperty("propA", "propertyAValue");
em.setProperty("propB", "propertyBValue");
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<TableA> criteriaQuery = cb.createQuery(TableA.class);
Root<TableA> tableA = criteriaQuery.from(TableA.class);
Predicate pred = cb.equal(tableA.get("tableAfield1"), "keyA1");
criteriaQuery.where(pred);
List<TableA> results = em.createQuery(criteriaQuery).getResultList();
With tableA set as per the example (with JoinFetch commented out)
the applications creates 2 SQLs
SELECT TABLEAFIELD1, TABLEAFIELD2 FROM TABLE_A WHERE ((TABLEAFIELD1 = ?) AND (TABLEAFIELD2 = ?))
bind => [keyA1, propertyAValue]
SELECT TABLEBFIELD1, TABLEBFIELD2 FROM TABLE_B WHERE ((TABLEBFIELD1 = ?) AND (TABLEBFIELD2 = ?))
bind => [propertyAValue, propertyBValue]
which is fine, as eclipselink is loading the table_b on demand.
but for our application we need to have a single SQL, as there maybe 1000s of rows and we need a single join.
So, if I put back the #JoinFetch then the sql generated is;
SELECT t1.TABLEAFIELD1, t1.TABLEAFIELD2, t0.TABLEBFIELD1, t0.TABLEBFIELD2 FROM TABLE_A t1 LEFT OUTER JOIN TABLE_B t0 ON (t0.TABLEBFIELD1 = t1.TABLEAFIELD2) WHERE ((t1.TABLEAFIELD1 = ?) AND (t1.TABLEAFIELD2 = ?))
bind => [keyA1, propertyAValue]
the additionalCriteria from TableB is not added (there is no t0.tableBField1=? (propertyBValue) )
Any suggestions? Its driving me mad.
Many thanks
For completeness here are the tables
create table TABLE_A (
TABLEAFIELD1 varchar2(20),
TABLEAFIELD2 varchar2(30),
CONSTRAINT tableApk PRIMARY KEY (TABLEAFIELD1)
) ;
create table TABLE_B (
TABLEBFIELD1 varchar2(20),
TABLEBFIELD2 varchar2(30),
CONSTRAINT tableBpk PRIMARY KEY (TABLEBFIELD1)
) ;
insert into TABLE_A (TABLEAFIELD1,TABLEAFIELD2) values ('keyA1','propertyAValue');
insert into TABLE_A (TABLEAFIELD1,TABLEAFIELD2) values ('keyA2','propertyAValue');
insert into TABLE_A (TABLEAFIELD1,TABLEAFIELD2) values ('keyA3','random');
insert into TABLE_B (TABLEBFIELD1,TABLEBFIELD2) values ('propertyAValue','propertyBValue');
So this is a long term bug with eclipselink and doesn't look like it will be fixed.
The solution was to change
#JoinFetch(JoinFetchType.OUTER)
to
#BatchFetch(BatchFetchType.JOIN)
This doesn't exactly have the result I was hoping for, originally wanted the generated sql to include an OUTER JOIN,
but BatchFetch results in only 2 SQLs, one to get the Table_A items, then another to fetch all the Table_B items (including the additionalcriteria requirements)
I have Spring Data method like this:
List<RegionBasics> findTop10ByRegionMappingsActiveTrue();
What I expect is that it will find top 10 records from db in one query but What I see in logs is (I didn't paste whole logs in order to keep this readable but this select query is invoke 10 times):
select regionmapp0_.id as id1_2_1_, regionmapp0_.is_active as is_activ2_2_1_, regionmapp0_.region_basic_id as region_b3_2_1_, regionbasi1_.id as id1_1_0_, regionbasi1_.hotel_count as hotel_co2_1_0_, regionbasi1_.name_long as name_lon3_1_0_, regionbasi1_.name as name4_1_0_, regionbasi1_.type as type5_1_0_ from region_mappings regionmapp0_ left outer join region_basics regionbasi1_ on regionmapp0_.region_basic_id=regionbasi1_.id where regionmapp0_.region_basic_id=?
How can I ensure that this method will hit db only once (instead of 10)?
My Model:
#Data
#NoArgsConstructor
#Entity
#Table(name = "region_basics")
public class RegionBasics {
#Id
Integer id;
#Column
String type;
#Column
String name;
#Column(name = "name_long")
String longName;
#Column(name = "hotel_count")
Integer hotelCount;
#OneToOne(mappedBy="regionBasics")
RegionMappings regionMappings;
}
I think you should join fetching the RegionMappings:
Like this: #Query("SELECT rb FROM RegionBasics r JOIN FETCH r.regionMappings rm WHERE rm.active=true")
With pageable parameter new PageRequest(0,10)
I thought I understood hibernate's fetching strategies, but it seems I was wrong.
So, I have an namedNativeQuery:
#NamedNativeQueries({
#NamedNativeQuery(
name = "getTest",
resultClass = ArticleOnDate.class,
query = "SELECT `a`.`id` AS `article_id`, `a`.`name` AS `name`, `b`.`price` AS `price` FROM article a LEFT JOIN price b ON (a.id = b.article_id) WHERE a.date <= :date"
)
})
#Entity()
#Immutable
public class ArtikelOnDate implements Serializable {
#Id
#OneToOne
#JoinColumn(name = "article_id")
private Article article;
...
}
Then I call it:
Query query = session.getNamedQuery("getTest").setDate("date", date);
List<ArticleOnDate> list = (List<ArticleOnDate>) query.list();
The query returns thousand of entities... Well, ok, but after that query hibernate queries thousand other queries:
Hibernate:
select
article0_.id as id1_0_0_,
article0_.bereich as name2_0_0_,
price1_.price as price1_14_1_
from
article artikel0_
where
artikel0_.id=?
Ok, that's logic, because the #OneToOne relation is fetched eagerly. I don't want to fetch it lazy, so I want a batch fetching strategy.
I tried to annotate the Article property but it didn't work:
#Id
#OneToOne
#JoinColumn(name = "article_id")
#BatchSize(size=100)
private Article article;
So what can I do to fetch the relation in a batch?
I have the following two different HQL statements.
My Data Structure looks like this:
User
#Entity (name = "User")
public class User
{
#Id
#GeneratedValue
#Column (name = "id")
private int id;
#Column (name = "user_name")
private String username;
#Column (name = "password")
private String password;
#Column (name = "enabled")
private boolean enabled;
#ManyToMany (targetEntity = Role.class, cascade =
{
CascadeType.ALL
})
#JoinTable (name = "user_role", joinColumns =
{
#JoinColumn (name = "user_id")
}, inverseJoinColumns =
{
#JoinColumn (name = "role_id")
})
private Set<Role> roles;
/* getters and setters)
}
To cut it short the only difference between the two queries is that one is ASC the other is DESC
#NamedQuery (name = "user.getUsersOrderByRoleAsc",
query = "FROM User as u left outer join u.roles roles WHERE u.username like :username ORDER BY roles.name ASC"),
#NamedQuery (name = "user.getUsersOrderByRoleDesc",
query = "FROM User as u left outer join u.roles roles WHERE u.username like :username ORDER BY roles.name DESC"),
The query for ASC returns: A list of Users -> As I would expect.
The query of DESC returns: An List of Object[], and in each object the [0] is the User, while the [1] is just another null object.
That does not make any sense to me. How can simply changing ASC to DESC change the structure of the result set ?
I am using Hibernate 4.3.6.Final.
The fastest way to determin, what went wrong is to set the show_sql flag to true in you hibernate configuration file. This will log every rendered query.
See Hibernate show real SQL
Probably there is some Hibernate bug you bumped into, but because you are join fetching a one to many children collections, it's safer to use distinct as well:
#NamedQuery (name = "user.getUsersOrderByRoleAsc",
query = "select distinct u FROM User as u left outer join u.roles roles WHERE u.username like :username ORDER BY roles.name ASC"),
#NamedQuery (name = "user.getUsersOrderByRoleDesc",
query = "select distinct u FROM User as u left outer join u.roles roles WHERE u.username like :username ORDER BY roles.name DESC")
I try to convert a sql query to Criteria API without success so far. I can create two separate queries which return the values I need, but I don't know how to combine them in a single query.
Here is the sql statement which works:
select company.*, ticketcount.counter from company
join
(select company, COUNT(*) as counter from ticket where state<16 group by company) ticketcount
on company.compid = ticketcount.company;
This Criteria query returns the inner query results:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<intCompany> qTicket = cb.createQuery(intCompany.class);
Root<Ticket> from = qTicket.from(Ticket.class);
Path groupBy = from.get("company");
Predicate state = cb.notEqual(from.<State>get("state"), getStateById(16));
qTicket.select(cb.construct(
intCompany.class, cb.count(from),from.<Company>get("company")))
.where(state).groupBy(groupBy);
em.createQuery(qTicket).getResultList();
In the application I defined a small wrapper/helper class:
public class intCompany{
public Company comp;
public Long opentickets;
public intCompany(Long opentickets,Company comp){
this.comp = comp;
this.opentickets = opentickets;
}
public intCompany(){
}
}
So does anyone has an idea how to get this working?
Update
Thank you. I changed my criteria query as you suggested. I just had to add a loop at the end to get the information I wanted.
List<intCompany> result = em.createQuery(cq).getResultList();
List<Company> cresult = new ArrayList();
for(intCompany ic: result){
ic.comp.setOpentickets(ic.opentickets.intValue());
cresult.add(ic.comp);
}
return cresult;
Maybe it is just not possible to convert the original sql to Criteria API.
Another update
I figured out I had to change the original sql expression to
select company.*, ticketcount.counter from company
left join
(select company, COUNT(*) as counter from ticket where state<16 group by company) ticketcount
on company.compid = ticketcount.company;
Otherwise I do not get companies with no entries in the ticket table.
So are there any other suggestions?
You have almost everything done.
//---//
CriteriaBuilder cb = em.getCriteriaBuilder();
//Your Wrapper class constructor must match with multiselect arguments
CriteriaQuery<IntCompany> cq = cb.createQuery(IntCompany.class);
//Main table
final Root<Ticket> fromTicket= cq.from(Ticket.class);
//Join defined in Ticket Entity
final Path company = fromTicket.get("company");
//Data to select
cq.multiselect(cb.count(from), company);
//Grouping
cq.groupBy(company);
//Restrictions (I don't really understand what you're querying)
Predicate p = cb.lessThan(fromTicket.get("state"), 16);
//You can add more restrictions
// p = cb.and/or(p, ...);
cq.where(p);
List<IntCompany> results = entityManager.createQuery(cq).getResultList();
This should work as expected.
I had similar problem. My solution was to use left outer joins.
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Entity> query = cb.createQuery(Entity.class);
Root<Entity> root = query.from(Entity.class);
Join<Entity,ChildEntity> join = root.join(Entity_.children, JoinType.LEFT);
query.groupBy(root.get( Entity_.id ));
query.select(
cb.construct(
EntityDTO.class,
root.get( Entity_.id ),
root.get( Entity_.name ),
cb.count(join)
));
This JoinType.LEFT guarantees that you will get Entity records (companies) even if it doesn't have any child entities (tickets).
Entity class:
#Entity
public class Entity {
...
#OneToMany(targetEntity = ChildEntity.class, mappedBy = "parent", fetch = FetchType.LAZY, orphanRemoval = false)
private Set<ChildEntity> objects;
...
}
Static model:
#StaticMetamodel( Entity.class )
public class Entity_ {
public static volatile SingularAttribute<Entity, Long> id;
public static volatile SingularAttribute<Entity, String> name;
...
public static volatile SetAttribute<Entity, ChildEntity> objects;
}