I am playing around with JPA and other Java EE 6 stuff, but now I am facing some problems with a types query using the criteria Builder.
The business case for my poc is a twitter clone, so I have users which has a list of subscribers and subscriptions and I got Tweets.
#Entity
public class Tweet {
#Id #GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
#ManyToOne
private TwitterUser author;
#ManyToOne
private TwitterUser receiver;
#Temporal(TemporalType.TIMESTAMP)
private Date datetime;
private String message;
}
#Entity
public class TwitterUser {
#Id #GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
private String username;
private String displayname;
private String password;
private String email;
#Temporal(TemporalType.TIMESTAMP)
private Date since;
#ManyToMany(cascade=CascadeType.ALL)
private List<TwitterUser> subscriptions;
#ManyToMany(mappedBy = "subscriptions")
private List<TwitterUser> subscribers;
}
The datamodel generated by JPA consists of 3 tables: Tweet, TwitterUser and TwitterUser_TwitterUser. Everything's fine so far. Basic queries do work well.
Now I want to select all Tweets for a timeline, what means all tweets of users, who are in the list of subscriptions of the current logged in user. I thought of a solution by using a subselect, so my sql would look like:
select * from TWEET where author_id in (
select subscriptions_ID from TWITTERUSER_TWITTERUSER where subscribers_ID = ?loggedInUserId);
This query works well, but I have no idea how to write it down using the CriteriaBuilder and generics. I don't even get a compilable piece of code that could fail at some other point, because I am not sure what the type of the subquery should be.
I was searching through lots of examples now, but they are mostly just using raw-types or using the subquery for retrieving the element of the lefthand-side of the in-clause.
Maybe I miss the wood for trees but I am really perplexed here. :(
In JPQL, one way of expressing this would be something akin to the following:
SELECT
tweet
FROM
Tweet tweet JOIN tweet.author author
WHERE
EXISTS (
SELECT
user
FROM
TwitterUser user JOIN user.subscriptions subscriber
WHERE
user = :loggedinUser AND
subscriber = author
)
As Criterias following the JPQL model almost directly, maybe this gets you started.
So thanks again to you guys helping me. I finally managed to find a solution with your input and it looks as follows:
public List<Tweet> findAllForSubscriber(TwitterUser user) {
// Select tweets
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Tweet> cq = cb.createQuery(Tweet.class);
Root<Tweet> tweet = cq.from(Tweet.class);
cq.select(Tweet);
// Select subscribers of user
Subquery<Long> sq = cq.subquery(Long.class);
Root<TwitterUser> twitterUser = sq.from(TwitterUser.class);
Join<TwitterUser, TwitterUser> subscriptions = twitterUser.join(TwitterUser_.subscriptions);
sq.select(subscriptions.get(TwitterUser_.id));
sq.where(cb.equal(twitterUser, user));
// Where authorId in list of subscribers
cq.where(cb.in(tweet.get(Tweet_.author).get(TwitterUser_.id)).value(sq));
//
return em.createQuery(cq).getResultList();
}
My drawbacks were:
The Criteria API is just able to
do a compare of objects within an
equal() but not within a in()
statement. But I did not get a
helpful exception, all that was
generated was a wrong query (eclipselink & derby) :(
The in() statement
takes the lefthand-side of the in
statement, the value statement takes
the list of possibilities, the
wording confused me.
I cannot simply access the ids
of all subscriptions, I have to use a
join. It's quite clear if you think
about the table structure but I
missed that when simply thinking of
my objects.
P.S.: My solution totally misses to select the tweets of the user himself! ^^
There is a similar Criteria query example here,
http://en.wikibooks.org/wiki/Java_Persistence/Querying#Subselect.2C_querying_all_of_a_ManyToMany_relationship
Related
I want to find the applications that has no documents and I want to do it using hibernate criteria. Once I use createAlias in criteria, I only find the tables with associations. Does anyone have idea how can I do so or is it even possible?
Application.java
private int appId;
private String lname;
private String fname;
//getters setter generated
Document.java
private int appId;
private int docId;
//getters setters generated
Here is what my criteria looks like
Criteria criteria = session.createCriteria(Application.class, "application");
criteria.createAlias("application.appId","document");
if (looking for no association)
{
criteria.add(Restrictions.isNull("document.appId"));
}
Basically with the way I am setting criteria, I am creating inner join so the restriction condition is not working.
Try this:
Criteria criteria = session.createCriteria(Application.class);
criteria.createAlias("document");
if (looking for no association) {
criteria.add(Restrictions.isNull("document.appId"));
}
Firstly you create a criteria based on Application class. Secondly you assign the alias you want to and finally you apply the desired restriction.
Specifying join type helped me resolve the issue
Criteria criteria = session.createCriteria(Application.class);
criteria.createAlias("document",Criteria.LEFT_JOIN);
if (looking for no association) {
criteria.add(Restrictions.isNull("document.appId"));
}
JSF application with hibernate
Is there a way to use a join to filter the results returned by criteria list?
Example: i have 2 tables. orders and customers.
#Entity(name = "order")
public class Order
{
#Id
private int id;
private String billingCustomerId;
private String shippingCustomerId;
private Date orderDate;
....
}
#Entity(name = "customer")
public class Customer
{
#Id
private String id;
private String name;
private String emailAddress
....
}
I need to return all orders for customers that are missing an email address and all orders that the order.billingCustomerId = null and order.shippingCustomerId = null.
The customer could match on the billingCustomerId or shippingCustomerId.
The SQL I would use
select o.* from order as o
LEFT join customer as c1 on o.billingCustomerId = c1.id
LEFT join customer as c2 on o.shippingCustomerId= c2.id
where (o.billingCustomerId is null and o.shippingCustomerId is null) or
(o.billingCustomerId is not null and c1.emailAddress is null) or
(o.shippingCustomerIdis not null and c2.emailAddress is null)
Hibernate Criteria
Criteria criteria1 = session.createCriteria(Order.class);
criteria.add(Restrictions.and(Restrictions.isNull("billingCustomerId"),
Restrictions.isNull("shippingCustomerId"));
List<Order> = criteria.list();
This will return the list of orders that billing /shipping customer = null.
How can i change the criteria to also include the orders for customers with missing email addresses?
Disjunction disjunciton = Restrictions.disjunction();
Criteria criteria = session.createCriteria(Order.class);
disjunciton.add(Restrictions.and(Restrictions.isNull("billingCustomerId"),
Restrictions.isNull("shippingCustomerId")));
disjunciton.add(...
...)
criteria.add(disjunciton);
List<Order> = criteria.list();
I have not been able to find examples of joining on a column, but only where the table have a common key.
I asked this question: Hibernate trouble getting composite key to work and discovered Hibernate can only create a join on columns that were created by relating 2 objects. I am going to add more to my answer to give more useful information but the best alternative you your case is to do a Session.createSQLQuery() using the query you showed above. Then before running the query put Query.addEntity(Order.class).addEntity(Customer.class). As long as your query returns the correct rows to fill out the Java objects correctly, Hibernate can populate them automatically. If that doesn't work you can still retrieve the data and populate it manually yourself.
I know there's a lot of questions about this but none of the solutions helped me.
I'm using PrimeFaces to build a lazy loadind datatable. That means that this datatable list is a LazyDataModel list, and I had to develop a LazyDataModel implementation where I overrode the load method. All of this can be learned from PrimeFaces showcase, and it works fine for most of cases, when the datable uses just one table from the database (which is not my case).
Now, I have two entities:
#Entity
#Table(name="UNIVERSITY")
public class University {
#Id
#Column(name="ID_UNIVERSITY")
#GeneratedValue(strategy = GenerationType.AUTO ,generator="SQ_UNIVERSITY")
#SequenceGenerator(name="SQ_UNIVERSITY", sequenceName="SQ_UNIVERSITY")
private Long idUniversity;
#Column(name="NAME")
private String name;
#ManyToOne
#JoinColumn(name="ID_DIRECTOR")
private Director director;
#OneToMany(fetch=FetchType.EAGER, mappedBy = "university")
private Set<Students> students = new HashSet<Students>(0);
...
public int getStudentsQty(){
return this.students.size();
}
...
Where I'll use the getStudentsQty() method to fill one column from my datatable. And here's the Students entity:
#Entity
#Table(name="STUDENTS")
public class Students
{
#Id
#Column(name="ID_STUDENTS")
#GeneratedValue(strategy = GenerationType.AUTO ,generator="SQ_STUDENTS")
#SequenceGenerator(name="SQ_STUDENTS", sequenceName="SQ_STUDENTS")
private Long idStudent;
#ManyToOne
#JoinColumn(name="ID_UNIVERSITY")
private University student;
#Column(name="NAME")
private String name;
...
Here is the search method that my load implementation will use:
public List<University> find(int startingAt, int maxPerPage,
final String sortField, final SortOrder sortOrder, Map<String, String> filters) {
session = HibernateUtil.getSession();
Criteria criteria =session.createCriteria(University.class);
List<String> aliases = new ArrayList<String>();
if(maxPerPage > 0){
criteria.setMaxResults(maxPerPage);
}
criteria.setFirstResult(startingAt);
addFiltersToCriteria(filters, criteria, aliases);
Order order = Order.desc("name");
if(sortField != null && !sortField.isEmpty()){
if(sortField.contains(".")){
String first = (sortField.split("\\."))[0];
if(!aliases.contains(first)){
criteria.createAlias(first, first);
aliases.add(first);
}
}
if(sortOrder.equals(SortOrder.ASCENDING)){
order = Order.asc(sortField);
}
else if(sortOrder.equals(SortOrder.DESCENDING)){
order = Order.desc(sortField);
}
}
criteria.addOrder(order);
return (List<University>) criteria.list();
}
And now, my problem. If I use FetchType.LAZY, everything works fine, but the performance is terrible, so I wish to use EAGER. If I use EAGER the results will come duplicated as expected and explained here. I tried to implement equals() and hashCode() methods in the University entity to use a LinkedHashSet as suggested in the last link but it didn't worked I don't know how. I also tried to use DISTINCT with Criteria but it doesn't work because of the addOrder that I use, where it asks for joins.
So, I found this other suggestion which worked perfectly. Basically the solution is to do a second Criteria query, searching only for Universities with ID included in the original search. Like this:
private List<University> removeDuplicates(Order order,
List<University> universities) {
Criteria criteria;
List<University> distinct = null;
if(universities.size() > 0){
Set<Long> idlist = new HashSet<Long>();
for(University univ: universities){
idlist.add(univ.getIdUniversity());
}
criteria = session.createCriteria(University.class);
criteria.add(Restrictions.in("id", idlist)) ;
distinct = (List<University>) criteria.list();
return distinct;
}
else{
return universities;
}
}
So it will bring, say, the first 100 lines for my lazy loadind pagination datatable. In the first Criteria search they will be sorted for the first page, and the same 100 correct rows will be present after my second Criteria search, but now they will be unsorted. It's the correct rows for the first page, but unsorted inside the first page. I cant use "addOder" in the second Criteria or else they will come duplicated.
And the strangest thing: if I try to sort the results with Collections.sort the results will be duplicated!!! How?? How can I order my result after all?
Thanks!!
EDIT: the students count is just an example, I'll need in another scenarios get information inside each associated entity.
If I understand correctly you are outputting a table listing universities and you want to show the number of students for each university. If so, loading x000 student records into memory just to get a count is crazy (regardless of whether you do it eagerly or lazily).
Try one of the following:
One
rather than loading the associated students to get the count use Hibernates #Formula functionality and add a derived property to you University entity.
#Formula(value = "select count(*) from students where university_id = ?")
private int studentCount;
Two
Create a a database view say university_summary_data which includes this count and create an Entity mapped to this view (works just like a table) then in University:
#OneToOne
private UniversitySummaryData data;
Three
Look into Hibernate's #LazyCollection(LazyCollectionOption.EXTRA) which will allow you to call size() on the mapped collection without loading all Students.
All much simpler solutions that what you have.
You say that you want to switch to EAGER loading because the performance with Lazy loading is terrible, but with EAGER loading your performance will be the same. You will still get the select n + 1 problem explained for example here and here with solution.
For performance to improve you need to modify the query that Hibernate will generate. Usually I do a left outer join in Hibernate to obtain this, e.g.
sessionFactory.getCurrentSession().createCriteria(University.class)
.createAlias("students", "students_alias", JoinType.LEFT_OUTER_JOIN)
.list();
And it's best to keep the Hibernate default of lazy loading.
I'm kind of lost in the woods at this point trying to optimize how Hibernate gets its data from the database. Here's the case:
I have a Person class that has a OneToMany association with the Address class. At the time of querying it is known from the application point of view that we're not going to need the Person instance but we'd like to have the list of that Person's Addresses.
The classes look more/less like this (getters/setters omitted):
#Entity
public class Person {
#Id
#GeneratedValue
private Long id;
#Column
private String firstName;
#Column
private String lastName;
#OneToMany(cascade = CascadeType.ALL)
#JoinColumn(name="address_id")
private Set<Address> addresses = new HashSet<Address>();
//...
}
#Entity
public class Address {
#Id
#GeneratedValue
private Long id;
#Column
private String city;
#Column
private String street;
//...
}
Now what I'd like to achieve is a criteria query (I have all the other parts of the system already using Criteria API and I'd very much like to keep it consistent) that'll return addresses belonging to a given person.
Criteria is the limiting factor here: it only allows to select the root entity, or scalars. And since you don't have the reverse association (from address to person), it's not possible to do it in a simple way with Criteria.
I find HQL much more flexible, and much more readable as well. Criteria is useful when a query must be dynamically composed, but in this case, using a HQL query is straightforward:
select a from Person p inner join p.addresses a where p.id = :personId
It's actually doable in Criteria, but it would need a query which is less efficient and straightforward: something like
select a from Address a where a.id in (select a2.id from Person p inner join p.addresses a2 where p.id = :personId)
translated in Criteria.
That would be:
Criteria criteria = session.createCriteria(Address.class, "a");
DetachedCriteria dc = DetachedCriteria.forClass(Person.class, "p");
dc.createAlias("p.addresses", "a2");
dc.add(Restrictions.eq("p.id", personId);
dc.setProjection(Projections.property("a2.id"));
criteria.add(Subqueries.propertyIn("a.id", dc));
As you see: less readable, much longer, and less efficient.
I'm trying to build a smaller SQL, to avoid the "select * from A" that is being build by default for hibernate Criteria.
If I use simple fields (no relation), through "Transformers", I have can manage to have this SQL:
select description, weight from Dog;
Hi, I have this Entity:
#Entity
public class Dog
{
Long id;
String description;
Double weight;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "person_id", nullable = false)
Person owner;
}
#Entity
public class Person
{
Long id;
String name;
Double height;
Date birthDate;
}
My goal is to have this:
select description, weight, owner.name from Dog
I tried this with with Criteria (and subcriteria):
Criteria dogCriteria = sess.createCriteria(Dog.class);
ProjectionList proList = Projections.projectionList();
proList.add(Projections.property("description"), description);
proList.add(Projections.property("weight"), weigth);
dogCriteria.setProjection(proList);
Criteria personCriteria = dogCriteria.createCriteria("owner");
ProjectionList ownerProList = Projections.projectionList();
ownerProList.add(Projections.property("name"), description);
dogCriteria.setProjection(ownerProList); //After this line, debugger shows that the
//projection on dogCriteria gets overriden
//and the query fails, because "name" is
//not a field of Dog entity.
How should I use Projections, to get a smaller SQL, less columns ?
Thanks in advance.
First of all,
select description, weight, owner.name from Dog
is not valid SQL. It would have to be something like
select description, weight, Person.name
from Dog join Person on Dog.person_id = Person.id
instead. Secondly, why? While it's possible to do what you want (see below), it's extremely verbose to do so via Criteria API and you gain nothing to show for it. Savings on data transfer for a couple of columns are negligible unless said columns are huge blobs or you're selecting hundreds of thousands of records. In either case there are better ways to deal with this issue.
Anywho, to do what you want for criteria, you need to join linked table (Person) via alias and specify projection on main criteria using said alias:
Criteria criteria = session.createCriteria(Dog.class, "dog")
.createAlias("owner", "own")
.setProjection( Projections.projectionList()
.add(Projections.property("dog.description"))
.add(Projections.property("dog.weight"))
.add(Projections.property("own.name"))
);
There's a description and an example of the above in Criteria Projections documentation. Keep in mind that, when executed, the above criteria would return a list of object arrays. You'll need to specify a ResultTransformer in order to have results converted into actual objects.
I didn't tried it yet by myself, but I think you can also use another constructor in your Entity (Pojo) and pass the columns there.
See https://www.thoughts-on-java.org/hibernate-best-practices/ chapter "1.2 Pojo" for a detailed instruction.
Altough for me it's not yet clear if this also works for ManyToOne relationships too. I will have a try.