Hibernate L2 cache issues with EntityGraph and LEFT JOIN FETCH queries - java

I'm using hibernate 5.3.14 with hazelcast 3.11.5 as L2 cache provider and spring boot 2.1.11.
I have 3 entities defined with relations:
one order has many order items
one order has many custom fields
L2 cache is enabled for entities, associations and queries.
#Entity
#Table(name = "orders")
#org.hibernate.annotations.Cache(usage =CacheConcurrencyStrategy.READ_WRITE)
public class Order extends AbstractBaseEntity implements Orderable {
#OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
#org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
#LazyCollection(LazyCollectionOption.TRUE)
#Fetch(FetchMode.SELECT)
private List<OrderItem> orderItems;
#MappedSuperclass
public abstract class AbstractBaseEntity
#OneToMany(orphanRemoval = true, cascade = CascadeType.ALL)
#org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
#JoinColumn(name = "parent_rid")
#LazyCollection(LazyCollectionOption.TRUE)
private List<CustomField> customFields = new ArrayList<>();
#Entity
#Table(name = "custom_fields")
#org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class CustomField implements Serializable {
#Entity
#Table(name = "order_items")
#org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class OrderItem extends AbstractBaseEntity implements Orderable {
I have one repository:
#Repository
public interface OrderRepository extends JpaRepository<Order, String> {
#EntityGraph(attributePaths = "customFields")
Optional<Order> findById(String rid);
#QueryHints(value = {#QueryHint(name = "org.hibernate.cacheable", value = "true")})
#EntityGraph(attributePaths = "customFields")
#Query("select o from Order left join fetch o.orderItems where o.status = 'ACTIVE' ")
List<Order> findAllActiveWithOrderItems();
There are 3 problems:
repo method findById doesn't load from the cache the main entity, order, with the relation, customFields, indicated by entity graph loaded
cached query results for repo method findAllActiveWithOrderItems does not seem to have the relations, orderItems, loaded by the FETCH JOIN
cached query results
for repo method findAllActiveWithOrderItems does not seem to have the relations loaded by the the EntityGraph, customFields
Are there any known hibernate tickets or workarounds to fix those?

That's a known issue and I think Hibernate 6.0 will fix it, but I don't remember if there ever was a ticket for this.

Related

Why are lazy fields on related entities loaded

In my REST API project (Java 8, Spring Boot 2.3.1) I have a problem with some queries triggering massive query chains by loading lazy relations, even though the related objects are never accessed.
I have a UserEntity and a polymorphic CompanyEntity that are related with a ManyToMany relationship. I have an endpoint that returns all users and I include the IDs of the related companies in the JSON. I excpect a query to the user table and a query to the company table, however all related entities of one sub-entity of CompanyEntity are always loaded for each of those sub-entities resulting in large query chains.
Here are snippets of my classes:
User entity
#Entity(name = "USERS")
public class UserEntity {
#Id
#GeneratedValue
private UUID id;
#EqualsAndHashCode.Exclude
#Fetch(FetchMode.SUBSELECT)
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(
name = "users_company",
joinColumns = #JoinColumn(name = "USER_ID"),
inverseJoinColumns = #JoinColumn(name = "COMPANY_ID")
)
private Set<CompanyEntity> companies = new HashSet<>();
public List<UUID> getCompanyIds() {
return companies.stream()
.map(CompanyEntity::getId)
.collect(Collectors.toList());
}
}
Polymorphic company entity
#Entity(name = "COMPANY")
#Inheritance(strategy = InheritanceType.JOINED)
public abstract class CompanyEntity {
#Id
#GeneratedValue
private UUID id;
#Fetch(FetchMode.SUBSELECT)
#ManyToMany(mappedBy = "companies", fetch = FetchType.LAZY)
private Set<UserEntity> users = new HashSet<>();
}
Concrete company subclass that triggers the problem
#Entity(name = "CUSTOMER")
public class CustomerEntity extends CompanyEntity {
#NotNull
#OneToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.LAZY)
private ContactPersonEntity contactPerson;
#Fetch(FetchMode.SUBSELECT)
#OneToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.LAZY, mappedBy = "customer")
private Set<TransactionEntity> transactions = new HashSet<>();
public Set<UUID> getTransactionIds() {
return this.transactions.stream()
.map(TransactionEntity::getId)
.collect(Collectors.toSet());
}
}
In the REST controller I return the following mapping:
#GetMapping(value = "", produces = MediaType.APPLICATION_JSON_VALUE)
public List<UserReadModel> getUsers() {
return userRepository.findAll().stream()
.map(userEntity -> new UserReadModel(userEntity))
.collect(Collectors.toList());
}
Where the UserReadModel is a DTO:
#Data
public class UserReadModel {
private UUID id;
private List<UUID> companyIds;
}
Logging the database queries results in the following output:
// Expected
Hibernate: select userentity0_.id as id1_47_, ... from users userentity0_
Hibernate: select companies0_.user_id ... case when companyent1_1_.id is not null then 1 when companyent1_2_.id is not null then 2 when companyent1_.id is not null then 0 end as clazz_0_ from users_company companies0_ inner join company companyent1_ on companies0_.company_id=companyent1_.id left outer join customer companyent1_1_ on companyent1_.id=companyent1_1_.id left outer join external_editor companyent1_2_ on companyent1_.id=companyent1_2_.id where companies0_.user_id in (select userentity0_.id from users userentity0_)
// Unexpected as they are marked lazy and never accessed
Hibernate: select contactper0_.id ... from contact_person contactper0_ where contactper0_.id=?
Hibernate: select transactio0_.customer_id ... from transactions transactio0_ where transactio0_.customer_id=?
Hibernate: select contactper0_.id ... from contact_person contactper0_ where contactper0_.id=?
Hibernate: select transactio0_.customer_id ... from transactions transactio0_ where transactio0_.customer_id=?
...
I've read through loads of articles on entity mapping and lazy loading but I can't seem to find a reason why this behavior persists. Did anyone have this problem before?
You are accessing the collection, so Hibernate has to load the collection. Since you only need the ids and already have a DTO, I think this is a perfect use case for Blaze-Persistence Entity Views.
I created the library to allow easy mapping between JPA models and custom interface or abstract class defined models, something like Spring Data Projections on steroids. The idea is that you define your target structure(domain model) the way you like and map attributes(getters) via JPQL expressions to the entity model.
A DTO model for your use case could look like the following with Blaze-Persistence Entity-Views:
#EntityView(UserEntity.class)
public interface UserReadModel {
#IdMapping
UUID getId();
#Mapping("companies.id")
Set<UUID> getCompanyIds();
}
Querying is a matter of applying the entity view to a query, the simplest being just a query by id.
UserReadModel a = entityViewManager.find(entityManager, UserReadModel.class, id);
The Spring Data integration allows you to use it almost like Spring Data Projections: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features
Page<UserReadModel> findAll(Pageable pageable);
The best part is, it will only fetch the state that is actually necessary! In your case, a query like the following will be generated:
select u.id, uc.company_id
from users u
left join users_company uc on uc.user_id = u.id
left join company c on c.id = uc.company_id
Depending on the Hibernate version, the join for the company might even be omitted.
I eventually figured out the solution and want to post it here, in case anyone stumbles upon this question. This was purely a mistake on my side and is not reproducible from the examples I posted.
I used lombok annotations to generate equals and hashcode methods on the customer entity (and all other entities for that matter) and forgot to annotate the contactPerson and transactions fields with #EqualsAndHashcode.Exclude. As the equals method was called somewhere along the execution, it triggered the lazy loading of those fields. Implementing equals and hashcode manually and using the guidelines from this article for that solved the problem.

How to refer mapped superclass fields in JPA query syntax?

I have a class with multiple mapped superclasses
#EqualsAndHashCode(callSuper = true)
#Entity(name = "Supported_cars_usage")
#Data
#NoArgsConstructor
public class SupportedCarUsage extends SupportedUsageBase {
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name = "supported_car_id")
private SupportedCar supportedCar;
}
#MappedSuperclass
#Data
#EqualsAndHashCode(callSuper = true)
#NoArgsConstructor
public class SupportedUsageBase extends BaseEntity {
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "user_id")
User user;
boolean allowed;
}
I am trying to select for, search by and group by some nested field. For the beginning I found I can refer nested field at all.
I tried variations like
Query query = em.createQuery("select supportedCar, allowed from Supported_cars_usage");
Query query = em.createQuery("select supportedCar, super.allowed from Supported_cars_usage");
Query query = em.createQuery("select supportedCar, SupportedUsageBase.allowed from Supported_cars_usage");
but failed with various errors. Is these some syntax to refer fields inside mapped superclass?
Database itself is created normally.
Getters are present and created automatically with Lombok (see #Data annotation).
I wish not to use native queries.
You can write something like this:
List<Object[]> result = em.createQuery("select s.supportedCar, s.allowed from Supported_cars_usage s").getResultList();

Fetch concurrent tables with Criteria API

I've got 3 entity classes:
(The BaseAutoIncrementModel contains the declaration of the id for each entity)
Table 1: Dossier
#Entity
#Table(name = "DOSSIER", schema = "ADOP")
public class Dossier extends BaseAutoIncrementModel<Integer> implements BaseModelCode<Integer> {
...
}
Table 2: AlerteDossier
#Entity
#Table(name = "ALERTE_DOSSIER", schema = "ADOP")
public class AlerteDossier extends BaseAutoIncrementModel<Integer> implements BaseModelCode<Integer> {
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "FK_DOSSIER")
private Dossier dossier;
...
}
Table 3: AlerteEnvoi
#Entity
#Table(name = "ALERTE_ENVOI", schema = "ADOP")
public class AlerteEnvoi extends BaseAutoIncrementModel<Integer> implements BaseModelCode<Integer> {
#ManyToOne(fetch = FetchType.EAGER, optional = false)
#JoinColumn(name = "FK_ALERTE_DOSSIER")
private AlerteDossier alerteDossier;
...
}
What I have atm:
CriteriaBuilder builder = getEntityManager().getCriteriaBuilder();
CriteriaQuery<AlerteEnvoi> query = builder.createQuery(AlerteEnvoi.class);
Root<AlerteEnvoi> root = query.from(AlerteEnvoi.class);
query.select(root);
List<Predicate> predicateList = new ArrayList<>();
...
As you can see in AlerteEnvoi the AlerteDossier table is eagerly loaded, though in AlerteDossier the Dossier table is lazily loaded.
I need to create, using the Criteria Api, a select statement for AlerteEnvoi where Dossier would also be loaded within the AlerteDossier entity.
I know how I'd need to fetch the AlerteDossier within the AlerteEnvoi if AlerteDossier would be lazily-loaded (root.fetch("alerteDossier", JoinType.LEFT), I've got no clue how to fetch a sub-entity of a sub-entity though. Anyone can help me with this?
I haven't tested it out yet, but heard from a collegue something like this should be do-able:
Fetch<AlerteEnvoi, AlerteDossier> fetchAlerteDossier = root.fetch("alerteDossier", JoinType.LEFT);
fetchAlerteDossier.fetch("dossier", JoinType.LEFT);
I'll be putting this answer as accepted once I've tested it out.

