Spring boot JPA how to query a #OneToMany relationship given one object in the many relationship - spring

I've seen a few related questions but I can't seem to find the right answer for what I'm trying to do.
I have two tables Jobs and Workers, a job can have many workers, simplified entity
#Entity
#Table(name = "jobs")
data class Job(
#Id
#Type(type = "pg-uuid")
val id: UUID = UUID.randomUUID()
) {
#ManyToOne
var office: Office? = null
#OneToMany(targetEntity = Worker::class)
var requests: MutableList<Worker> = mutableListOf()
}
I want to be able to fetch a list of jobs for a specific worker
I've tried a few queries native and not, but trying to just do it by namedMethods now, whatever works I guess to be honest here is what seems like it should work in my jobs repo
#Repository
interface JobsRepo : CrudRepository<Job, UUID> {
#Query("SELECT j FROM Job j WHERE id = ?1")
fun findJobById(id: UUID): Job?
#Query("SELECT j FROM Job j WHERE office_id = ?1")
fun findJobsByOffice(id: UUID): List<Job>?
#Modifying
#Transactional
#Query("UPDATE jobs SET job_status = 4 WHERE job_status = 1 AND start_time < ?1", nativeQuery = true)
fun expireJobs(date: Date)
fun findByRequests_Worker(worker: Worker): List<Job>?
}
I'm not really sure how to query the array property
requests
with in input of one worker. I tried querying the UUID of the worker too since thats whats in the join table
JPA creates a join table with both foreign keys the table is
jobs_requests
and columns
job_id UUID
requests_id UUID

You mentioned that a Job can have many workers, and you also mentioned that you want to get a list of jobs for a specific worker. So this sounds like a ManyToMany relationship rather than OneToMany.
For a ManyToMany relationship, a join table is unavoidable. You need to specify #ManyToMany annotation on both entities, then you can use WorkerRepository to query for the worker and then to get the job list for that worker you will just need to access by worker.getJobs().
Following is the setup for the ManyToMany relationship, hope it can help:
#Entity
#Table(name = "jobs")
data class Job (
#ManyToMany
#get:ManyToMany
#get:JoinTable(
name = "worker_job",
joinColumns = [JoinColumn(name = "job_id")],
inverseJoinColumns = [JoinColumn(name = "worker_id")]
)
val worker: Set<Worker> = HashSet()
)
#Entity
#Table(name = "worker")
data class Worker (
#ManyToMany
#get:ManyToMany
#get:JoinTable(
name = "worker_job",
joinColumns = [JoinColumn(name = "worker_id")],
inverseJoinColumns = [JoinColumn(name = "job_id")]
)
val jobs: Set<Jobs> = HashSet()
)

Its also worth noting that for all practical uses, you should try to avoid explicit ManyToMany annotation. You should rather make a new Entity/Table which will have fields of a Job and Worker both with ManyToOne associations to their respective entities.
The reason why you should avoid explicit ManyToMany is because, say when there is a specific job for example JavaDev, and there are say 1000 Workers on the given job. And at one point you have a JavaDev entity and you want to get all the workers whose ages are between 20 and 25. To do this, one would iterate on the Workers list which is part of JavaDev entity and filter them out. When that happens there is 2 scenarios:
Workers list is a lazy association and when you try to access it hibernate will send 1000(n+1 problem) queries to the database.
Workers list is an eager association and hibernate will send same amount of queries to the database but as soon as you try to fetch JavaDev entity.
Hence your execution times will increase as there will be unnecessary amount of load on the database itself. And on larger projects this will get even more complicated.
All of this can be avoided if you create your own manytomany(job_workers for ex.) table, with accompanying repository and a service, from witch you'll be able to query your data more efficiently.

Related

How to dynamically select columns in Spring Data JPA

