Use existing table joins for Hibernate Entity Criteria - java

Suppose I have an entity like the following, where each of the sets is a separate entity that has a foreign key relationship to the PersonEntity primary key.
PersonEntity - PK: person_id
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "person", orphanRemoval = true)
Set<AddressEntity> addresses
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "person", orphanRemoval = true)
Set<NameEntity> nameParts
Both the AddressEntity and NameEntity have a PersonEntity, which is the FK relationship expressed in entity form.
All tables also have a field called the tenant_id which they are partitioned on.
If I create HibernateCriteria like follows:
final Criteria criteria = sessionFactory.getCurrentSession().createCriteria(PersonEntity.class, "p");
criteria.add(Restrictions.eq("p.personId", personId));
criteria.add(Restrictions.eq("p.tenantId", tenantId));
I get SQL like:
select ALL_ATTRIBUTES_SNIPPED
FROM person this_
LEFT OUTER JOIN address addresses2_
ON this_.person_id=addresses2_.person_id
LEFT OUTER JOIN name nameparts4_
ON this_.person_id=nameparts4_.person_id
WHERE this_.person_id=?
AND this_.tenant_id=?
Looking at the explain plan, I see that this checks all the partitions when doing the join. That's unnecessary as it just needs to look in one partition.
What I would like to would be to add additional restrictions on all tables, such that all of them are restricted by tenant_id. So the SQL might look like this:
select ALL_ATTRIBUTES_SNIPPED
FROM person this_
LEFT OUTER JOIN address addresses2_
ON this_.person_id=addresses2_.person_id
LEFT OUTER JOIN name nameparts4_
ON this_.person_id=nameparts4_.person_id
WHERE this_.person_id=?
AND this_.tenant_id=?
AND addresses2_.tenant_id =?
AND nameparts4_.tenant_id =?
However, I can't seem to figure out how to create criteria to perform this. When I try something like the following:
final Criteria criteria = sessionFactory.getCurrentSession().createCriteria(PersonEntity.class, "p")
.createAlias("addresses", "address", JoinType.LEFT_OUTER_JOIN)
.createAlias("nameParts", "namePart", JoinType.LEFT_OUTER_JOIN)
criteria.add(Restrictions.eq("p.personId", personId));
criteria.add(Restrictions.eq("p.tenantId", tenantId));
criteria.add(Restrictions.eq("address.tenantId", tenantId));
criteria.add(Restrictions.eq("namePart.tenantId", tenantId));
I get SQL that looks like this:
select ALL_ATTRIBUTES_SNIPPED
FROM person this_
LEFT OUTER JOIN address addresses2_
ON this_.person_id=addresses2_.person_id
LEFT OUTER JOIN name nameparts4_
ON this_.person_id=nameparts4_.person_id
LEFT OUTER JOIN address addresses3_
ON this_.person_id=addresses3_.person_id
LEFT OUTER JOIN name nameparts1_
ON this_.person_id=nameparts1_.person_id
WHERE this_.person_id=?
and this_.tenant_id = ?
and addresses3_.tenant_id = ?
and nameparts1_.tenant_id = ?
As you can see, the tables are joined twice.
How can I create a restriction that uses the original tables? I don't see how I would be able to supply a restriction that would access the existing joins. I tried something like p.addresses.tenantId but it said addresses was not recognized.
Edit: I have largely resolved the query issue, by placing this line on the Set in PersonEntity and on the PersonEntity in the set entity (ie, AddressEntity).
#JoinColumns(value={
#JoinColumn(name="PERSON_ID", referencedColumnName="PERSON_ID", insertable=false, updatable=false),
#JoinColumn(name="TENANT_ID", referencedColumnName="TENANT_ID", insertable=false, updatable=false)
})
I also removed the mappedBy attribute for those columns.
This forces a join on both the person_id and tenant_id and makes the explain plan cost significantly better (as well as real world performance). However, I'm not sure if this is a real solution because it introduces a new problem.
My problem now is that when I try to create a PersonEntity, I get the following error:
12:09:26.672 WARN [main] org.hibernate.engine.jdbc.spi.SqlExceptionHelper - SQL Error: 1400, SQLState: 23000
12:09:26.672 ERROR [main] org.hibernate.engine.jdbc.spi.SqlExceptionHelper - ORA-01400: cannot insert NULL into ("USER"."ADDRESS"."PERSON_ID")
This occurs even though the SQL shows the person insert occurred just before the attempt at the address insert. It seems like the person_id is not being passed along to be placed into the address insert. How could I force Hibernate to do that? Previously, it just happened automatically (from my point of view).
I am using the sequence generator to create my primary keys, if that matters.