#Where clause does not work inside hibernate join query

I have 2 entities with #Where annotation. First one is Category;
#Where(clause = "DELETED = '0'")
public class Category extends AbstractEntity
and it has the following relation;
#OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "category")
private Set<SubCategory> subCategories = Sets.newHashSet();
and second entity is SubCategory;
#Where(clause = "DELETED = '0'")
public class SubCategory extends AbstractEntity
and contains corresponding relation;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "CATEGORY_ID")
private Category category;
Whenever I call the following Dao method;
#Query(value = "select distinct category from Category category join fetch category.subCategories subcategories")
public List<Category> findAllCategories();
I got the following sql query;
select
distinct category0_.id as id1_3_0_,
subcategor1_.id as id1_16_1_,
category0_.create_time as create2_3_0_,
category0_.create_user as create3_3_0_,
category0_.create_userip as create4_3_0_,
category0_.deleted as deleted5_3_0_,
category0_.update_time as update6_3_0_,
category0_.update_user as update7_3_0_,
category0_.update_userip as update8_3_0_,
category0_.version as version9_3_0_,
category0_.name as name10_3_0_,
subcategor1_.create_time as create2_16_1_,
subcategor1_.create_user as create3_16_1_,
subcategor1_.create_userip as create4_16_1_,
subcategor1_.deleted as deleted5_16_1_,
subcategor1_.update_time as update6_16_1_,
subcategor1_.update_user as update7_16_1_,
subcategor1_.update_userip as update8_16_1_,
subcategor1_.version as version9_16_1_,
subcategor1_.category_id as categor11_16_1_,
subcategor1_.name as name10_16_1_,
subcategor1_.category_id as categor11_3_0__,
subcategor1_.id as id1_16_0__
from
PUBLIC.t_category category0_
inner join
PUBLIC.t_sub_category subcategor1_
on category0_.id=subcategor1_.category_id
where
(
category0_.DELETED = '0'
)
Could you please tell me why the above query lacks
and subcategor1_.DELETED = '0'
inside its where block?
I have just solved a similar problem in my project.
It is possible to put #Where annotation not only on Entity, but on also on your child collection.
According to the javadoc:
Where clause to add to the element Entity or target entity of a collection
In your case, it would be like :
#OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "category")
#Where(clause = "DELETED = '0'")
private Set<SubCategory> subCategories = Sets.newHashSet();
Please find a similar issues resolved here
I believe thus solution is not as invasive compared to using Hibernate Filters.These filters are disabled by default and operate on Session level, thus enabling them each time new Session opens is extra work especially when your DAO works through abstractions like Spring Data
This is a quick reply;
#Where(clause = "DELETED = '0'")
public class SubCategory extends AbstractEntity
Where will effect when direct query for SubCategry.
To not get deleted sub categories use Hibernate Filters
as exampled on here

