JPA #OneToMany but with only one rekord - spring

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?

Related

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.

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

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);

Join 2 tables using Spring JPA

I have two entities(Invoice and InvoiceItems) without adding any relationship.
Invoice
public class Invoice {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long invoiceID;
#Column(name="code")
private String code;
//other columns
}
Invoice Items
public class InvoiceItems {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long invItemID;
#Column(name="invoice_id")
private Integer invoiceId;
//other columns
}
Can I join these entities and get data without adding relationship using JPA?
If it isn't possible how to join 2 entities using JPQL or Native query?
If your data is valid then using native query you can do that
#Query(nativeQuery = true, "select * from Invoice i join InvoiceItems im on i.id = im.invoice_id")
public List<Invoice> findData();
But that is not a good way join without relation using JPA.
Yes, you can join these entities and get data without adding relationship using JPA, but it's a little bit losing the purpose of using JPA.
You need to create a java class first, which will be the returning data object from the DB. After that you can use entityManager's createNamedQuery method to get the result.
createNamedQuery(String sqlString, ResultClass.Class)
sqlString may be something like:
SELECT INV.INVOICE_ID
INV.CODE
INV_ITEMS.INV_ITEM_ID
FROM INVOICE INV
JOIN INVOICE_ITEMS INV_ITEMS
ON INV.INVOICE_ID = INV_ITEMS.INVOICE_ID;
And the corresponding ResultClass:
public class ResultClass {
private Long invoiceID;
private String code;
private Long invItemID;
// other columns
}
Or you can even use RowMapper to map the object all by yourself for more flexibility by using JdbcTemplate with query() method.

Spring Data find by inner relation

My question is about the way Spring data is generating the query .
I have two entities : Message , Sender
#Entity
public class Message extends BaseEntity {
#ManyToOne
protected Account sender;
}
I have a call to
messageDao.findBySenderId(Long id)
The result is query all columns from the two two table with a left outer join between the two tables , but my expectation was simply to just select from message table where sender_id = the passed value.
So is there a way to force selecting only the first message entity and not to join with the other one? I want simple condition in the where clause
by using findBy not custom #Query
You will need a repository like (untested) :
#Repository
public interface MessageRepository extends JpaRepository<Message, Long> {
Message findFirstBySenderId(Long id);
}
See repositories.query-methods
I think Hibernate is doing a LEFT JOIN because your #ManyToOne is optional = true (default).
Try:
#ManyTone(optional = false)
And you will see Hibernate doing a query without a JOIN, as you expected.
You can use fetch type LAZY for the associations types:
#ManyToOne(optional = false, fetch = FetchType.LAZY)

Sort by joined table's field Spring JPA

I have two entity classes Request,User:
//Ommiting some annotations for brevity
public class User{
private Long id;
private String name;
private Integer age;
}
public class Request{
private Long id;
private String message;
private Date createTime;
#ManyToOne
#JoinColumn(name="user_id")
private User user;
}
I can sort request list by create time :
Sort = new Sort(Direction.ASC,"createTime");
Is there a possible way to sort request list by User's name? Like:
Sort = new Sort(Direction.ASC,"User.name");
Yes. new Sort(Direction.ASC,"user.name"); should work just fine.
Spring Data JPA will left outer join the User to the Request and order by the joined column's name resulting in SQL like this:
select
id, message, createTime
from
Request r
left outer join User u on u.id = r.user_id
order by
u.name asc
This works great on one-to-one and, like you have here, many-to-one relationships but because a left outer join is employed to implement the order by clause, if the joined entity represents a many relationship (like in a one-to-many), the Sort may result in SQL in which duplicate records are returned. This is the case because the Sort parameter will always result in a new left outer join even if the entity being joined is already joined in the query!
Edit Incidentally, there is an open ticket concerning this issue: https://jira.spring.io/browse/DATAJPA-776

Resources