What resolved this for me was adding these lines to the collections in the PersonEntity and then on the PersonEntity field in the child entity classes (AddressEntity, NameEntity):
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumns(value={
#JoinColumn(name = "PERSON_ID", referencedColumnName = "PERSON_ID", nullable = false),
#JoinColumn(name = "TENANT_ID", referencedColumnName = "TENANT_ID", nullable = false)
})
public PersonEntity getPerson() {
return personEntity;
}
That worked for the query but I couldn't do inserts or updates, so the other thing I had to do was make sure that the existing tenantId field had insertable=false and updateable=false, like so:
#Column(name = "TENANT_ID", insertable = false, updatable = false)
public String getTenantId() {
return tenantId;
}
Then, executing the criteria in the original question would result in all child tables having a join on the PERSON_ID and the TENANT_ID, exactly as I wanted.
This changed my estimated cost from 2525 to 15 on the explain plan since it could go straight to the correct partition instead of looping through them.

Related

SpringBoot - Entity propagation for relationships

I have two entities: WorkoutTemplate and ConcreteExercise.
In WorkoutTemplate I have this relationship with ConcreteExercises
#OneToMany(cascade = CascadeType.ALL, mappedBy = "belongingWorkout", fetch = FetchType.LAZY)
private List<ConcreteExercise> concreteExercises;
And in ConcreteExercise I have this relationship with WorkoutTemplate
#ManyToOne
private WorkoutTemplate belongingWorkout;
I would like to insert a WorkoutTemplate into my database...
I make the request to the controller sending a Json like this:
{ "workoutName" : "My Workout",
concreteExercises: [
{
"name" : "Squat"
}
]
}
The DAO insert into my DB the WorkoutTemplate
And insert also in the table of the ConcreteExercise the name.
But not the reference to the WorkoutTemplate...
Practically, the table ConcreteExercise is made of:
id, name, belongin_workout_id
With the request above, we populate the id (auto-increment) and the name, but not the foreign key to the WorkoutTemplate.
How can I solve this ?
I would like to automatically insert the foreign key without sending it in the request or doing it manually into the service
Hi there it's because you are not using #JoinColumn which marks a column for as a join column for an entity association or an element collection.
On your WorkoutTemplate entity - you can retain this:
#OneToMany(cascade = CascadeType.ALL, mappedBy = "belongingWorkout", fetch = FetchType.LAZY)
private List<ConcreteExercise> concreteExercises;
But on your ConcreteExercise entity - you need to have this change:
#ManyToOne
#JoinColumn(name = "workout_template_id", nullable = false)
private WorkoutTemplate belongingWorkout;
The above code will create a foreign key linking the ConcreteExercise entity with the primary key from the WorkoutTemplate entity. The name of the foreign key column in the ConcreteExercise entity is specified by name property which for this case is workout_template_id - feel free to change this.
If you have set this up but still not working, it will also be helpful to share a code snippet on how are you saving these.

Why isn't the foreign key field of a Many-to-One relationship being set on insert?