I have a typical Spring boot(2.7.6) application with apis' to fetch data in Kotlin.
Assume an entity called Employee
#Entity
data class Employee(
val id: Long,
val name: String,
val age: Int,
val interviewDate: LocalDate,
val joiningDate: LocalDate,
val resignationDate: LocalDate,
val lastWorkingDate: LocalDate
)
For brevity I have removed annotations like #Id etc from above entity class.
One of the APIs which vends out Employee data is such that, in request params I get something like dateType and it will have one of interviewDate/joiningDate/resignationDate/lastWorkingDate. And in request params dateFrom and dateTo, I get the date as an input like 2020-10-01 and 2022-12-30
For example, if api gets input like dateType=interviewDate&dateFrom=2020-10-01&dateTo=2022-12-30 then API has to return all the employee records whose interview_date column has values between 2020-10-01 and 2022-12-30
The example given above is just for ease of explaining. For real use-case have to fetch data from many tables and has many joins(inner/left/right).
Based on the input, what is the better way to select columns dynamically in repository method?
I tried Specification Criteria API, but it was a dead end because I cannot use joins as there is no mapping between Entities like #OneToMany etc.
I am trying with #Query to get data but have to duplicate lots of lines of sql for each condition.
Example of one of the queries I have written in repository class is like below:
#Query(
"""
select
t.a as A,
t.b as B,
tt.c as C,
p.d as D,
p.e as E
from Employee p
join Department t on p.some_id = t.id
join PersonalData tt on tt.id = t.some_id
left outer join SalaryInformation ps on p.id = ps.come_id
left outer join ManagerInformation sbt on p.some_id = sbt.id
. few more joins here
.
.
where p.id= :id and p.interviewDate>=:dateFrom and p.interviewDate<=:dateTo
""" ,
nativeQuery = true
)
fun findByEmployeeIdForInterviewDate(employeeId: Long, dateFrom:String, dateTo: String, pageable: Pageable): Slice<EmployeeDetailsProjection>
With current approach, I have to repeat this query for remaining date columns which I dont want to as it looks ugly.
Any better suggestions will be really helpful :)
I tried Specification Criteria API, but it was a dead end because I cannot use joins as there is no mapping between Entities like #OneToMany etc.
Hibernate 6 introduces a JPA Criteria extension API which you can use to model joins to entities. To use Hibernate 6 you will have to update to Spring 3 though. For every JPA Criteria interface, Hibernate introduces a sub-interface prefixed with Jpa that contains the Hibernate JPA Criteria extensions. You could do e.g.
JpaEntityJoin<SalaryInformation> ps = ((JpaRoot<?>) root).join(SalaryInformation.class, SqmJoinType.LEFT);
ps.on(cb.equal(root.get("id"), ps.get("comeId")));

spring data jpa + unwanted unique key generation

I am using spring data jpa for creation of my tables. My Requirement.
I have two Tables.
Basket Table - It has one to many Relationship with the Item table. A basket can have many Items.
Item Table - Here an Item can be associated with many Baskets.
I am using IGNORE_ROW_ON_DUPKEY_INDEX to make sure that the same combination of basketId and itemId, does not get persisted
So there is a mapping table in between in which holds the mapping of baskets and items. Now, in this table, i want the combination of basketId and itemId to be unique. Below is my entity stucture.
#Entity
Class Basket{
#Id
private long basketId;
...
#OneToMany(cascade = {CascadeType.Merge, CascadeType.DETACH, CascadeType.REFRESH})
#JoinTable(
name= "mapping_table",
joinColumns = #JoinColumn(name ='basketId'),
inverseJoinColumns = #JoinColumn(name ='itemId'),
indexes = {#Index(name = "my_index", columnList = "basketId, itemId", unique = true)}
#SQLInsert(sql = "INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX (mapping_table, my_index) */ INTO mapping_table(basketId, itemId) values (?,?)")
private List<Item> itemList;
...
}
#Entity
Class Item{
#Id
private long itemId;
}
my_index with the combination of both the keys are getting created, as expected in the mapping_table
Problem 1:
In the mapping_table, for some wierd reason, a new unique constraint with only itemId is created. Due to this unique key constraint, i am not able to persist rows where an item is associated with multiple baskets. As i said, i want the combination of both the keys to be unique and i am achieving this by creating the index (my_index)
Problem 2:
Why is basketId (which is also Identifier) in the basketTable not marked as unique in the mapping table. This not a problem but more of a question. Becuase itemId which is identifier in the item table has unique key constraint in the mapping table.
Solution :
I create the tables using spring data jpa, login to the db manually and drop the unique key constraint and the persist. But this is not possible in Production
Question
I want to do alter table to drop the constraint first before the persist thing happens. How to do that.
P.S, As you can see, I have imagined these tables and have not put the names of the actual table. Not withstanding the made up table names, the problem statement is accurate. I am using Oracle as target DB
Try to use a Set instead of a List, JPA should generated the correct schema with the correct constraints.

How to fetch the association table columns using Spring Data JPA?

I have Three tables 1) student 2) game and 3)student_game. Here studentid is the primary key of student table and gameid is the primary key of the game table. The association table has four columns 1) uuid 2) studentid 3) gameid 4) gametype.
I am using the spring data jpa, So my student entity class has relation as follows,
#OneToMany(fetch = FetchType.EAGER)
#JoinTable(name = "student_game", joinColumns = #JoinColumn(name = "studentid"), inverseJoinColumns = #JoinColumn(name = "gameid"))
private Set<Game> GameSet;
Game entity has the following relation,
#OneToMany(fetch = FetchType.EAGER)
#JoinTable(name = "student_game", joinColumns = #JoinColumn(name = "gameid"), inverseJoinColumns = #JoinColumn(name = "studentid"))
private Set<Student> studentSet;
In my business layer, I am able to fetch the set of game names for respective student by using the following logic,
Student s = studentRepository.findOne(1L);
Set<Game> games= dd.getGamesSet();
games.forEach(game-> System.out.println(game.getGameId() + " : " + game.getGameName()));
Now My question is,
I need to fetch the gameType column value from the association table (student_game) by passing input as student id.
How can I fetch the association column values?
Please suggest the best approach to fetch the association table column values.
Your mapping is wrong:
you're mapping the same, bidirectional, ManyToMany association as two different OneToMany associations, but using the same join table. This can cause undefined behavior, since you might, in the same transaction remove a game from a player, and add the player to the game.
You can't use the student_game table as a join table: every time a game is added to a player (or vice-versa), since Hibernate doesn't know anything about the uuid and gametype columns, it won't insert anything into these two columns, and this is not what you want.
What you need todo is map the student_game table as another entity. Let's call it a Play. Then, a Play has a gameType, field, one User has multiple Plays; one Game has multiple Plays. And you may of course make these two OneToMany associations bidirectional.

