Single jpa #Query to return true/false when count in one table is equal to column value in other table - jpa

I have 2 entities: Leaflet and Page with One to Many relation (many Pages per Leaflet)
#Entity
Leaflet {
#Id
#GeneratedValue
private UUID leafletId;
private Integer noPages;
#OneToMany(mappedBy = "leaflet", cascade = CascadeType.ALL, orphanRemoval = true)
Set<Page> pages = new HashSet<>();
}
#Entity
Page {
#Id
#GeneratedValue
private UUID pageId;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "leaflet_id")
private Leaflet leaflet;
#Enumerated
private PageStatus status = PageStatus.CREATED;
}
and status enum
public enum PageStatus {
CREATED,
FRAMED
}
I would like to write single query to return whether all Pages for given Leaflet are already FRAMED. So I wrote this
#Repository
public interface PageRepository extends JpaRepository<Page, UUID> {
#Query("SELECT case when (COUNT(p) = l.noPages) then true else false end from Page p inner join Leaflet l on p.leaflet.leafletId = l.leafletId where p.status = 1 and l.leafletId = ?1")
boolean allPagesFramed(UUID leafletId);
}
but error comes which means I cannot use l.noPages directly
ERROR: column "leaflet1_.no_pages" must appear in the GROUP BY clause or be used in an aggregate function
org.hibernate.exception.SQLGrammarException: could not extract ResultSet
Is there a way to make it 1 query ?
Of course, I can first select l.noPages with first hit to DB, then inject this value to above query (instead of join) which I'm doing right now as workaround.

You can do this based on page table. With nativeQuery = true
#Query(value = "select case when ( count(*) > 0 ) then false else true end " +
"from page p " +
"where p.leaflet_id = ?1 and p.status <> 1 ", nativeQuery = true)
boolean allPagesFramed(UUID leafletId);
If a page has at least one status different from 1 (FRAMED), then the query return false, not all the pages are FRAMED.

Related

JPQL Query Sort by average rating then box office value for movie

Ok so I have two JPA Entities a Movie and a MovieRating. Similar to the below
#Entity
#Table(name = Movie.TABLE_NAME)
public class Movie {
static final String TABLE_NAME = "Movies";
#Id
#Column(name = "IMDB_ID")
private String imdbID;
#Column(name = "BOX_OFFICE_TAKINGS")
private int boxOfficeTakings;
#OneToMany(mappedBy = "movie", cascade = CascadeType.ALL, orphanRemoval = true)
private List<MovieRating> ratings = new ArrayList<>();
// Getters and setters
}
And
#Entity
#Table(name = MovieRating.TABLE_NAME)
public class MovieRating {
static final String TABLE_NAME = "MovieRatings";
#Id
#GeneratedValue(generator = "UUID")
#GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
private UUID id;
private int rating;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "IMDB_ID")
private Movie movie;
//Getters and setters
}
I've written a #Query in the JPA Repository that should return results ordered by average rating descending and then box office value descending eg.
#Query("SELECT m FROM Movie m LEFT JOIN m.ratings r GROUP BY m ORDER BY AVG(r.rating) desc, m.boxOfficeTakings desc")
List<Movie> findTop10OrderedByBoxOfficeTakings(Pageable pageable);
So I'd expect for example a rated movie with a lower box office value to appear above an unrated movie with a higher box office value.
So I've written a test around this and it seems to work like I'd expect
#Test
public void testGetTop10ReturnsRatedMovieAboveUnratedMovieWithHigherValue() {
Movie movie1 = createMovieWithRatings(new Movie("tt000001", "The Godfather part 1", 2010, 268500000), null);
Movie movie2 = createMovieWithRatings(new Movie("tt000002", "The Godfather part 2", 2010, 93000000),
Arrays.asList(new MovieRating(10)));
movieRepository.saveAll(Arrays.asList(movie1, movie2));
List<MovieResponse> top10 = movieService.getTop10OrderedByBoxOfficeTakings();
List<String> expectedMovieIds = Arrays.asList(movie2.getImdbID(), movie1.getImdbID());
List<String> actualMovieIds = top10.stream().map(movie -> movie.getImdbID()).collect(Collectors.toList());
Assertions.assertEquals(expectedMovieIds, actualMovieIds);
}
private Movie createMovieWithRatings(Movie movie, List<MovieRating> ratings) {
if (ratings != null) {
ratings.forEach(rating -> movie.addRating(rating));
}
return movie;
}
However in practice when the code is running the order is movie1, and then movie2. Movie2 only moves above movie1 when it receives more than 1 rating. Why?
Maybe because you are doing a LEFT JOIN and with LEFT JOIN anything you try to join on if doesn't have a value, in your case "The Godfather part 1" doesn't have any ratings so comes as null. try putting a null check in the query itself and if it is null replace with 0, so now it can calculate the AVG for each movie. That might help.