My Spring web application allows users to update "Employee" records to change the fields or add new "Phone" records related to this "Employee" record. However, when the "Employee" record is submitted for update after adding a new "Phone" record, it's throwing a SQL error exception.
The problem is that the "employee_id" foreign key on "Phone" table to the "Employee" table isn't set in the eventual SQL insert statement submitted to the database. However, in the "PhoneEntity" JPA entity object that is referenced by the updated/merged "EmployeeEntity" object, the property associated with the employee_id database field isn't null, it's set to the "EmployeeEnity" Object being updated/merged.
From my understanding of JPA, having the entity property associated with a database field should set it when the insert statement for the entity's record is submitted to the database, but in this case it isn't which is causing this error.
I've tried stepping through with a debugger, and I have verified that the created PhoneEntity object is a member of EmployeeEntity's phones property, and that the same PhoneEntity's employee property is set to the same EmployeeEntity object (with the same object IDs) in a bidirectional relationship.
I've also set the hibernate.show_sql=true to see the SQL statement being submitted to the database and it includes the statement (with the ellipses being more fields):
Hibernate:
insert
into
phone
(id, employee_id, ...)
values
(?, ?, ...)
Which means that it is inserting a new phone for the new PhoneEntity object.
After trying to running this insert statement it gives the SQL error "Column 'employee_id' cannot be null". However like I said before, I've checked with the debugger and the employee property is indeed set to the EmployeeEntity object.
this is a simplified example of what my code looks like:
#Entity
#Table(name = "employee")
public class EmployeeEntity implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", unique = true, nullable = false)
private Integer id;
#OneToMany(mappedBy="employee", cascade = {CascadeType.PERSIST})
private Set<PhoneEntity> phones = new HashSet<>();
...
}
#Entity
#Table(name = "phone")
public class PhoneEntity implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", unique = true, nullable = false)
private Integer id;
#ManyToOne
#JoinColumn(name = "employee_id", nullable = false)
private EmployeeEntity employee;
...
}
With tables that have the structure created by the following SQL statements.
CREATE TABLE employee (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
...
);
CREATE TABLE phone (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
employee_id INT NOT NULL,
...
FOREIGN KEY(employee_id) REFERENCES employee(id)
);
And the following is where it actually submits the updates to the entity manager to make updates to the database.
public void update(EmployeeDomain employee) {
EmployeeEntity entity = employeeDomainToEntity.transform(employee)
getEntityManager().merge(entity);
}
The EmployeeEntity and PhoneEntity objects are created by converting similar domain objects that were in turn deserialized from a http request. I'd include more of this section of the code but, as I've mentioned, I've already confirmed with my debugger that the actual entity objects being submitted to the merge are already in the form that we expected with the phones fields and employee fields being set correctly, so the end entities should be correct.
In the official JPA specification document (version 2.1) in section "3.2.7.1 Merging Detached Entity State" (page 85) we find:
For all entities Y referenced by relationships from X having the cascade element value cascade=MERGE or cascade=ALL, Y is merged recursively as Y'. For all such Y referenced by X, X' is set to reference Y'. (Note that if X is managed then X is the same object as X'.)
This explains that you are lacking cascade=MERGE for the annotation of the phones field.
As proposed in thanh ngo's answer, the aforementioned definition (or: explanation) thus translates to:
#OneToMany(mappedBy="employee", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private Set<PhoneEntity> phones = new HashSet<>();
Alternatively, you could also make use of cascade=CascadeType.ALL. However, this would also include operations such as CascadeType.REMOVE which might not always be intended.
Hope it helps.
I think the problem is that you are using merge.
The cascade type setting for the entity should be:
#OneToMany(mappedBy="employee", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private Set<PhoneEntity> phones = new HashSet<>();

JPA native query with annotation executes different query