JPA multiple queries instead of one

I have two entities:
#Entity
#Table(name = "ACCOUNT")
#Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class MyCloudAccount implements Serializable {
...
#OneToMany(mappedBy = "account", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private Set<ServerInstance> servers = new HashSet<ServerInstance>();
...
}
#Entity
#Table(name = "SERVER_INSTANCE")
public class ServerInstance implements Serializable {
...
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "ACCOUNT_ID")
private MyCloudAccount account;
...
}
I am getting all accounts by this code:
StringBuilder sql = new StringBuilder();
sql.append("SELECT e FROM ");
sql.append(persistentClass.getName());
sql.append(" e");
return entityManager.createQuery(sql.toString()).getResultList();
And this produces one query for the account and N queries for the servers instead of one with outer join. How to force JPA to make the query in optimal way?
I find it more convenient to use Java Persistence Query Language
you can do:
#NamedQueries{
#NamedQuery(name="myQuery" query="SELECT a FROM MyCloudAccount JOIN FETCH a.servers")
}
public class MyCloudAccount{
...
}
then you can do
TypedQuery<MyCloudAccount> query = em.createNamedQuery("MyCloudAccount.myQuery", MyCloudAccount.class);
List<MyCloudAccount> results = query.getResultList();
EDIT
You are actually already using JPQL. The key thing to your problem is using the JOIN FECTH command.

Categories