Cascade delete problem on **custom** unidirectional #OneToMany relationship (JPA eclipselink)

I'm having problem deleting an entity having an Unidirectional #OneToMany custom relationship.
Here the relationship on the "base" entity (relevant columns only):
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Basic(optional = false)
#Column(name = "Id", updatable = false)
protected Integer id;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
#JoinColumn(name = "ObjectId", referencedColumnName = "Id")
protected Collection<Attachment> attachmentsCollection;
Here the relevant columns on the "child" entity:
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Basic(optional = false)
#Column(name = "Id", updatable = false)
protected Integer id;
#NotNull
#Basic(optional = false)
#Size(min = 1, max = 64)
#Column(name = "ObjectTable", updatable = false)
protected String objectTable;
#Basic(optional = false)
#Column(name = "ObjectId", updatable = false)
protected Integer objectId;
#NotNull
#Basic(optional = false)
#Lob
#Column(name = "Data")
protected byte[] data;
#NotNull
#Basic(optional = false)
#Column(name = "SizeInBytes")
protected Long sizeInBytes;
#NotNull
#Basic(optional = false)
#Size(min = 1, max = 128)
#Column(name = "Name")
protected String name;
Here explanation on why this is a custom relationship:
The base object is the super class of all entities. It has, additional to its own id, the opportunity to associate any number of attachments via the attachments collection. Since Ids are not unique between tables (entities), it's needed to add an additional column in the child table (the attachment table). This additional column (ObjectTable) identifies the entity-kind owning the attachment. Adding this column to the entity'id (ObjectId) column, the relation is complete:
Suppose record 99 of entity Invoice has 2 attachments (attachment 'Z' and attachment 'Y'):
Table Invoice
-------------
Id ColumnA ColumnB ColumnC...
99 'xyz' '2343' 'zyx'
.
.
Table Attachment
----------------
Id ObjectTable ObjectId Data SizeInBytes Name
43542 'Invoice' 99 11100110 437834 'Z.pdf'
43543 'Invoice' 99 101110 867454 'Y.pdf'
I managed to load the relation with a mapping customizer:
public static final String TABLENAME = "TECAttachment";
public static final String OBJECTIDFIELDNAME = TABLENAME + ".ObjectId";
public static final String OBJECTTABLEFIELDNAME = TABLENAME + ".ObjectTable";
// Customize how records are selecting inside entity's attachments collection: include the entities table name
#Override
public void customize(ClassDescriptor descriptor) {
OneToManyMapping mapping = (OneToManyMapping)descriptor.getMappingForAttributeName(AttachmentEntitySessionCustomizer.ATTACHMENTSCOLLECTIONNAME);
ExpressionBuilder eb = new ExpressionBuilder();
Expression eObjectIdNotNull = eb.getField(AttachmentEntitySessionCustomizer.OBJECTIDFIELDNAME).notNull();
Expression eObjectId = eb.getField(AttachmentEntitySessionCustomizer.OBJECTIDFIELDNAME).equal(eb.getParameter(descriptor.getPrimaryKeyFields().get(0)));
Expression eObjectTable = eb.getField(AttachmentEntitySessionCustomizer.OBJECTTABLEFIELDNAME).equalsIgnoreCase(descriptor.getTableName());
mapping.setSelectionCriteria(eObjectIdNotNull.and(eObjectId.and(eObjectTable)));
}
... but I'm having problems during the delete operation of any entity. For a reason that I still don't understand, JPA is executing an update statement over the Attachment table without taking in consideration the ObjectTable column. Here is what's going on when I delete a record from the table PRHTABidParticipationItem:
Finest: Execute query DeleteObjectQuery(com.tec.uportal.prhta.model.bid.participation.PRHTABidParticipationItem[ id=24 ])
Finest: Execute query DataModifyQuery()
Fine: UPDATE TECAttachment SET ObjectId = ? WHERE (ObjectId = ?)
bind => [null, 24]
Fine: DELETE FROM TECPRHTABidParticipationItem WHERE (Id = ?)
bind => [24]
Finer: end unit of work flush
Finer: resume unit of work
Finer: begin unit of work commit
Finer: commit transaction
My problem is that the UPDATE statement over the table TECAttachment updates all records with the given Id and not only those related to the Entity TECPRHTABidParticipationItem.
I think I must override the sql statements DeleteObjectQuery or DataModifyQuery but don't know how.
Any help will be really appreciated.
I'm working with eclipselink-2.7.4
Thanks in advanace!
After a lot or searching, digging and reading I managed to solve my problem.
Basically:
My attachments collection is an unidirectional custom #OneToMany relationship.
I was in the correct path trying to achieve my goal with a collection customizer (a class associated to my collection via annotation and implementing the DescriptorCustomizer interface).
My customizer not only needs to customize the way the "child" records are selected, also it's needed to customize what to do when the parent record is deleted.
The way to do this is to override the default removeAllTargetsQuery property providing a new custom query through the method mapping.setCustomRemoveAllTargetsQuery(DataModifyQuery).
The most difficult part was to understand how the underlying eclipselink implementation sends parameters (which, order, type, etc.) to the custom DataModifyQuery. I had to download the source code of EclipseLink's JPA implementation and figure out how things are done...
Finally, everything is working good and ok thanks to the following simple DescriptorCustomizer:
package com.tec.uportal.model.customizer;
import org.eclipse.persistence.config.DescriptorCustomizer;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.expressions.Expression;
import org.eclipse.persistence.expressions.ExpressionBuilder;
import org.eclipse.persistence.internal.helper.DatabaseField;
import org.eclipse.persistence.mappings.OneToManyMapping;
import org.eclipse.persistence.queries.DataModifyQuery;
/**
*
* #author MarcB
*/
public class AttachmenstCollectionAttributeCustomizer implements DescriptorCustomizer {
public static final String ATTACHMENTS__COLLECTION_NAME = "attachmentsCollection";
public static final String ATTACHMENTS_TABLE_OBJECT_TABLE__FIELD_NAME = "ObjectTable";
// Customize attachments collection mapping
#Override
public void customize(ClassDescriptor descriptor) {
// Customize how records are selected inside parent entity's attachments collection: include in the WHERE clause the column ObjectTable
OneToManyMapping mapping = (OneToManyMapping)descriptor.getMappingForAttributeName(ATTACHMENTS__COLLECTION_NAME);
ExpressionBuilder eb = new ExpressionBuilder();
Expression eObjectId = eb.getField(mapping.getTargetForeignKeyFields().get(0).getQualifiedName()).equal(eb.getParameter(descriptor.getPrimaryKeyFields().get(0)));
Expression eObjectTable = eb.getField(mapping.getTargetForeignKeyFields().get(0).getTable().getQualifiedName() + "." + ATTACHMENTS_TABLE_OBJECT_TABLE__FIELD_NAME).equalsIgnoreCase(descriptor.getTable(descriptor.getTableName()).getQualifiedName());
mapping.setSelectionCriteria(eObjectId.and(eObjectTable));
// Customize what must be done (delete childs) when parent entity is deleted
DataModifyQuery dmf = new DataModifyQuery("DELETE " + mapping.getTargetForeignKeyFields().get(0).getTable().getQualifiedName() + " WHERE " + mapping.getTargetForeignKeyFields().get(0).getName() + " = #" + mapping.getTargetForeignKeyFields().get(0).getName() + " AND " + ATTACHMENTS_TABLE_OBJECT_TABLE__FIELD_NAME + " = '" + descriptor.getTableName() + "'");
mapping.setCustomRemoveAllTargetsQuery(dmf);
}
}