I am trying to implement an example like this: A Person class has a list of places that it likes. But when I want to query it, I want result as each person with only the most favorite place(just the first one not all of them). So I have done this:
#Entity
class Person{
...
#ManyToMany(cascade = {CascadeType.REFRESH,CascadeType.PERSIST}, fetch = FetchType.EAGER)
#JoinTable(name = "person_favorite_place",
joinColumns = #JoinColumn(name = "person_id", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(name = "place_id", referencedColumnName = "id")
)
#OrderColumn(name="favorite_place_order")
List<Place> favoritePlaces;
}
And in repository, I did:
public interface PersonRepository extends JpaRepository<Person, Long> {
#Query(value = "select person0_.id as id1_0_0_, person0_.age as age2_0_0_, person0_.name as name3_0_0_, favoriteme1_.person_id as person_id1_1_1_, place2_.id as place_id2_1_1_, favoriteme1_.favorite_place_order as favorite3_1_, place2_.id as id1_3_2_, place2_.invented as invented2_3_2_, place2_.name as name3_3_2_ from person person0_ left outer join person_favorite_place favoriteme1_ on person0_.id=favoriteme1_.person_id left outer join place place2_ on favoriteme1_.place_id=place2_.id where person0_.id=:personId and favoriteme1_.favorite_place_order = 0", nativeQuery = true)
Person getPersonWithFavoritePlace(#Param("personId") Long personId);
}
But it seems , there are 2 sql queries is getting run. the first one is:
select person0_.id as id1_0_0_, person0_.age as age2_0_0_, person0_.name as name3_0_0_, favoriteme1_.person_id as person_id1_1_1_, place2_.id as place_id2_1_1_, favoriteme1_.favorite_place_order as favorite3_1_, place2_.id as id1_3_2_, place2_.invented as invented2_3_2_, place2_.name as name3_3_2_ from person person0_ left outer join person_favorite_place favoriteme1_ on person0_.id=favoriteme1_.person_id left outer join place place2_ on favoriteme1_.place_id=place2_.id where person0_.id=:personId and favoriteme1_.favorite_place_order = 0
the second one:
select favoriteme0_.person_id as person_id1_1_0_, favoriteme0_.place_id as place_id2_1_0_, favoriteme0_.favorite_place_order as favorite3_0_, place1_.id as id1_3_1_, place1_.invented as invented2_3_1_, place1_.name as name3_3_1_ from person_favorite_place favoriteme0_ inner join place place1_ on favoriteme0_.place_id=place1_.id where favoriteme0_.person_id=?
I can understand the first one, which is completely the query that I want to execute but the second one , I do not know where it comes. So I think, because of that I am having all the favorite places of a person but not the most desired one.
Any ideas?
BR
PS: I have written the query over native output of "findOne" method. Just added "and favoriteme1_.favorite_place_order = 0" in the end. Also I have tried to use the exact query without modification and it worked like a charm!!
The second query is used to load the favoritePlaces that you defined with FetchType.EAGER
Your repository is querying Persons but not places and because of FetchType.EAGER loads all Places also with the second query; To query for the Places use PlacesRepository.

JPA / EclipseLink: Joined WHERE query does not give expected result

Now that's very confusing... I have a JPA entity Order that references an entity User. The User can be either buyer or seller of the Order.
Because both buyer and seller can enter additional information for an order, I moved that to an extra entity OrderUserData. There might or might not be a corresponding OrderUserData object, but IF one exists, the user should only be able to see the entry they created (based on USER_ID) and not the one of the other party.
The entities look like this:
#Entity
#Table(name = "T_ORDER")
public class Order {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#ManyToOne
#JoinColumn(name = "SELLER_ID")
private User seller;
#ManyToOne
#JoinColumn(name = "BUYER_ID")
private User buyer;
#OneToMany(mappedBy = "order", fetch = FetchType.EAGER)
private List<OrderUserData> userData = new ArrayList<>();
//..
}
--
#Entity
#Table(name = "T_ORDER_USERDATA")
public class OrderUserData {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#ManyToOne
#JoinColumn(name = "ORDER_ID")
private Order order;
#ManyToOne
#JoinColumn(name = "USER_ID")
private User user;
private String comment;
//...
}
( User is not very exciting, just ID and basic name fields )
Now when I'm trying to select the appropriate data to display in the website, I have a problem:
String qry = "SELECT o FROM Order o LEFT JOIN o.userData ud "
+ " WHERE (o.seller.id = :userId OR o.buyer.id = :userId)"
+ " AND ( ud.user IS NULL OR ud.user.id = :userId )";
TypedQuery<Order> query = em.createQuery(qry, Order.class);
query.setParameter("userId", userId);
Let's say I execute this, setting userId to 2:
My Database looks like this:
ORDER
=====
ID SELLER_ID BUYER_ID
1 1 2
2 2 3
3 3 1
ORDER_USERDATA
===============
ID ORDER_ID USER_ID COMMENT
1 1 1 Comment that only user 1 should see
2 1 2 Comment that only user 2 should see
But unlike you would expect, when executing the above query, both records are included in the userData list! It seems like JPA is executing two queries (despite the EAGER fetch) and ignoring the WHERE on the second one. Why is that? And what other solution than to loop through the userData list on Java level and kick out the entry that the appropriate user should not see?
There is no way to load OrderUserData objects inside an Order object using a query. Maybe you're confusing the ORM functionality, mapping rows in the database to Java objects, with the query functionality.
Mapping means 1-1 correspondence between rows and objects, hence Order objects always contain all OrderUserData objects for each OrderUserData row related to Order rows.
The fetch type is just a loading strategy, determining at which time are the related objects fetched, as soon as the containing object is loaded (EAGER) or as soon as the contained objects are accessed (LAZY).
You can obtain your list issuing a query on OrderUserData objects with the proper filters and getting Order objects from each of them, i.e.
SELECT ud FROM OrderUserData ud WHERE (ud.order.seller.id = :userId
OR ud.order.buyer.id = :userId) AND ( ud.user IS NULL OR ud.user.id =
:userId )
your query seems to work well as it selects properly Order entity. Then JPA fetch all the OrderUserData child of the selected Order : that's because oneToMany join is not filtered.
I don't think it is possible to modelize pre-filtered oneToMany with eclipseLink (like Hibernate #FILTER), so you should remove it and map orderUserDataId field only. Then you can fetch your entities in 1 query, but they will not be linked
SELECT o, ud FROM Order o, o.userData ud WHERE (o.seller.id = :userId OR o.buyer.id = :userId) AND ( ud.orderUserDataId = o.id and (ud.user IS NULL OR ud.user.id = :userId) )";
On the other hand, if the oneToMany is required by other use cases, then you can create 2 different Order entities :
1 "OrderLight" without the oneToMany
1 "OrderFull" with the oneToMany, derived from OrderLight.
While user3580357 and remigio have already given the correct answer as to why this doesn't work, might I suggest that you create a view on database level.
Something like (might need to be adapted for your needs or RDBMS):
CREATE OR REPLACE VIEW
ORDER_WITH_USERDATA
AS
SELECT o.*, oud.*
FROM ORDER o
LEFT JOIN ORDER_USERDATA oud
ON o.id = oud.order_id
This will essentially give you two different "logical" records for every order. You can then create an additional JPA entity that works on this view and do your SELECT/WHERE... without needing to (LEFT)JOIN at all.

Hibernate Exception: Missing Column (column exists)

Okay, so within the database we have a table called distributionCompanies, created like so:
CREATE TABLE `distributionCompanies` (
`distributionCompanyID` INT(11) NOT NULL,
`distributionCompanyName` VARCHAR(255) NOT NULL,
PRIMARY KEY (distributionCompanyID)
);
I'm trying to map this table to a class using Hibernate:
#Entity
#Table(name = "distributionCompanies")
public class DistributionCompany implements DatabaseObject {
#Id
#GeneratedValue
#Column(name = "distributionCompanyID", length = 11, unique = true, nullable = false)
private int distributionCompanyID;
....
However, when running, I hit this issue:
Initial SessionFactory creation failedorg.hibernate.HibernateException: Missing column: distributionCompanyID_distributionCompanyID in database2.distributionCompanies
This isn't the only table in the database, and I've managed to map other classes successfully using the same method, so I'm a little stumped as to why this is causing an issue.
Thank you for your time,
Samuel Smith
EDIT: In response to Xavi's comment, I temporarily removed another mapping for the column, and the error went away, so the bad-egg probably lays in the following code:
#ManyToOne(targetEntity = DistributionCompany.class)
#JoinTable(name = "distributionCompanies", joinColumns = { #JoinColumn(name = "distributionCompanyID", nullable = false) })
private int distributionCompanyID;
Hibernate is looking for a column named distributionCompanyID_distributionCompanyID in your distributionCompanies table.
This is probably due to a ToOne association mapping towards this table without #JoinColum.
From Hibernate Documentation:
The #JoinColumn attribute is optional, the default value(s) is like in one to one, the concatenation of the name of the relationship in the owner side, _ (underscore), and the name of the primary key column in the owned side. In this example company_id because the property name is company and the column id of Company is id.
If you've got a #ManyToOne or #OneToOne association mapping in another entity, this would explain why Hibernate is looking for such a column.
EDIT Seeing the association mapping you posted, it looks like it should be:
#ManyToOne(targetEntity = DistributionCompany.class)
#JoinColumn(name = "distributionCompanyID")
private DistributionCompany distributionCompany;
The #JoinTable annotation is used to specify a join table (that means an intermediate table used to model many-to-many associations). And the point of mapping an association would be to dispose of the mapped object instance (in this case a DistributionCompany, not just a distributionCompanyId).

Categories

Resources