How to handle a Hibernate multi-field search query on nullable child entities - spring-boot

Using Spring Boot Web & Data JPA (2.3.1) with QueryDSL with PostgreSQL 11, we are trying to implement a custom search for a UI table on an entity with two #ManyToOne child entities. The idea is to be able to provide a single search input field and search for that string (like or contains ignore case) across multiple String fields across the entities' fields and also provide paging. During UI POCs, we were originally pulling the entire list and having the web UI provide this exact search functionality but that will not be sustainable in the future.
My original thought was something to this effect:
/devices?externalId={{val}}&site.externalId={{val}}&organization.name={{val}}&size=10
but the more human readable intent was:
externalId={{val}} OR site.externalId={{val}} OR organization.name={{val}} WITH size=10
I tried implementing it with QueryDSL's Web Bindings (hence the above example) with a QuerydslBinderCustomizer but it didn't work. Then I realized that it doesn't provide much for this particular situation so I moved to a JpaRepository with an #Query and shortened the URL to.
/devices?search={{val}}&size=10
Either way, what seems to be happening is that if, for example, device.site is null, the entire result is always zero. Even if device.organization or device.site is null, I would expect results where the device.externalId matches the search value criteria. If I remove support for site.externalId, then it works; I get results matching the device.externalId. I also tried this on a database with devices with non-null site references and that also worked. So the issue seems to be centered around null child entities.
Quick Scenario:
Note: Device in JSON format and id's in non-UUID format for brevity.
{
"id" : "a",
"externalId" : "VTD1002",
"site" : null
},
{
"id" : "b",
"externalId" : "VTD_1000",
"site" : { "externalId" : "VTS_1000" }
},
{
"id" : "c",
"externalId" : "VFD_1000"
"site" : { "externalId" : "VFS_1000" }
}
Pseudo Tests:
search = "t" -> resulting IDs = a, b
Only a and b have a T
search = "Z" -> resulting IDs =
None have Z
search = "1" -> resulting IDs = a, b, c
All have a 1
search = "2" -> resulting IDs = a
Only a has a 2
Entities
Note: redacted from original for brevity
#Entity
#Data #Builder
#Table(name = "devices")
#EqualsAndHashCode(of = "id")
#NoArgsConstructor #AllArgsConstructor
public class Device {
#Id
#GeneratedValue
private UUID id;
private String externalId;
#ManyToOne
private Product product;
#ManyToOne
private Site site;
}
#Entity
#Data #Builder
#Table(name = "organizations")
#EqualsAndHashCode(of = "id")
#NoArgsConstructor #AllArgsConstructor
public class Organization {
#Id
#GeneratedValue
private UUID id;
private String name;
#ToString.Exclude
#OneToMany(mappedBy = "organization")
private Set<Device> devices;
}
#Entity
#Data #Builder
#NoArgsConstructor
#AllArgsConstructor
#EqualsAndHashCode(of = "id")
#Table(name = "sites")
public class Site {
#Id
#GeneratedValue
private UUID id;
private String externalId;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "site")
private Set<Device> devices;
}
Here's the latest I've tried without luck:
#Query("from Device d where (d.externalId like lower(:search)) or (d.site.id is not null and d.site.externalId like lower(:search)) or (d.organization.id is not null and d.organization.externalId like lower(:search))")
Page<Device> search(String search, Pageable pageable);
The idea was to check for the device's site_id reference on the table before even trying to evaluate the Site's externalId. Removing the is not null didn't make a difference and going d.site is not null also didn't work.
I think what's happening is that the native SQL that is generated is causing things to go awry. I suspect the issue is in the cross joins but after a few days of searching, any clues or insights would be appreciated.
select
device0_.id as id1_3_,
device0_.external_id as external3_3_,
device0_.organization_id as organiz13_3_,
device0_.site_id as site_id15_3_,
from
devices device0_ cross
join
sites site1_ cross
join
organizations organizati2_
where
device0_.site_id=site1_.id
and device0_.organization_id=organizati2_.id
and (
device0_.external_id like lower(?)
or (
device0_.site_id is not null
)
and (
site1_.external_id like lower(?)
)
or (
device0_.organization_id is not null
)
and (
organizati2_.external_id like lower(?)
)
) limit ?

The problem as you can see is that Hibernate uses inner joins for your implicit joins, which is forced onto it by JPA. Having said that, you will have to use left joins like this to make this null-aware stuff work
#Query("from Device d left join d.site s left join d.organization o where (d.externalId like lower(:search)) or (s.id is not null and s.externalId like lower(:search)) or (o.id is not null and o.externalId like lower(:search))")
Page<Device> search(String search, Pageable pageable);