How to return a count column not exists in table by JPA

I want find a way to get extra column that count my records and return it in 1 mapping entity with extra filed.
I tried #transient on field but it will not return value when query.
Then I remove #transient but get an exception when save.
Also I tried #Formula but received null pointer exception.
Here's my repository code:
#Query(value = "select id,account,session_id,create_time,count from query_history a join " +
"(select session_id sessionId,max(create_time) createTime,count(*) count from query_history group by session_id) b " +
"on a.session_id = b.sessionId and a.create_time = b.createTime where account = ?1 order by create_time desc",
countQuery = "select count(distinct(session_id)) from query_history where account = ?1",
nativeQuery = true)
Page<QueryHistory> findByNtAndGroupBySessionAndAction(String account, Pageable pageable);
entity code:
#Entity
#Table(name = "query_history")
#Data
public class QueryHistory {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column
private String account;
#Column
private Long sessionId;
#Column
private long createTime;
#Transient
private Integer count;
}
Sorry about my English and thanks a lot for any advice.
I solved the problem by projections spring-data-projections, in fact I tried this before but in my sql:
select id,account,session_id,create_time,count
which should be:
select id,account,session_id sessionId,create_time createTime,count
PS:
projection interface:
public interface QueryHistoryWithCountProjection {
Long getId();
String getAccount();
Long getSessionId();
long getCreateTime();
Integer getCount();
}

