Querying #ManyToMany with unmapped #JoinTable with hibernate in a spring-mvc app - spring

I have two entities. (Find code below)
I am trying to write a query that would count customDetails=:myCriteria of EntitiesA that are associated to EntityB of specific id.
I have written the necessary query using session.CreateSQLQuery that reads the associated_entitites table, however, I am unable to use it as the customDetails column is encrypted by hibernate's #ColumnTransformer and returns a BLOB. And I cannot replicate it in HQL as associated_entities is not mapped.
a
#Entity
public class entityA{
#Id
private int id;
#Column
#ColumnTransformer
private CustomDetails customDetails;
#ManyToMany(fetch = FetchType.EAGER,
cascade = {
CascadeType.PERSIST,
CascadeType.MERGE
},
mappedBy = "entitiesA")
private List<entityB> entitiesB;
//getters and setters
}
b
#Entity
public class entityB{
#Id
private int id;
#JoinTable(name = "associated_entities",
joinColumns = { #JoinColumn(name = "entityA_id") },
inverseJoinColumns = { #JoinColumn(name = "entityB_id") })
private List<EntityA> entitiesA;
//getters and setters
}

The solution I have found, but is not ideal as the logic is not done by hibernate. Had to write the logic in the DAOImpl.
Example code:
public Long getQuery(String criteria, String, fromdate, String todate){
Query theQuery = currentSession.createQuery(
"from EntityA a "+
"where a.CustomDetails >= :from "+
"and a.CustomDetails <= :to");
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate From = LocalDate.parse(fromDate, formatter);
LocalDate To = LocalDate.parse(toDate, formatter);
theQuery.setParameter("from", From);
theQuery.setParameter("to", To);
Long count = (long)0;
List<EntityA> entities= theQuery.list();
for(EntityA EA:entities) {
for(EntityB EB: EA.getEntityB()) {
if(EB.someValue().equals(criteria)) count++;
}
}
return count;

Another solution I have found and is much preferred as the logic is performed by hibernate, which I have found to be a lot more faster, is to use two separate queries and utilise where :foo in elements()
Code example below (not matching question example, but idea and use of elements() should be clear)
Query<Object1> q1 = currentSession.createQuery("from Object1 o where o.objectNumber= :objectNumber");
q1.setParameter("objectNumber", objectNumber);
Object1 obj1 = q1.getSingleResult();
Query<Long> q2 = currentSession.createQuery("select count(id) from Object2 o where :object1param in elements(o.associatedObjects));
q2.setParameter("object1param ", obj1);

Related

OrderBy is not working with the In clause in Spring Data JPA

I have a method whose signature is like this.
public Page<Config> searchByCriteria(Map<String, String> params, Pageable pageable) {
//prework
if (zoneIds.equals(BLANK_STRING)) {
return repository.findByUserIdAndNameContains(userId, name, pageable);
}
List<Integer> zones = Arrays
.stream(zoneIds.split(","))
.distinct()
.map(Integer::parseInt)
.collect(toList());
return repository.findByUserIdEqualsAndNameContainsAndZonesInAndOrderByUpdatedOnDesc(userId, name, zones, pageable);
}
Here the second query is not working when the OrderBy clause is being appended.
However, it works fine when I remove the OrderBy clause.
eg: repository.findByUserIdEqualsAndNameContainsAndZonesIn(...) This works fine.
Please suggest how can I use In and OrderBy clauses together.
here is the error which I am getting here in the stack trace.
Caused by: org.springframework.data.mapping.PropertyReferenceException: No property inAnd found for type Integer! Traversed path:
Config Object
#Entity
public class Config extends AbstractEntity implements Serializable {{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer configId;
private Integer userId;
private String name;
#Enumerated(EnumType.STRING)
private State state;
#ElementCollection
#CollectionTable(name = "zone", joinColumns = #JoinColumn(name = "config_id"))
#Column(name = "zone_id")
private Set<Integer> zones = new HashSet<>();
#Valid
#ElementCollection
#CollectionTable(name = "param", joinColumns = #JoinColumn(name = "config_id"))
private Set<ApParam> apParams = new HashSet<>();
private String remarks;
//getter
//setter
}
I think the problem is you are connecting the method query and the order by with And. This should work: repository.findByUserIdEqualsAndNameContainsAndZonesInOrderByUpdatedOnDesc(userId, name, zones, pageable);
Example: https://www.baeldung.com/spring-data-sorting#1-sorting-with-the-orderby-method-keyword
Document reference: https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation

Best way to combined hibernate search queries on different indexes

We have the following situation
Given the following two entities
#Indexed
#Spatial(spatialMode = SpatialMode.HASH)
#Entity
#Table(name = "address")
Address{
#Field
#Basic
#Column(name = "state")
private String state;
#Field
#Basic
#Column(name = "town_city")
private String townCity;
#Field
#Longitude
#Basic
#Column(name = "x_coord")
private Double xCoord;
#Field
#Latitude
#Basic
#Column(name = "y_coord")
private Double yCoord;
}
And
#Indexed
#Entity
#Table(name = "person")
Person{
#Field
#Column(name = "weight")
private Double weight;
#Column(name = "age")
private Integer age;
#org.hibernate.annotations.Cache(usage =
org.hibernate.annotations.CacheConcurrencyStrategy.READ_WRITE)
#ManyToMany
#Cascade({org.hibernate.annotations.CascadeType.SAVE_UPDATE})
#JoinTable(name = "person_address",
joinColumns = {#JoinColumn(name = "person_id")},
inverseJoinColumns = {#JoinColumn(name = "address_id")})
private Set<Address> addressSet = new HashSet<>();
}
Getters and Setters rest of the fields omitted
We want to return in our search results as an example people within a 5KM radius of a given position who are also within an age range.
So
FullTextSession fullTextSession = Search.getFullTextSession(entityManagerFactory.unwrap(SessionFactory.class).openSession());
this.queryBuilder = fullTextSession.getSearchFactory()
.buildQueryBuilder().forEntity(Person.class)
.overridesForField("identifiers.identifier_edge", "identifier_query_analyzer")
.get();
this.bool = queryBuilder.bool();
LocalDateTime lowerLocalDateTime = localDateTime.withYear(localDateTime.getYear() - upperAge);
lowerDate = Date.from(lowerLocalDateTime.atZone(ZoneId.systemDefault()).toInstant());
LocalDateTime upperLocalDateTime = localDateTime.withYear(localDateTime.getYear() - lowerAge);
upperDate = Date.from(upperLocalDateTime.atZone(ZoneId.systemDefault()).toInstant());
bool.must(getQueryBuilder().range().onField("datesOfBirth.dateOfBirth").from(lowerDate).to(upperDate).createQuery());
which will give us people within the relevant age range
We have a separte query to get the address id's within a radius around a given point
public Set<Integer> getSpatialAddressResults(SpatialSearchCommand spatialSearchCommand) {
FullTextSession fullTextSession = Search.getFullTextSession(entityManagerFactory.unwrap(SessionFactory.class).openSession());
this.userSearchPreference = userSearchPreference;
this.queryBuilder = fullTextSession.getSearchFactory()
.buildQueryBuilder().forEntity(Address.class)
.get();
this.bool = queryBuilder.bool();
Set<Integer> addressIdSet = new HashSet<>();
bool.must(getQueryBuilder().spatial()
.within(spatialSearchCommand.getRadius(), Unit.KM).ofLatitude
(spatialSearchCommand.getLat()).andLongitude(spatialSearchCommand.getLng()).createQuery());
FullTextQuery fullTextQuery =
fullTextSession.createFullTextQuery(bool.createQuery(), Address.class)
.setProjection("addressId")
.initializeObjectsWith(ObjectLookupMethod.SECOND_LEVEL_CACHE,
DatabaseRetrievalMethod.QUERY);
List results = fullTextQuery.list();
for (Object result : results) {
Object[] arrayResult = (Object[]) result;
addressIdSet.add(((Integer) arrayResult[0]));
}
if (addressIdSet.size() == 0) {
addressIdSet.add(-1);
}
return addressIdSet;
}
Which we use like below (in reality these are done in separate classes but for simplicity I have just shown the relevant code
Set<Integer> localAddressIds = getSpatialAddressResults(new SpatialSearchCommand(userSearchPreference.getRadius(), userSearchPreference.getLat(), userSearchPreference.getLng()));
if(localAddressIds.size() > 0){
BooleanJunction<BooleanJunction> localSquQueryBool = getQueryBuilder().bool();
for (Integer localAddressId : localAddressIds) {
localSquQueryBool.should(getQueryBuilder().keyword().onField("currentLocation.address.indexId").matching(localAddressId).createQuery());
if(!personSearchCommand.getCurrentOnly()){
localSquQueryBool.should(getQueryBuilder().keyword().onField("locations.address.indexId").matching(localAddressId).createQuery());
}
}
bool.must(localSquQueryBool.createQuery());
}
The problem is there can be a huge amount of addresses returned which results in a BooleanQueryTooManyClauses: maxClauseCount is set to 1024
The question really is what is the best way to combined queries on two different indexed entities to avoid problems like above.
Essentially, you are trying to implement a join operation. As you can see, there are technical challenges to joins which are not easily solved on the client side.
Generally, the recommended approach in Elasticsearch and Lucene is to avoid joins when you can. Instead, you will de-normalize your schema: within the document representing each person, embed a copy of every address. Then you will be able to express all your constraints in a single query targeting the person index.
This is done by annotating the addresses property in Person with #IndexedEmbedded.
Now, as you can imagine, this de-normalization comes at a cost: whenever an address is changed, Hibernate Search has to update the relevant person.
To that end, you will need to add a List<Person> property to your Address class and annotate it with #ContainedIn in order for Hibernate Search to be able to fetch the persons to reindex whenever an address is modified.
In short, change your model to this:
//#Indexed // No longer needed
#Spatial(spatialMode = SpatialMode.HASH, name = "location") // Give a name to the spatial field
#Entity
#Table(name = "address")
Address {
// Add this
#ManyToMany(mappedBy = "addressSet")
#ContainedIn
private Set<Person> personSet = new HashSet<>();
#Field
#Basic
#Column(name = "state")
private String state;
#Field
#Basic
#Column(name = "town_city")
private String townCity;
//#Field// This is not necessary
#Longitude
#Basic
#Column(name = "x_coord")
private Double xCoord;
//#Field// This is not necessary
#Latitude
#Basic
#Column(name = "y_coord")
private Double yCoord;
}
#Indexed
#Entity
#Table(name = "person")
Person{
#Field
#Column(name = "weight")
private Double weight;
#Column(name = "age")
private Integer age;
#org.hibernate.annotations.Cache(usage =
org.hibernate.annotations.CacheConcurrencyStrategy.READ_WRITE)
#ManyToMany
#Cascade({org.hibernate.annotations.CascadeType.SAVE_UPDATE})
#JoinTable(name = "person_address",
joinColumns = {#JoinColumn(name = "person_id")},
inverseJoinColumns = {#JoinColumn(name = "address_id")})
#IndexedEmbedded // Add this
private Set<Address> addressSet = new HashSet<>();
#Transient
#IndexedEmbedded // Also add this
public Address getCurrentAddress() {
// This was missing in your schema, I suppose it's a getter that picks the current address from addressSet?
}
}
Then reindex. Your Person documents will now have two new fields: addressSet.location and currentAddress.location.
Then write your query like this:
FullTextSession fullTextSession = Search.getFullTextSession(entityManagerFactory.unwrap(SessionFactory.class).openSession());
this.queryBuilder = fullTextSession.getSearchFactory()
.buildQueryBuilder().forEntity(Person.class)
.overridesForField("identifiers.identifier_edge", "identifier_query_analyzer")
.get();
this.bool = queryBuilder.bool();
LocalDateTime lowerLocalDateTime = localDateTime.withYear(localDateTime.getYear() - upperAge);
lowerDate = Date.from(lowerLocalDateTime.atZone(ZoneId.systemDefault()).toInstant());
LocalDateTime upperLocalDateTime = localDateTime.withYear(localDateTime.getYear() - lowerAge);
upperDate = Date.from(upperLocalDateTime.atZone(ZoneId.systemDefault()).toInstant());
bool.must(getQueryBuilder().range().onField("datesOfBirth.dateOfBirth").from(lowerDate).to(upperDate).createQuery());
SpatialSearchCommand spatialSearchCommand = new SpatialSearchCommand(userSearchPreference.getRadius(), userSearchPreference.getLat(), userSearchPreference.getLng());
// The magic happens below
BooleanJunction<BooleanJunction> localSquQueryBool = getQueryBuilder().bool();
localSquQueryBool.should(getQueryBuilder().spatial()
.onField("currentAddress.location")
.within(spatialSearchCommand.getRadius(), Unit.KM)
.ofLatitude(spatialSearchCommand.getLat())
.andLongitude(spatialSearchCommand.getLng())
.createQuery());
if(!personSearchCommand.getCurrentOnly()) {
localSquQueryBool.should(getQueryBuilder().spatial()
.onField("addressSet.location")
.within(spatialSearchCommand.getRadius(), Unit.KM)
.ofLatitude(spatialSearchCommand.getLat())
.andLongitude(spatialSearchCommand.getLng())
.createQuery());
}
bool.must(localSquQueryBool.createQuery());

Spring Data lock table to read while writing

I am using Spring data in my app (2.0.1). The app is REST-based service which add orders.
The order entity looks as follows:
#Entity
#Table(name = "orders")
#Data
#NoArgsConstructor
public class OrderEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#Version
private int version;
private Date date;
private String post;
private BigDecimal totalAmount;
#Enumerated(EnumType.STRING)
private OrderStatus status;
#OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinColumn(name = "order_id")
List<OrderSetEntity> sets;
private int dailyNumber;
private String documentNumber;
The posting or orders happens in OrderService:
public Long postOrder(OrderDTO orderDTO){
Date date = new Date();
OrderEntity order = new OrderEntity(date, orderDTO.getPost(), orderDTO.getPaymentMethod());
Integer dailyNumber = orderRepository.findDailyNumberByDate(date) + 1;
order.setDailyNumber(dailyNumber);
orderRepository.save(order);
return order.getId();
}
while findDailyNumberByDate is implemented this way:
#Override
public int findDailyNumberByDate(Date date) {
String sql = String.format("select max(o.dailyNumber) from OrderEntity o ");
Query query = entityManager.createQuery(sql);
Integer result = (Integer) query.getSingleResult();
if (result == null){
return 0;
}else {
return result.intValue();
}
Now I have a problem, that it get duplicated dailyNumber. The table isn't locked for reading when I am about to write into it.
How can I achieve it?
I tried lockin the table - #Lock(LockModeType.PESSIMISTIC_WRITE)
or query.setLockMode(LockModeType.PESSIMISTIC_FORCE_INCREMENT);
but is still isn't working.
Thanks a lot for help
LockModeType can work in different ways with different databases, especially with Oracle db it gets a little tricky. A similar issue was answered here jpa lockmode type npt working as expected

HIbernate + JPA OneToMany Lazy loading not working if no foreign key specified in the db

Hibernate lazy loading is not working in my code. It loads the entire data even it is specified as FetchType LAZY
#Transactional(propagation = Propagation.NEVER)
public OrderItem getItem(String itemId) throws Exception {
OrderItem item = itemDao.find(OrderItem.class, Integer.parseInt(itemId));
if (item == null) {
throw new Exception(502, "We are unable to load item for #" + itemId);
}
return item;
}
#NotFound(action = NotFoundAction.IGNORE)
#OneToMany(cascade={CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.LAZY)
#JoinColumn(name = "id_order_detail")
#Fetch(value= FetchMode.JOIN)
#JsonManagedReference
private Set<OrderItemStateChangeEntry> itemStateHistory;
I could not able to lazy load the contents. There is no foreign key constraint set in the db. And its not possible to set as the many parent data not present in the system.
Can somebody help me on this
Update
Added my class and reference. But lazy load work
#Entity
#Table(name = "ps_orders")
#AttributeOverrides({
#AttributeOverride(name="id",column=#Column(name="id_order")),
#AttributeOverride(name="createTime",column=#Column(name="date_add")),
#AttributeOverride(name="updateTime",column=#Column(name="date_upd"))
})
public class Order extends BaseEntity{
#Column(name = "id_carrier")
private Integer carrier = 0;
#NotFound(action = NotFoundAction.IGNORE)
#OneToMany(fetch = FetchType.LAZY,cascade={CascadeType.PERSIST, CascadeType.MERGE}, mappedBy="order")
#Fetch(FetchMode.SELECT)
#JsonManagedReference
private Set<OrderStateChangeEntry> orderHistory;
//Getters and Setters
}
#Entity
#Table(name = "ps_order_history")
#EnableBouncerProfile
public class OrderStateChangeEntry implements java.io.Serializable{
public OrderStateChangeEntry(){}
public OrderStateChangeEntry(Order order){
this.order = order;
}
#Id
#Column(name = "id_order_history")
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name="id_order", nullable=false)
#JsonBackReference
private Order order;
//Getters and Setters
}
It is because of your
#Fetch(value= FetchMode.JOIN)
it disables the lazy loading ...
As you specify the fetch mode in your #OnetoMany relationship, i would say that you can simply remove that line above.

Spring Data JPA Specification Predicate for a #OneToMany Collection not working

For background:
I have built a module that captures a list of a historical events that occur against an asset over its life and using JPA specifications using spring-data-jpa with hibernate to run the dynamic query using the JPA SpecificationExecutor interface. I have the following historical event JPA object with a many to one asset this historical event is directly against and other associated assets this historical event is also associated with defined in a many-to-many relationship. I am trying to write a JPA Specification predicate that pulls all historical events for a given asset that the asset is either directly against or associated too by using the includeAssociations flag in the predicate. When I try to execute the predicate I am not getting the correct results when I have the includeAssociations flag set to true. I would expect it would by default return at a minimum all the historical events they are directly as if the includeAssociations was false plus any ones they are indirectly associated with. I need help figuring out why this predicate is not returning back what I would expect. Any help is much appreciated!
Here is my Historical Event JPA object:
#Entity
#Table(name = "LC_HIST_EVENT_TAB")
public class HistoricalEvent extends BaseEntity implements Comparable<HistoricalEvent>, Serializable
{
private static final long serialVersionUID = 1L;
#ManyToOne(targetEntity = Asset.class, cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#JoinColumn(nullable = false, name = "ASSET_ID")
private Asset asset;
#ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, targetEntity = Asset.class)
#JoinTable(name = "LC_HIST_EVENT_ASSETS", joinColumns =
{
#JoinColumn(name = "HIST_EVENT_ID", referencedColumnName = "id")
}, inverseJoinColumns =
{
#JoinColumn(name = "ASSET_ID", referencedColumnName = "id")
}, uniqueConstraints =
{
#UniqueConstraint(columnNames =
{
"HIST_EVENT_ID", "ASSET_ID"
})
})
#BatchSize(size=10)
#OrderBy("partCatalogItem.partID, serialNumber ASC")
private Set<Asset> associatedAssets;
#Column(name = "START_DATE", nullable = true)
#Temporal(value = TemporalType.TIMESTAMP)
private Calendar startDate;
#Column(name = "END_DATE", nullable = true)
#Temporal(value = TemporalType.TIMESTAMP)
private Calendar endDate;
}
JPA Metamodel for Historical Event:
#StaticMetamodel(HistoricalEvent.class)
public class HistoricalEvent_ extends BaseEntity_
{
public static volatile SingularAttribute<HistoricalEvent, Asset> asset;
public static volatile SetAttribute<HistoricalEvent, Asset> associatedAssets;
public static volatile SingularAttribute<HistoricalEvent, Calendar> startDate;
public static volatile SingularAttribute<HistoricalEvent, Calendar> endDate;
public static volatile SingularAttribute<HistoricalEvent, String> type;
public static volatile SingularAttribute<HistoricalEvent, String> description;
public static volatile SingularAttribute<HistoricalEvent, HistoricalEvent> triggeringEvent;
public static volatile SetAttribute<HistoricalEvent, HistoricalEvent> associatedEvents;
public static volatile MapAttribute<HistoricalEvent, String, HistoricalEventMap> data;
}
Here is my Asset JPA Object:
#Entity
#Table(name = "LC_ASSET_TAB")
public class Asset extends BaseEntity implements Comparable<Asset>, Serializable
{
private static final long serialVersionUID = 1L;
#ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL, targetEntity = PartCatalog.class)
#JoinColumn(name = "PART_CATALOG_ID", nullable = false)
private PartCatalog partCatalogItem;
#Column(name = "SERIAL_NO", nullable = false)
private String serialNumber;
#Column(name = "DATE_INTO_SERVICE", nullable = false)
#Temporal(value = TemporalType.TIMESTAMP)
private Calendar dateIntoService;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "asset", targetEntity = AssetMap.class)
#MapKey(name = "fieldName")
#BatchSize(size=25)
private Map<String, AssetMap> data;
}
Asset Metamodel:
#StaticMetamodel(PartCatalog.class)
public class PartCatalog_ extends BaseEntity_
{
public static volatile SingularAttribute<PartCatalog, String> partID;
public static volatile SingularAttribute<PartCatalog, String> nsn;
public static volatile SingularAttribute<PartCatalog, String> description;
public static volatile MapAttribute<PartCatalog, String, PartCatalogMap> data;
}
Here is my Part Catalog JPA object:
#Entity
#Table(name = "LC_PART_CATALOG_TAB")
public class PartCatalog extends BaseEntity implements Comparable<PartCatalog>, Serializable
{
private static final long serialVersionUID = 1L;
#Column(name = "PART_ID", length=100, nullable = false)
private String partID;
#Column(name = "NSN", length=100, nullable = true)
private String nsn;
#Column(name = "DESCRIPTION", length=250, nullable = false)
private String description;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "partCatalogItem", targetEntity = PartCatalogMap.class)
#MapKey(name = "fieldName")
private Map<String, PartCatalogMap> data;
}
Part Catalog Metamodel:
#StaticMetamodel(PartCatalog.class)
public class PartCatalog_ extends BaseEntity_
{
public static volatile SingularAttribute<PartCatalog, String> partID;
public static volatile SingularAttribute<PartCatalog, String> nsn;
public static volatile SingularAttribute<PartCatalog, String> description;
public static volatile MapAttribute<PartCatalog, String, PartCatalogMap> data;
}
Specification Predicate for returning historical events by a given Part Number and Serial Number:
PROBLEM: If includeAssociations is false, it returns fine however soon as it is true, it returns the wrong list of associations and never returns any results from the events the asset is directly tied too like if the includeAssociations was false. This is where I need help how to best write the criteria builder query to properly pull the data.
These are the two JPQL queries I am trying to combine into the Predicate using the Criteria API:
Normal:
#Query("SELECT he FROM HistoricalEvent he WHERE he.asset.partCatalogItem.partID =:partID AND he.asset.serialNumber =:serialNumber " +
"AND he.startDate >:startDate AND he.endDate <:endDate")
Association:
#Query("SELECT he FROM HistoricalEvent he INNER JOIN he.associatedAssets associated WHERE associated.partCatalogItem.partID =:partID AND associated.serialNumber =:serialNumber " +
"AND he.startDate >:startDate AND he.endDate <:endDate");
/**
* Creates a specification used to find historical events by a given asset part number and serial
* parameter.
*
* #param partID - part identifier
* #Param serialNumber
* #return Historical Event Specification
*/
public static Specification<HistoricalEvent> hasPartAndSerial(final String partID, final String serialNumber, final Boolean includeAssociations)
{
return new Specification<HistoricalEvent>() {
#Override
public Predicate toPredicate(Root<HistoricalEvent> historicalEventRoot,
CriteriaQuery<?> query, CriteriaBuilder cb) {
if (partID == null || partID == "")
{
return null;
}
if(serialNumber == null || serialNumber =="")
{
return null;
}
Path<Asset> assetOnEvent = historicalEventRoot.get(HistoricalEvent_.asset);
Path<PartCatalog> partCatalogItem = assetOnEvent.get(Asset_.partCatalogItem);
Expression<String> partIdToMatch = partCatalogItem.get(PartCatalog_.partID);
Expression<String> serialToMatch = assetOnEvent.get(Asset_.serialNumber);
if(includeAssociations)
{
SetJoin<HistoricalEvent, Asset> assetsAssociatedToEvent = historicalEventRoot.join(HistoricalEvent_.associatedAssets);
Path<PartCatalog> partCatalogItemFromAssociatedAsset = assetsAssociatedToEvent.get(Asset_.partCatalogItem);
Expression<String> partIdToMatchFromAssociatedAsset = partCatalogItemFromAssociatedAsset.get(PartCatalog_.partID);
Expression<String> serialToMatchFromAssociatedAsset = assetsAssociatedToEvent.get(Asset_.serialNumber);
return cb.or(cb.and(cb.equal(cb.lower(partIdToMatch), partID.toLowerCase()), cb.equal(cb.lower(serialToMatch), serialNumber.toLowerCase())),
cb.and(cb.equal(cb.lower(partIdToMatchFromAssociatedAsset), partID.toLowerCase()), cb.equal(cb.lower(serialToMatchFromAssociatedAsset), serialNumber.toLowerCase())));
}
else
{
return cb.and(cb.equal(cb.lower(partIdToMatch), partID.toLowerCase()), cb.equal(cb.lower(serialToMatch), serialNumber.toLowerCase()));
}
}
};
}
Finally I am calling this to find the historical events:
#Override
public Page<HistoricalEvent> getByCriteria(String type, String partID,
String serialNumber, Calendar startDate, Calendar endDate,
Boolean includeAssociations, Integer pageIndex, Integer recordsPerPage)
{
LOGGER.info("HistoricalEventDatabaseServiceImpl - getByCriteria() - Searching historical event repository for type of " + type + " , part id of " + partID +
" , serial number of " + serialNumber + " , start date of " + startDate + " , end date of " + endDate + ", include associations flag of " + includeAssociations
+ " , pageIndex " + pageIndex + " and records per page of " + recordsPerPage);
Page<HistoricalEvent> requestedPage = historicalEventRepository.findAll(Specifications
.where(HistoricalEventSpecifications.hasType(type))
.and(HistoricalEventSpecifications.greaterThanOrEqualToStartDate(startDate))
.and(HistoricalEventSpecifications.lessThanOrEqualToEndDate(endDate))
.and(HistoricalEventSpecifications.hasPartAndSerial(partID, serialNumber, includeAssociations)),
DatabaseServicePagingUtil.getHistoricalEventPagingSpecification(pageIndex, recordsPerPage));
LOGGER.info("HistoricalEventDatabaseServiceImpl - getByCriteria() - Found " + requestedPage.getTotalElements() + " that will comprise " + requestedPage.getTotalPages() + " pages of content.");
return requestedPage;
} UPDATE: i have been able to get the specification if the historical event was either directly or indirectly associated working however using the following Predicate 1 = cb.equals(cb.lower(partIDToMatch, partID.toLowercase()); Predicate2 = cb.equals(cb.lower(serialToMatch), serialNumber.toLowercase(); Predicate3 = cb.or(Predicate1, Predicate2 ); Predicate4 = cb.equals(cb.lower(partIDToMatchFromAssociatedAsset), partIDToMatch.toLowercase()); Predicate5 = cb.equals(cb.lower(serialNumberFromAssociatedAsset), serialNumberToMatch.toLowercase()); Predicate6 = cb.and(Predicate4, Predicate5); Predicate7 = cb.or(Predicate3,Predicate6); When i return Predicate I only get results matching Predicate6 not either one as i would expect. I want it to pull events where either predicate condition returns a record. Each predicate returns the right data but when i use the cb.or it doesnt combine results as i would expect. What am I missing?
You have to start printing the query and parameters value that are bean generated, just enable this properties.
After that you have to analyze your query and make some tests with different combinations to check your jpa specification are falling.
There is no magic way to do that and it's hard and painful :(
Good look

Resources