I am quite a beginner with Spring Data, and I have to code my first difficult query :)
I am making a reservation engine for an event. My data model is composed by:
a RoomType entity defining a possible configuration for a room (e.g. double, triple, quadruple)
a Room entity representing the actual Room
a RoomArrangement entity defining all the possible RoomTypes for a Room (e.g. the room 7 can be configured as Triple or Double room)
a RoomAssignment entity representing the actual configuration chosen for a room after having been reserved
FKs are configured this way
RoomType <--* RoomArrangement *--> Room <-- RoomAssignment
(see code below for Entity definition)
I need to find the Room without an Assignment that has the highest Priority (= nearest to 1) for a given RoomType.
I have configured Spring as below.
RoomType.java
#OneToMany(fetch = FetchType.LAZY)
#JoinColumn(name = "room_type_id")
private List<RoomArrangement> roomArrangements;
RoomArrangement.java
#ManyToOne(targetEntity = RoomType.class)
#JoinColumn(name = "room_type_id", nullable = false)
private RoomType roomType;
#ManyToOne(targetEntity = Room.class)
#JoinColumn(name = "room_id", nullable = false)
private Room room;
#Column(name = "priority")
private Integer priority;
Room.java
#OneToMany(fetch = FetchType.LAZY)
#JoinColumn(name = "room_id")
private List<RoomArrangement> roomArrangements;
#OneToOne(fetch = FetchType.LAZY, mappedBy = "room", cascade = CascadeType.ALL)
private RoomAssignment assignment;
RoomArrangementRepository.java
RoomArrangement findFirstByRoomTypeAndRoom_AssignmentIsNullOrderByPriorityAsc(RoomType rt);
The query above is translated to
select
roomarrang0_.id as id1_3_,
roomarrang0_.priority as priority2_3_,
roomarrang0_.room_id as room_id3_3_,
roomarrang0_.room_type_id as room_typ4_3_
from
room_arrangements roomarrang0_
left outer join
rooms room1_ on roomarrang0_.room_id=room1_.id
where
roomarrang0_.room_type_id=9
and
(room1_.id is null)
order by
roomarrang0_.priority asc
limit 1;
The issues are two:
I do not know where the where clause
(room1_.id is null)
comes from
I do not know where the "AndRoom_AssignmentIsNull" clause has gone
Should I "invert" the OneToOne relationship and put the FK on the Room class?
Thanks for your help!
Lorenzo
I've tried to apply some of the suggestions, and "turned" the query on the RoomRepository.
The query came like this:
Room findFirstByRoomArrangements_RoomTypeAndAssignmentIsNullOrderByRoomArrangements_PriorityAsc(RoomType rt);
We come to the same problem:
select
room0_.id as id1_6_,
room0_.room_name as room_nam2_6_
from
rooms room0_
left outer join
room_arrangements roomarrang1_ on room0_.id=roomarrang1_.room_id
where
roomarrang1_.room_type_id=?
and
(room0_.id is null)
order by
roomarrang1_.priority asc
limit ?;
I think the problem lies in the fact that the one-to-one relationship between Room and RoomAssignment is represented on the database with a FK on the room_assignments table to the rooms table.
I will try to put the FK on the rooms table instead of on the room_assignments table and see if something changes.
If you need to find a room without an without an Assignment, shouldn't you be looking that in the Room repository?
You said that you have to find a Room but you are returning a RoomArrangement in a RoomAssignmentRepository. This is quite confusing.
Let's assume you are in right place, that means RoomRepository and as you said:
I need to find the Room without an Assignment that has the highest Priority (= nearest to 1) for a given RoomType.
try to use the following method name
Room findByRoomArrangementRoomTypeAndRoomAssignmentIsNullOrderByPriorityAsc(RoomType rt)
Made it!
In the end, the problem lied in the fact that the one-to-one relationship between Room and RoomAssignment was mapped by an FK from RoomAssignment to Room. Apparently, Spring Data didn't manage this configuration properly: the Assignment is null was translated to room_assignment.room_id = null and, since room_id was an FK to room.id, to room.id = null.
Inverting the FK mapping, the query is translated to
select
room0_.id as id1_6_,
room0_.assignment_id as assignme3_6_,
room0_.room_name as room_nam2_6_
from
rooms room0_
left outer join room_arrangements roomarrang1_ on room0_.id=roomarrang1_.room_id
where
roomarrang1_.room_type_id=?
and (room0_.assignment_id is null)
order by
roomarrang1_.priority asc
limit ?
which correctly returns what I needed.
Related
I am working on a project and I have felt like in #OneToMany Unidirectional association with #JoinColumn in JPA with springboot generates extra queries. For example if we have 2 entities
#Entity(name = "Post")
#Table(name = "post")
public class Post {
#Id
#GeneratedValue
private Long id;
private String title;
#OneToMany(
cascade = CascadeType.ALL,
orphanRemoval = true
)
#JoinColumn(name = "post_id")
private List<PostComment> comments = new ArrayList<>();
//Constructors, getters and setters removed for brevity
}
#Entity(name = "PostComment")
#Table(name = "post_comment")
public class PostComment {
#Id
#GeneratedValue
private Long id;
private String review;
//Constructors, getters and setters removed for brevity
}
when we run the following code
Post post = new Post("First post");
post.addComment(
new PostComment("My first review")
);
post.addComment(
new PostComment("My second review")
);
post.addComment(
new PostComment("My third review")
);
entityManager.persist(post);
it generates following queries
insert into post (title, id)
values ('First post', 1)
insert into post_comment (review, id)
values ('My first review', 2)
insert into post_comment (review, id)
values ('My second review', 3)
insert into post_comment (review, id)
values ('My third review', 4)
update post_comment set post_id = 1 where id = 2
update post_comment set post_id = 1 where id = 3
update post_comment set post_id = 1 where id = 4
Now my question is that why does JPA updates post_comment records after inserting post_comment records ? why didn't JPA insert the post_comment records with the post_id while it was inserting records in post_comment table so that it don't have to update records again ?
You are depending on the CascadeType.ALL to insert the PostComments for you. Why is that? I consider that a JPA anti-pattern. JPA specifies that the owner of the relationship will persist relations. See If the relationship is bidirectional, the mappedBy element must be used to specify the relationship field or property of the entity that is the owner of the relationship. You have not specified an owner of the relationship.
What in the JPA specification can you point to that dictates that using the CascadeType should be implemented in any specific fashion?
What you show kind of makes sense to me. The Post is saved, the PostComments are saved b/c of the CascadeType, and then the Post id updated into the PostComments. You didn't set the Post id into the PostComments yourself so you don't have any say about how the implementation does it. I have seen CascaseType do some interesting things, but it's still an anti-pattern as far as I'm concerned. If you don't understand it, don't use it!
Besides that, you have also added a static = new ArrayList<>() on comments. Another anti-pattern. The majority of those new ArrayLists will be tossed onto the garbage heap in very short order. Even though Java manages memory for you, you should have some idea of how you are using it.
Short answer, the PostComments should be saved by you specifically, but only when you already have saved the Post and it is ready to be set into the PostComment. Of course, you should have the Post and the ManyToOne in the PostComment so you can do that.
I don't know if you've investigated the Query behaviors of the related Entities, but I think you'll find more surprises there as well. Since I have already done that, I make these comments.
References:
What is the "owning side" in an ORM mapping?
37.1 Entities
I have a question about performance and common practice, if someone could explain this to me.
I have recently started using JPA and hibernate and have come across an Entity that has a foreign key and I need to get some data from it. So for example: CustomerAddress has a City and that city has a lot of detail and also a name.
SQL:
select
CA.Id, CI.Name
from
CustomerAddress as CA
inner join City as CI
on CA.CityID = CI.Id
So now in Java JPA Entity I can have a one-to-many annotation:
#Entity
#Table(name = "CustomerAddress")
public class CustomerAddressEntity {
#Id
#Column(name = "Id", unique = true, nullable = false)
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
#NotFound(action = NotFoundAction.IGNORE)
#JoinColumn(name = "city", referencedColumnName = "id", insertable = false, updatable = false)
private City city;
}
Where City is also an #Entity with #Id and simple object.
Which in my opinion does not turn out the best because it makes a lot of SQL requests.
And then I have the option having just two findAll() calls at the beginning, where I would collect all the City Entities in a HashMap<String, City> and when needing the name I would just call hashmap.get(key).getName().
EDIT (thanks for the heads up :)):
And when using this HashMap I can use a simpler Entity without the #JoinColumn
#Entity
#Table(name = "CustomerAddress")
public class CustomerAddressEntity {
#Id
#Column(name = "Id", unique = true, nullable = false)
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#Column(name = "CityID")
private Long cityId;
}
In the hashmap case I only get two SQL calls and I think it works much faster. Is there a way to get this behavior also using JPA and hibernate?
If my question and code needs some more refinement please let me know.. I can edit the question with more details and perhaps if necessary provide a working example. Thank you for your thoughts :)
And the same would go for OneToMany, where the hashmap would be: new HashMap<String, List<City>> for example - I mean the whole example should be created a bit differently - I guess it could even be a HashMap<String, HashMap<String,City>> - if one would need quick access to the City by Id or sth... but i digress :) I will edit the question and respond to comments as I will go.. and refine the question if necessary.. I would just like to hear some thoughts and where my thinking is wrong :) and what am I failing to see and missing :)
EDIT: For example a code that would create a lot of SQL requests:
public interface CustomerAddressRepository extends JpaRepository<CustomerAddressEntity, Long> {
#Override
List<CustomerAddressEntity> findAll();
}
This for example creates an SQL Query (I would use findAll() at the beginning to list all - or most of the Entities for the user) and you would get an SQL query for every Entity because it would want to find the Name of the City as well - because the ID of the City Entity does not really help to the user.
Also - I like to have all the Entities in my RAM so I can do a quick search for the user more responsive - So a search does not always do SQL Query + #(found results) Queries.
The HashMap has nothing to do with the fact that Hibernate issues a query when you want to get the name of the City object.Here's why it's happening.
In your CustomerAddressEntity you have a OneToOne with City , and since you have a #JoinColumn there ,it means that CustomerAddressEntity database Table will have the Primary Key of the City table as a foreign key , and since you specified in your class that it should be fetched LAZY,Hibernate will create a Proxy object wrapping the City object,ready to get queried from the database in case you call any getMethod ,like getName() ,(excluding the getId() method since the ID exists prealably in the proxy object,you can check the sql query logs and see that the query selects the foreign key with all the other fields of CustomerAddressEntity ),that's why when you trigger the getName() method Hibernate will fetch that entity from the database.
I'm trying to query for a list of entities (MyOrders) that have mappings to a few simple sub-entities: each MyOrder is associated with exactly one Store, zero or more Transactions, and at most one Tender. The generated SELECT appears correct - it retrieves all the columns from all four joined tables - but afterwards, two more SELECTs are executed for each MyOrder, one for Transactions and one for Tender.
I'm using QueryDSL 4.1.3, Spring Data 1.12, JPA 2.1, and Hibernate 5.2.
In QueryDSL, my query is:
... = new JPAQuery<MyOrder>(entityManager)
.from(qMyOrder)
.where(predicates)
.join(qMyOrder.store).fetchJoin()
.leftJoin(qMyOrder.transactions).fetchJoin()
.leftJoin(qMyOrder.tender).fetchJoin()
.orderBy(qMyOrder.orderId.asc())
.transform(GroupBy
.groupBy(qMyOrder.orderId)
.list(qMyOrder));
which is executed as:
SELECT myorder0_.ord_id AS col_0_0_,
myorder0_.ord_id AS col_1_0_,
store1_.sto_id AS sto_id1_56_1_, -- store's PK
transactions3_.trn_no AS trn_no1_61_2_, -- transaction's PK
tender4_.tender_id AS pos_trn_1_48_3_, -- tender's PK
myorder0_.ord_id AS ord_id1_39_0_,
myorder0_.app_name AS app_name3_39_0_, -- {app_name, ord_num} is unique
myorder0_.ord_num AS ord_num8_39_0_,
myorder0_.sto_id AS sto_id17_39_0_,
store1_.division_num AS div_nu2_56_1_,
store1_.store_num AS store_nu29_56_1_,
transactions3_.trn_cd AS trn_cd18_61_2_,
tx2myOrder2_.app_name AS app_name3_7_0__, -- join table
tx2myOrder2_.ord_no AS ord_no6_7_0__,
tx2myOrder2_.trn_no AS trn_no1_7_0__,
tender4_.app_name AS app_name2_48_3_,
tender4_.ord_num AS ord_num5_48_3_,
tender4_.tender_cd AS tender_cd_7_48_3_,
FROM data.MY_ORDER myorder0_
INNER JOIN data.STORE store1_ ON myorder0_.sto_id=store1_.sto_id
LEFT OUTER JOIN data.TX_to_MY_ORDER tx2myOrder2_
ON myorder0_.app_name=tx2myOrder2_.app_name
AND myorder0_.ord_num=tx2myOrder2_.ord_no
LEFT OUTER JOIN data.TRANSACTION transactions3_ ON tx2myOrder2_.trn_no=transactions3_.trn_no
LEFT OUTER JOIN data.TENDER tender4_
ON myorder0_.app_name=tender4_.app_name
AND myorder0_.ord_num=tender4_.ord_num
ORDER BY myorder0_.ord_id ASC
which is pretty much what I'd expect. (I cut out most of the data columns for brevity, but everything I need is SELECTed.)
When querying an in-memory H2 database (set up with Spring's #DataJpaTest annotation), after this query executes, a second query is made against the Tender table, but not Transaction. When querying a MS SQL database, the initial query is identical, but additional queries happen against both Tender and Transaction. Neither makes additional calls to load Store.
All the sources I've found suggest that the .fetchJoin() should be sufficient (such as Opinionated JPA with Query DSL; scroll up a few lines from the anchor) and indeed if I remove them, the initial query only selects columns from MY_ORDER. So it appears that .fetchJoin() does force generation of a query that fetches all the side tables in one go, but for some reason that extra information isn't being used. What's really weird is that I do see the Transaction data being attached in my H2 quasi-unit test without a second query (if and only if I use .fetchJoin() ) but not when using MS SQL.
I've tried annotating the entity mappings with #Fetch(FetchMode.JOIN), but the secondary queries still fire. I suspect there might be a solution involving extending CrudRepository<>, but I've had no success getting even the initial query correct there.
My primary entity mapping, using Lombok's #Data annotations, other fields trimmed out for brevity. (Store, Transaction, and Tender all have an #Id a handful of simple numeric and string field-column mappings, no #Formulas or #OneToOnes or anything else.)
#Data
#NoArgsConstructor
#Entity
#Immutable
#Table(name = "MY_ORDER", schema = "Data")
public class MyOrder implements Serializable {
#Id
#Column(name = "ORD_ID")
private Integer orderId;
#NonNull
#Column(name = "APP_NAME")
private String appName;
#NonNull
#Column(name = "ORD_NUM")
private String orderNumber;
#ManyToOne
#JoinColumn(name = "STO_ID")
private Store store;
#OneToOne
#JoinColumns({
#JoinColumn(name = "APP_NAME", referencedColumnName = "APP_NAME", insertable = false, updatable = false),
#JoinColumn(name = "ORD_NUM", referencedColumnName = "ORD_NUM", insertable = false, updatable = false)})
#org.hibernate.annotations.ForeignKey(name = "none")
private Tender tender;
#OneToMany
#JoinTable(
name = "TX_to_MY_ORDER", schema = "Data",
joinColumns = { // note X_to_MY_ORDER.ORD_NO vs. ORD_NUM
#JoinColumn(name = "APP_NAM", referencedColumnName = "APP_NAM", insertable = false, updatable = false),
#JoinColumn(name = "ORD_NO", referencedColumnName = "ORD_NUM", insertable = false, updatable = false)},
inverseJoinColumns = {#JoinColumn(name = "TRN_NO", insertable = false, updatable = false)})
#org.hibernate.annotations.ForeignKey(name = "none")
private Set<Transaction> transactions;
/**
* Because APP_NAM and ORD_NUM are not foreign keys to TX_TO_MY_ORDER (and they shouldn't be),
* Hibernate 5.x saves this toString() as the 'owner' key of the transactions collection such that
* it then appears in the transactions collection's own .toString(). Lombok's default generated
* toString() includes this.getTransactions().toString(), which causes an infinite recursive loop.
* #return a string that is unique per order
*/
#Override
public String toString() {
// use appName + orderNumber since, as they are the columns used in the join, they must (?) have
// already been set when attaching the transactions - primary key sometimes isn't set yet.
return this.appName + "\00" + this.orderNumber;
}
}
My question is: why am I getting redundant SELECTs, and how can I not do that?
I'm a little too late on the answer, but today the same problem happened to me. This response might not help you, but at least it would save someone the headache we went through.
The problem is on the relations between the entities, not in the query. I tried with QueryDSL, JPQL, and even native SQL but the problem was always the same.
The solution was to trick JPA into believing that the relations were there via annotating the child classes with #Id on those joined fields.
Basically you'll need to set Tender's id like this and use it from MyOrder like if it was a normal relationship.
public class Tender {
#EmbeddedId
private TenderId id;
}
#Embeddable
public class TenderId {
#Column(name = "APP_NAME")
private String appName;
#Column(name = "ORD_NUM")
private String orderNumber;
}
The same would go for the Transaction entity.
I have 3 entities, infact many more joined together for brevity i'm skipping those and i'm using open jpa 2.2.2 and oracle 11g. Any thoughts what's going wrong here?
Entity SystemRules{
#OneToMany(mappedBy = "systemRule", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
private List<ServiceActionMap> serviceActionMap;
}
Entity ServiceActionMap{
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "SYSTEM_RULE_ID")
private SystemRules systemRule;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "RFS_TYPE_ID", nullable = true)
private RfsTypeMap rfsType;
}
Entity RfsTypeMap{
#Id
#Column(name="RFS_TYPE_ID" ,nullable=false)
private BigDecimal rfsTypeId;
#Column(name="RFS_NAME")
private String rfsName;
}
Now, I am trying to order the result by RfsTypeMap.rfsName, i'm creating query using criteria builder in following manner
CriteriaQuery<SystemRules> query = cb.createQuery(SystemRules.class);
Root<SystemRules> root= query.from(SystemRules.class);
root.fetch(SystemRules_.serviceActionMap).fetch(ServiceActionMap_.rfsType, JoinType.LEFT);
My order by clause is like this
cb.desc(cb.upper(systemRules.get("serviceActionMap").get("rfsType").get("rfsName").as(String.class)));
Generate JPQL looks like, where i expect a left outer join clause between ServiceActionMap and RfsTypeMap but it's missing. Same gets translated in SQL and i miss those records which are having ServiceActionMap.rfsType as null value.
SELECT DISTINCT s FROM SystemRules s INNER JOIN FETCH s.serviceActionMap INNER JOIN FETCH s.serviceActionMap INNER JOIN FETCH s.ruleProperty INNER JOIN FETCH s.ruleProperty where ... ORDER BY UPPER(s.serviceActionMap.rfsType.rfsName)
I tried going over several answers here but no success, tried explicitly putting a where clause for ServiceActionMap.rfsType is null as suggested on few answers but it's getting ignored, since join happens before where evaluation. Somewhere this question openJPA outer join on optional many-to-one when have order by clause matches my scenario but not able to generate suggested JPQL through criteria API.
I found one related bug on apache jira https://issues.apache.org/jira/browse/OPENJPA-2318. But, not sure that's the case.
I see that every join is repeated twice and even the join alias is always referring to SystemRules. It might be that orderby has caused the repeating inner joins and we may need to explicitly use Join object to refer to extended column.
CriteriaQuery<SystemRules> query = cb.createQuery(SystemRules.class);
Root<SystemRules> root = query.from(SystemRules.class);
Join<SystemRules, ServiceActionMap> join1 = root.join(SystemRules_.serviceActionMap, JoinType.INNER);
Join<ServiceActionMap, RfsTypeMap> join2 = join1.join(ServiceActionMap_.rfsType, JoinType.LEFT);
query.orderBy(cb.desc(cb.upper(join2.get(RfsTypeMap_.rfsName))));
So, here is the problem that I got.
I am building a database model structure by JPA which has a many to many relation, here is the class model
public class UserActivityRelation{
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "user_id",nullable = false)
private User participant;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "activity_id",nullable = false)
private Activity activity;
}
here is the corresponding DAO code that I followed the spring-doc spring-boot-jpa
public List<UserActivityRelation> findDistinctParticipantByParticipantAndActivity(User user, Activity activity);
hoping to distinct rows with same user_id, but it came with an unexpected result, the sql sentence is as follows:
select distinct useractivi0_.id as id1_1_, useractivi0_.created_time
as created_2_1_, useractivi0_.modified_time as modified3_1_,
useractivi0_.activity_id as activity5_1_, useractivi0_.user_id as
user_id6_1_, useractivi0_.status as status4_1_ from act_users
useractivi0_ left outer join users user1_ on useractivi0_.user_id=user1_.id
left outer join activities activity2_ on
useractivi0_.activity_id=activity2_.id where (user1_.id is null)
and (activity2_.id is null)
the sql code seems really complex, but the point is clear, findDistinct only has effect on the primary key id rather than the participant----user_id, and it uses a lot of left outer join...
At first I thought I write the method wrong, so I test it with another method name
findDistinctHEHEByParticipantAndActivity , as you can see it has HEHE in the middle which has nothing do to with model class, neither a property nor a method... and it just gave an identical sql sentence, which I was so confused to see...
So, is it just the way that JPA with hibernate works or did I do it wrong?