How to make a CriteriaBuilder join with a custom "on" condition?

I want make a query where I join 2 tables, using the CriteriaBuilder. In MySQL the query I'm trying to make would look like this:
SELECT * FROM order
LEFT JOIN item
ON order.id = item.order_id
AND item.type_id = 1
I want to get all orders and if they have an item of type #1, I want to join with this item. However, if no item of type #1 is found, I still want to get the order. I can't figure out how to make this with the CriteriaBuilder. All I know how to make is:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Order> cq = cb.createQuery(Order.class);
Root<Order> order = cq.from(Order.class);
Join<Order, Item> item = order.join(Order_.itemList, JoinType.LEFT);
Join<Item, Type> type = order.join(Item_.type, JoinType.LEFT);
cq.select(order);
cq.where(cb.equal(type.get(Type_.id), 1));
This query is broke, since it results in something like this in MySQL:
SELECT * FROM order
LEFT JOIN item
ON order.id = item.order_id
WHERE item.type_id = 1
The result will only contain orders with items of type #1. Orders without are excluded. How can I use the CriteriaBuilder to create a query like in the first example?
It is possible starting from the version 2.1 of JPA using the on method Join<Z, X> on(Predicate... restrictions);
Here is how:
Root<Order> order = cq.from(Order.class);
Join<Order, Item> item = order.join(Order_.itemList, JoinType.LEFT);
item.on(cb.equal(item.get(Item_.type), 1));
I think this is the same problem as posed in this question. It looks like it is not possible in CriteriaBuilder. It is possible in Hibernate Criteria API, but that probably won't help you.
JPA Criteria API: Multiple condition on LEFT JOIN
I know this question was made a long time a go, but recently a had the same problem and i found this solution from an Oracle forum, i copied and pasted just in case the link is not longer available.
MiguelChillitupaArmijos 29-abr-2011 1:41 (en respuesta a 840578) Think
you should use something like:
em.createQuery("SELECT DISTINCT e.Id" +
" from Email e " +
" left join e.idEmailIn e2 *with* e2.responseType = 'response'" +
" where e.type = 'in' and e.responseMandatory = true").getSingleResult();
An this is the link.
JPA Criteria : LEFT JOIN with an AND condition
There is a workaround if you are using Hibernate 3.6 with JPA 2.0
It is not the better solution, however it works perfect for me.
I´ve duplicate the entity with the #Where hibernate annotation.It means that everytime you use the join with this entity, hibernate will add the extra condition on the join statement at generated SQL.
For instance, initially we have the follow example:
#Entity
#Table(name = "PERSON")
public class Person {
#Id
#Column(name = "PERSON_ID")
private Long id;
#Id
#Column(name = "PERSON_NAME")
private String name;
#OneToMany(mappedBy = "person", fetch = FetchType.LAZY)
private Set<Address> addresses;
}
#Entity
#Table(name = "ADDRESS")
public class Address {
#Id
#Column(name = "ADDRESS_ID")
private Long id;
#Id
#Column(name = "ADDRESS_STREET")
private String street;
#ManyToOne
#JoinColumn(name = "PERSON_ID")
private Person person;
}
In order to add extra conditions on criteria Join, we need duplicate the Address #Entity mapping , adding the #Where annotation #Where(clause = " ADDRESS_TYPE_ID = 2").
#Entity
#Table(name = "ADDRESS")
#Where(clause = " ADDRESS_TYPE_ID = 2")
public class ShippingAddress {
#Id
#Column(name = "ADDRESS_ID")
private Long id;
#Id
#Column(name = "ADDRESS_STREET")
private String street;
#OneToOne
#JoinColumn(name = "PERSON_ID")
private Person person;
}
Also, we need to add the duplicate mapping association for the new entity.
#Entity
#Table(name = "PERSON")
public class Person {
#Id
#Column(name = "PERSON_ID")
private Long id;
#Id
#Column(name = "PERSON_NAME")
private String name;
#OneToMany(mappedBy = "person", fetch = FetchType.LAZY)
private Set<Address> addresses;
#OneToOne(mappedBy = "person")
private ShippingAddress shippingAddress;
}
Finally, you can use a join with this specific Entity in your criteria :
PersonRoot.join(Person_.shippingAddress, JoinType.LEFT);
The Hibernate Snippet SQL should seems like this :
left outer join
address shippingadd13_
on person11_.person_id=shippingadd13_.person_id
and (
shippingadd13_.ADDRESS_TYPE_ID = 2
)
ON clause is supported in Hibernate 4.3 version, anyone is aware if there is a parameter indexing issue between the parameter index of the additional custom conditions with the index of the existing mapping filters when doing an outer join with ON clause?
Using the Person entity class below as an example, say I am adding this filter to limit the address types and the filter is enabled to populate the IN clause. The parameter index for the IN clause will cause the issue [2] when I add additional conditions (such as using 'street' column) part of the ON clause. Is is a known issue?
[1] #Filter(name = "addressTypes", condition = "ADDRESS_TYPE in (:supportedTypes)")
[2]
Caused by: ERROR 22018: Invalid character string format for type BIGINT.
private Set addresses;