With #christian-beikov help, I was able to get pass the nullability piece. However, I had to modify the query to get it working all the way through to the web controller.
#Query("select d from Device d left join d.site s left join d.organization o "
+ "where lower(d.externalId) like concat('%', lower(:search),'%')"
+ "or lower(s.externalId) like concat('%', lower(:search),'%')"
+ "or lower(o.externalId) like concat('%', lower(:search),'%')")
Page<Device> focusedSearchByValue(String search, Pageable pageable);
I had to add select d so that I would get back a list of Devices and not of Object.
I then had to make sure both the search term and the target column value of were in lower-case so that the comparison is "apples to apples".
Finally, I added the equivalent of ilike %{search}% because apparently there is no HQL support for ilike.
As a side note, for experimentation purposes, I also looked at what this equivalent approach generated to further explore the problem.
Page<Device> findAllByExternalIdContainsOrSiteExternalIdContainsOrOrganizationNameContains(String v1, String v2, String v3, Pageable pageable);

Related

JPA #OneToMany but with only one rekord

I have a table in the database that has a #OneToMany link to another table, JPA in standard form will return me the values from the other table as a list, however I would like to get the records as :
SELECT * FROM a LEFT JOIN b ON b.a_id = a.id
so if there are 2 records in the table "b" then I should get a list of 2 elements, not a one element list with a list inside that has values from table "b". Additionally, I will point out that I care to implement this by the function "Page findAll(#Nullable Specification spec, Pageable pageable);".
Example entity:
#Entity
public class A {
private Long id;
#OneToMany
private List<B> b;
soo i like to look like this
#Entity
public class A {
private Long id;
#OneToOne
private B b;
But when theres more that one B i will get second rekord instede of error.
What can I do to achieve this?

Put Reference from Audit table to Another Table in Hibernate Envers

I'm using Hibernate Envers for Auditing Change Data, I have a Class that store information about companies like this :
#Getter
#Setter
#Entity
#Table(name = "COMPNAY")
#Audited
public class Compnay {
private String name;
private String code;
}
and it's using Envers for keeping the changes of companies.
also, I have a class for Keep the data of items that manufacture in any of this company, the class will be like this :
#Getter
#Setter
#Entity
#Table(name = "COMPNAY")
#Audited
public class Item {
#Column(name = "NAME", nullable = false)
private String name ;
#Column(name = "CODE", nullable = false)
private String code;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "COMPANY_ID", nullable = false)
private Compnay compnay;
}
Consider that there is a company in company table like this :
ID
NAME
CODE
1
Apple
100
2
IBM
200
and the data in the item's table will be like this :
ID
NAME
CODE
COMPANY_ID
3
iPhone
300
1
4
iPad
400
1
if I edit the information of Apple company and change the code from 100 to 300 how can I fetch the information of Items that were saved before this change with the previous code? Is there is any way to reference to audit table?
Yes, you can write a HQL query that refers to the audited entities. Usually, the audited entities are named like the original ones, with the suffix _AUD i.e. you could write a query similar to the following:
select c, i
from Company_AUD c
left join Item_AUD i on i.id.revision < c.id.revision
where c.originalId = :companyId

Spring Data + View with Union return duplicate rows

i'm using Spring Boot 2.4.2 and Data module for JPA implementation.
Now, i'm using an Oracle View, mapped by this JPA Entity:
#Entity
#Immutable
#Table(name = "ORDER_EXPORT_V")
#ToString
#Data
#NoArgsConstructor
#EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class OrderExportView implements Serializable {
private static final long serialVersionUID = -4417678438840201704L;
#Id
#Column(name = "ID", nullable = false)
#EqualsAndHashCode.Include
private Long id;
....
The view uses an UNION which allows me to obtain two different attributes of the same parent entity, so for one same parent entity (A) with this UNION I get the attribute B in row 1 and attribute C in row 2: this means that the rows will be different from each other.
If I run the query with an Oracle client, I get the result set I expect: same parent entity with 2 different rows containing the different attributes.
Now the issue: when I run the query with Spring Data (JPA), I get the wrong result set: two lines but duplicate.
In debug, I check the query that perform Spring Data and it's correct; if I run the same query, the result set is correct, but from Java/Spring Data not. Why??
Thanks for your support!
I got it! I was wrong in the ID field.
The two rows have the same parent id, which is not good for JPA, which instead expects a unique value for each line.
So, now I introduced a UUID field into the view:
sys_guid() AS uuid
and in JPA Entity:
#Id
#Column(name = "UUID", nullable = false)
#EqualsAndHashCode.Include
private UUID uuid;
#Column(name = "ID")
private Long id;
and now everything works fine, as the new field has a unique value for each row.

Fetch child entities when finding by a normal field in Spring Data JPA

I am using Spring Data JpaRepository to find List of entities matching a particular field. Consider the following code snippet:
Entity:
#Entity
#Table(name = "master")
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public class Master implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
#SequenceGenerator(name = "sequenceGenerator")
#Column(name = "id", nullable = false)
private Long Id;
#NotNull
#Column(name = "user_id", nullable = false)
private String userId;
#OneToOne(fetch = FetchType.EAGER)
#JoinColumn(name="id", referencedColumnName="id", insertable=false, updatable=false)
private Details Details;
Spring Data Custom JpaRepository:
public interface MasterRepository extends JpaRepository<Master,Long> {
List<Master> findMasterByUserId(String userId);
}
When i am using findBookingMasterByUserId repository method to find all records with specific user id, I am getting the List of Master entity but I am not getting the Details entity that has id as foreign key in it.
However, I get all the dependent entities when I use out of the box findAll method of JpaRepository but with custom findMasterByUserId repository method, child entities are not being fetched eagerly.
Any type of help would be highly appreciated. Thanks!
You can use #EntityGraph in your repo to eagerly get associated data:
#EntityGraph(attributePaths = {"details"})
List<Master> findBookingMasterByUserId(String userId);
P.S. Don't forget to change 'Details' field to details;
Your entity name is "Master" not "booking_master".
Change your method to:
List<Master> findByUserId(String userId);
Refer to below spring docs for more information on query creation mechanism for JPA.
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/
Alternatively,
#Query("SELECT m FROM Master m WHERE m.userId = :userId")
List<Master> findByUserId(#Param("userId") String userId);
The query generation from the method name is a query generation strategy where the invoked query is derived from the name of the query method.
We can create query methods that use this strategy by following these rules:
The name of our query method must start with one of the following
prefixes: find…By, read…By, query…By, count…By, and get…By.
If we want to limit the number of returned query results, we can add
the First or the Top keyword before the first By word. If we want to
get more than one result, we have to append the optional numeric
value to the First and the Top keywords. For example, findTopBy,
findTop1By, findFirstBy, and findFirst1By all return the first entity
that matches with the specified search criteria.
If we want to select unique results, we have to add the Distinct
keyword before the first By word. For example, findTitleDistinctBy or
findDistinctTitleBy means that we want to select all unique titles
that are found from the database.
We must add the search criteria of our query method after the first
By word. We can specify the search criteria by combining property
expressions with the supported keywords.
If our query method specifies x search conditions, we must add x
method parameters to it. In other words, the number of method
parameters must be equal than the number of search conditions. Also,
the method parameters must be given in the same order than the search
conditions.

JPA Bi-directional OneToMany does not give me the desired collection

Im trying to map a oracle db model with JPA entities (Using eclipselink) - i have following simple setup :
table program with primary key id
table multi_kupon with compound primary keys id, spil and foreign key program_id
when i try to fetch program with a simple select i would expect the go get a list
of multi_kupon's but im getting a list of size 0. I've made certain that when i do
a select with joins i get data from both program and multi_kupon.
I believe that its got to do with my relations of the 2 entities - hope somebody can point me to my mistake(s)
Snippet from Entity 'Program' :
#Entity
#Table(name = "PROGRAM", schema = "", catalog = "")
public class Program implements Serializable{
#Id
private Integer id;
private List<MultiKupon> MultiKuponList;
#OneToMany(mappedBy = "program", fetch = FetchType.EAGER)
#JoinColumns({#JoinColumn(name = "id", referencedColumnName = "id"),
#JoinColumn(name = "spil", referencedColumnName = "spil")})
public List<MultiKupon> getMultiKuponList() {
return multiKuponList;
}
Snippet from Entity 'MultiKupon' :
#Entity
#Table(name = "MULTI_KUPON", schema = "", catalog = "")
public class MultiKupon implements Serializable {
private Integer id;
private String spil;
private Program program;
#ManyToOne
public Program getProgram() {
return program;
}
My stateless bean :
#Stateless
public class PostSessionBean {
public Program getProgramById(int programId) {
String programById = "select p from Program p where p.id = :id";
Program program = null;
try {
Query query = em.createQuery(programById);
query.setParameter("id", programId);
program = (Program) query.getSingleResult();
I do get the correcct program entity with data, but the list with
multi_kupon data is size 0
What im i doing wrong here ??
The mapping is incorrect, as you have specified both that the OneToMany is mappedBy = "program" and that it should use join columns. Only one or the other should be used, and since there is a 'program' mapping, I suggest you use:
#OneToMany(mappedBy = "program", fetch = FetchType.EAGER)
public List getMultiKuponList()
And then define the join columns on MultiKupon's ManyToOne mapping to define which fields should be used for the foreign keys to the Program table.
Since you have made this a bidirectional relationship, you must also maintain both sides of your relationship. This means that every time you add a MultiKupon and want it associated to a Program you must both have it reference the Program, AND add the instance to the Program's collection. If you do not, you will run into this situation, where the cached Program is out of sync with what is in the database.
It is much cheaper generally to keep both sides in sync, but if this is not an option, you can correct the issue (or just verify that this is the situation) by calling em.refresh(program). If the mappings are setup correctly, the instance and its list will be repopulated with what is in the database.
According to http://sysout.be/2011/03/09/why-you-should-never-use-getsingleresult-in-jpa/
Try to retrieve first program object of List:
List<Program> pList = query.getResultList();
if(!pList.isEmpty()){
return pList.get(0);
}

Resources