JDBC: select entities with Many to one relation

I have the two entity classes with bi-directional Many-to-one relation.
class A {
#Column(name="ID")
long Id;
}
class B {
#ManyToOne
#JoinColumn(name="A_ID")
A a;
}
The entities are well-coded with additional data fields and getters and setters. And now I want to construct a query string to fetch data from table B, where B's "A_ID" column is equal to A's "ID".
I tried something like this:
"select b.data1, b.data2 from B b, A a WHERE b.a.Id=a.Id"
But it does not work. What is the correct way to construct such a query? And if A and B are in a uni directional relation, would there be any difference?
Thanks in advance.
You don't need to join the tables, the whole idea behind #ManyToOne and #OneToMany is to do away with the need for most joins.
I refer you to a tutorial on JPA, like http://en.wikibooks.org/wiki/Java_Persistence/ManyToOne and http://en.wikibooks.org/wiki/Java_Persistence/OneToMany.
Now, without seeing your actual db definitions it's a bit difficult to guess the actual structure of your program and database, but it should be something like this:
class A {
#Id
#Column(name="ID")
long Id;
#OneToMany(mappedBy="a")
List<B> bees;
}
class B {
#ManyToOne
#JoinColumn(name="A_ID") // Note that A_ID is a column in the B table!!!
A a;
}
With the example above you could just select any list of B's you need, and JPA will automatically fetch the associated A for each found B. You don't need to do anything to be able to access it, b.a.Id will just work.
As we also have the OneToMany relationship, every A can have multiple B's associated with it. So, for any select that fetches a set of A's, each returned A's bees field will give access to the proper list of B objects, without the need to pull the B able into the query.

JPA entitymanager remove operation is not performant

When I try to do an entityManager.remove(instance) the underlying JPA provider issues a separate delete operation on each of the GroupUser entity. I feel this is not right from a performance perspective, since if a Group has 1000 users there will be 1001 calls issued to delete the entire group and itr groupuser entity.
Would it make more sense to write a named query to remove all entries in groupuser table (e.g. delete from group_user where group_id=?), so I would have to make just 2 calls to delete the group.
#Entity
#Table(name = "tbl_group")
public class Group {
#OneToMany(mappedBy = "group", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
#Cascade(value = DELETE_ORPHAN)
private Set<GroupUser> groupUsers = new HashSet<GroupUser>(0);
Simple answer is yes.
If you want to delete a Group and you know there are tons of records in GroupUser table, then it is much better to create a delete query that will do all in one batch instead of one and one.
If you have no cascading on the underlying database, (or even if you do) its good practice to do it in correct order.
So delete the GroupUser first.
Assuming you have a the Group object you want to delete.
int numberDeleted = entityManager.createQuery("DELETE FROM GroupUser gu WHERE gu.group.id=:id").setParameter("id",group.getId()).executeUpdate();
The returning int shows how many records where deleted.
Now you can finally delete Group
entityManager.remove(group);
entityManager.flush();
UPDATE
Seems like #OnDelete on the #OneToMany does the trick
Since the GroupUser may have cascades as well, I don't think there is a way to tell hibernate to batch-delete them via configuration.
But if you are certain there are no cascade=DELETE on GroupUser, feel free to issue an HQL/JPA-QL query:
DELETE FROM GroupUser WHERE group=:group
If there are cascades, handle them with a query as well.

Resources