JPA EclipseLink getCount of child entity

I am using EL and i keep getting 0 when i run the query below. I want to get the count of applicants (AP) that are currently active. The child entity Applicant is of Person and i want to avoid querying all elements of Person?
#RooJavaBean
#RooToString
#RooEntity(identifierColumn = "personID", inheritanceType = "SINGLE_TABLE")
#DiscriminatorColumn(name = "TYPE", discriminatorType = DiscriminatorType.STRING, length = 20)
#DiscriminatorValue("P")
public class Person {
#NotNull
#Size(min = 1, max = 50)
private String FirstName;
#NotNull
#Size(min = 1, max = 50)
private String LastName;
}
The child entity 'Applicant'
#RooJavaBean
#RooToString
#RooEntity
#DiscriminatorValue("AP")
public class Applicant extends Person{
private String major;
private String nativeLanguage;
private String ethnicity;
private String hispanic;
}
My query attempt:
/**
*
* #return
*/
public int getCountActiveApplicants(){
EntityManager entityManager = factory.createEntityManager();
int value = entityManager.createQuery("select count(distinct o) from Person o where o.TYPE = \"AP\" AND o.active = \"Yes\" ").getFirstResult();
System.out.println("wowzer " + value + "\n");
return value;
}
Why don't you simply count the applicants?
select count(distinct a) from Applicant a where a.active = true
EclipseLink will transform this in SQL and add the where clause on the discriminator for you.
Remember that JPQL works with your entities and their persistent fields/properties. It knows about their association and their inheritance hierarchy. JPQL never uses table and column names.
(Side note: why use "Yes" for a boolean field?)