Lets say I have an entity:
#Entity
public class Person {
#Id
#GeneratedValue
private Long id;
#ManyToMany(fetch = FetchType.LAZY)
private List<Role> roles;
#ManyToMany(fetch = FetchType.LAZY)
private List<Permission> permissions;
// etc
// other fields here
}
I want a to build a query using the Criteria API that filters these users and shows a list of people and among other info from the entity - how many roles does a person have.
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<Person> query = builder.createQuery(Person.class);
Root<Person> personRoot = query.from(Person.class);
// predicates here
However, this limits me to returning only a list of Person entities. I can always add a #Transient field to the entity, but this seems ugly since I might have many different queries and might end up with many such fields.
On the other hand - I cant use HQL and write the query since I want complex filtering and I would have to deal with appending and removing things from the HQL query.
My question, besides the one in the title of this post is this: how do I query the database using the Criteria API and return a non-entity (in case I want to filter the Person table but return only the number of roles, permissions, etc) and how do I do it for something very close to the actual entity (like the example with the role counter instead of the roles collection)?
UPDATE
Using Hibernate's projections I came up with this. But still don't know that to write in TODO. Projections.count doesn't work since it excpects some kind of grouping, and I don't seem to be able to find any examples in the Hibernate documentation.
Criteria cr = session.createCriteria(Person.class);
if (id != null) {
cr.add(Restrictions.eq("id", id));
}
ProjectionList projectionList = Projections.projectionList();
projectionList.add(Projections.property("id"), "id");
projectionList.add(TODO, "rolesCount");
CriteriaQuery<Long> query = entityManager.getCriteriaBuilder().get().createQuery(Long.class);
query.select(builder.get().countDistinct(root));
works for me:)
how do I do it for something very close to the actual entity (like
the example with the role counter instead of the roles collection
You could make these values properties of your User entity by various means, for example using a Hibernate #Forumula property. This will issue an inline subquery on Entity load to get the count without touching the collection.
#Formula("select count(*) from roles where user_id = ?")
private int numberOfRoles;
Another (JPA compliant) option is to handle these calculated fields by creating a view at the database level and the mapping this to your User:
e.g.
#OneToOne
private UserData userData; //entity mapped to your view (works just like a table)
....
public int getNumberOfRoles(){
return userData.getRoleCOunt();
or
by using #SecondaryTable to join this User data.
Related
I'm using a legacy database. In my example, we retrieve a product which have some characteristics. In the db, we can find a product table, a characteristic table and a jointable for the manyToMany association.
The only field i need is the label of the characteristics. So, my Product entity will contains a list of characteristics as String. I would like to not create to many entities in order to not overload my sourcecode. Let's see the example :
#Entity
#Table(name = "product")
public class Product implements Serializable {
#Id
#Column(name = "id")
private Long id;
// all field of Product entity
#ElementCollection(targetClass = String.class)
#Formula(value = "(SELECT characteristic.label FROM a jointable JOIN b characteristic ON jointable.characteristic_id = characteristic.id WHERE jointable.product_id = id)")
private Set<String> characteristics = new HashSet<>();
// Getter / setter
}
To represent my characteristics, i tried to use the association of #Formula and #ElementCollection. As you can see, the names of tables (a and b in the query) does not match with my representation of these datas.
But, when I try to load a product, I get an error like "PRODUCT_CHARACTERISTICS table not found".
Here the generated SQL query executed by hibernate :
SELECT product0_.id AS id1_14_0_,
-- Other fields
characteri10_.product_id AS product_1_15_1__,
(SELECT characteristic.label
FROM a jointable JOIN b characteristic ON jointable.characteristic_id = characteristic.id
WHERE jointable.product_id = id) AS formula6_1__,
FROM product product0_
-- Other Joins
LEFT OUTER JOIN product_characteristics characteri10_ ON product0_.cdprd = characteri10_.product_cdprd
WHERE product0_.id = ?;
In the FROM part, we can refind the call of product_characteristics table (which not exist in the database).
So, my main question is the following : How can I get the list of characterics as entity attribute ? Can I reach this result with #Formula ?
Edit
In other words, i would like to load only one attribute from Many to Many mapping. I found an example here but it works only with the id (which can find in the jointable)
I assume that what you want to achieve here is reducing the amount of data that is fetched for a use case. You can leave your many-to-many mapping as it is, since you will need DTOs for this and 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(Product.class)
public interface ProductDto {
#IdMapping
Long getId();
String getName();
#Mapping("characteristics.label")
Set<String> getCharacteristicLabels();
}
Querying is a matter of applying the entity view to a query, the simplest being just a query by id.
ProductDto a = entityViewManager.find(entityManager, ProductDto.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<ProductDto> findAll(Pageable pageable);
The best part is, it will only fetch the state that is actually necessary!
I have a USER object which has many fields having one-to-many mapping with PERMISSION object as well.
I want to fetch only few fields of user with a set of permissions as well. my code is
public class USER {
private Integer id;
private String username;
...
...
...
private Set<Permission> permissions = new HashSet<Permission>();
//setter - getter methods
}
Permission.java
public class Permission {
private String name;
private Integer id;
//setter - getter methods
}
Projection code:
Criteria criteria = session.createCriteria(User.class);
criteria.setCacheable(true);
criteria.add(eq("username", username).ignoreCase());
criteria.createAlias("permissions ", "perm", LEFT_OUTER_JOIN);
ProjectionList projectedFields = Projections.projectionList();
projectedFields.add(Projections.property("id").as("id"));
projectedFields.add(Projections.property("perm.id").as("permissions.id"));
criteria.setProjection(projectedFields);
criteria.setResultTransformer(new AliasToBeanNestedResultTransformer((User.class)));
User user = (User) criteria.uniqueResult();
I'm getting the following exception:
org.hibernate.PropertyNotFoundException: Could not find setter for id on interface java.util.Set
at org.hibernate.property.ChainedPropertyAccessor.getSetter(ChainedPropertyAccessor.java:66)
at org.hibernate.transform.AliasToBeanResultTransformer.initialize(AliasToBeanResultTransformer.java:121)
at org.hibernate.transform.AliasToBeanResultTransformer.transformTuple(AliasToBeanResultTransformer.java:84)
at ae.gov.adm.saeed.util.AliasToBeanNestedResultTransformer.transformTuple(AliasToBeanNestedResultTransformer.java:80)
at org.hibernate.transform.CacheableResultTransformer.retransformResults(CacheableResultTransformer.java:230)
any idea, how to resolve this issue?
This doesn't work. This only works for scalar results, but not for nested structures. You would have to build the object graph yourself.
Anyway, this is a perfect use case for Blaze-Persistence Entity Views.
Blaze-Persitence is a query builder on top of JPA which supports many of the advanced DBMS features on top of the JPA model. I created Entity Views on top of it to allow easy mapping between JPA models and custom interface defined models, something like Spring Data Projections on steroids. The idea is that you define your target structure the way you like and map attributes(getters) via JPQL expressions to the entity model. Since the attribute name is used as default mapping, you mostly don't need explicit mappings as 80% of the use cases is to have DTOs that are a subset of the entity model.
A mapping for your model could look as simple as the following
#EntityView(User.class)
interface ApplicationUser {
Integer getId();
#Mapping("permissions.id")
Set<Integer> getPermission();
}
Querying is a matter of applying the entity view to a query, the simplest being just a query by id.
ApplicationUser dto = entityViewManager.find(entityManager, ApplicationUser.class, id);
But 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
It will only fetch the mappings that you tell it to fetch
Given the following domain model, I want to load all Answers including their Values and their respective sub-children and put it in an AnswerDTO to then convert to JSON. I have a working solution but it suffers from the N+1 problem that I want to get rid of by using an ad-hoc #EntityGraph. All associations are configured LAZY.
#Query("SELECT a FROM Answer a")
#EntityGraph(attributePaths = {"value"})
public List<Answer> findAll();
Using an ad-hoc #EntityGraph on the Repository method I can ensure that the values are pre-fetched to prevent N+1 on the Answer->Value association. While my result is fine there is another N+1 problem, because of lazy loading the selected association of the MCValues.
Using this
#EntityGraph(attributePaths = {"value.selected"})
fails, because the selected field is of course only part of some of the Value entities:
Unable to locate Attribute with the the given name [selected] on this ManagedType [x.model.Value];
How can I tell JPA only try fetching the selected association in case the value is a MCValue? I need something like optionalAttributePaths.
You can only use an EntityGraph if the association attribute is part of the superclass and by that also part of all subclasses. Otherwise, the EntityGraph will always fail with the Exception that you currently get.
The best way to avoid your N+1 select issue is to split your query into 2 queries:
The 1st query fetches the MCValue entities using an EntityGraph to fetch the association mapped by the selected attribute. After that query, these entities are then stored in Hibernate's 1st level cache / the persistence context. Hibernate will use them when it processes the result of the 2nd query.
#Query("SELECT m FROM MCValue m") // add WHERE clause as needed ...
#EntityGraph(attributePaths = {"selected"})
public List<MCValue> findAll();
The 2nd query then fetches the Answer entity and uses an EntityGraph to also fetch the associated Value entities. For each Value entity, Hibernate will instantiate the specific subclass and check if the 1st level cache already contains an object for that class and primary key combination. If that's the case, Hibernate uses the object from the 1st level cache instead of the data returned by the query.
#Query("SELECT a FROM Answer a")
#EntityGraph(attributePaths = {"value"})
public List<Answer> findAll();
Because we already fetched all MCValue entities with the associated selected entities, we now get Answer entities with an initialized value association. And if the association contains an MCValue entity, its selected association will also be initialized.
I don't know what Spring-Data is doing there, but to do that, you usually have to use the TREAT operator to be able to access the sub-association but the implementation for that Operator is quite buggy.
Hibernate supports implicit subtype property access which is what you would need here, but apparently Spring-Data can't handle this properly. I can recommend that you take a look at Blaze-Persistence Entity-Views, a library that works on top of JPA which allows you map arbitrary structures against your entity model. You can map your DTO model in a type safe way, also the inheritance structure. Entity views for your use case could look like this
#EntityView(Answer.class)
interface AnswerDTO {
#IdMapping
Long getId();
ValueDTO getValue();
}
#EntityView(Value.class)
#EntityViewInheritance
interface ValueDTO {
#IdMapping
Long getId();
}
#EntityView(TextValue.class)
interface TextValueDTO extends ValueDTO {
String getText();
}
#EntityView(RatingValue.class)
interface RatingValueDTO extends ValueDTO {
int getRating();
}
#EntityView(MCValue.class)
interface TextValueDTO extends ValueDTO {
#Mapping("selected.id")
Set<Long> getOption();
}
With the spring data integration provided by Blaze-Persistence you can define a repository like this and directly use the result
#Transactional(readOnly = true)
interface AnswerRepository extends Repository<Answer, Long> {
List<AnswerDTO> findAll();
}
It will generate a HQL query that selects just what you mapped in the AnswerDTO which is something like the following.
SELECT
a.id,
v.id,
TYPE(v),
CASE WHEN TYPE(v) = TextValue THEN v.text END,
CASE WHEN TYPE(v) = RatingValue THEN v.rating END,
CASE WHEN TYPE(v) = MCValue THEN s.id END
FROM Answer a
LEFT JOIN a.value v
LEFT JOIN v.selected s
My latest project used GraphQL (a first for me) and we had a big issue with N+1 queries and trying to optimize the queries to only join for tables when they are required. I have found Cosium
/
spring-data-jpa-entity-graph irreplaceable. It extends JpaRepository and adds methods to pass in an entity graph to the query. You can then build dynamic entity graphs at runtime to add in left joins for only the data you need.
Our data flow looks something like this:
Receive GraphQL request
Parse GraphQL request and convert to list of entity graph nodes in the query
Create entity graph from the discovered nodes and pass into the repository for execution
To solve the problem of not including invalid nodes into the entity graph (for example __typename from graphql), I created a utility class which handles the entity graph generation. The calling class passes in the class name it is generating the graph for, which then validates each node in the graph against the metamodel maintained by the ORM. If the node is not in the model, it removes it from the list of graph nodes. (This check needs to be recursive and check each child as well)
Before finding this I had tried projections and every other alternative recommended in the Spring JPA / Hibernate docs, but nothing seemed to solve the problem elegantly or at least with a ton of extra code
Edited after your comment:
My apologize, I haven't undersood you issue in the first round, your issue occurs on startup of spring-data, not only when you try to call the findAll().
So, you can now navigate the full example can be pull from my github:
https://github.com/bdzzaid/stackoverflow-java/blob/master/jpa-hibernate/
You can easlily reproduce and fix your issue inside this project.
Effectivly, Spring data and hibernate are not capable to determinate the "selected" graph by default and you need to specify the way to collect the selected option.
So first, you have to declare the NamedEntityGraphs of the class Answer
As you can see, there is two NamedEntityGraph for the attribute value of the class Answer
The first for all Value without specific relationship to load
The second for the specific Multichoice value. If you remove this one, you reproduce the exception.
Second, you need to be in a transactional context answerRepository.findAll() if you want to fetch data in type LAZY
#Entity
#Table(name = "answer")
#NamedEntityGraphs({
#NamedEntityGraph(
name = "graph.Answer",
attributeNodes = #NamedAttributeNode(value = "value")
),
#NamedEntityGraph(
name = "graph.AnswerMultichoice",
attributeNodes = #NamedAttributeNode(value = "value"),
subgraphs = {
#NamedSubgraph(
name = "graph.AnswerMultichoice.selected",
attributeNodes = {
#NamedAttributeNode("selected")
}
)
}
)
}
)
public class Answer
{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(updatable = false, nullable = false)
private int id;
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "value_id", referencedColumnName = "id")
private Value value;
// ..
}
This is a simple Product entity that refers to a subgroup:
public class Product implements Comparable<Product> {
...
#ManyToOne(optional=false, fetch=FetchType.LAZY)
#NotNull
private ProductSubGroup productSubGroup;
...
}
I have a map containing Product's in another entity:
public class FinishedProduct {
...
#NotNull
#ManyToOne
private Product product;
#ElementCollection(fetch=FetchType.LAZY)
#MapKeyJoinColumn
#Column(name="amount")
#Sort(type=SortType.NATURAL)
#Fetch(FetchMode.SUBSELECT)
private SortedMap<Product, Double> byproducts = new TreeMap<>();
...
}
I can load the map with this code:
Root<FinishedProduct> root = q.from(FinishedProduct.class);
root.fetch("product", JoinType.LEFT);
root.fetch("byproducts", JoinType.LEFT);
This works, but I need the productSubGroup of the byproducts stored in the map without generating n+1 selects. How can I fetch them? Just adding the fetch to the end results in an exception:
root.fetch("byproducts", JoinType.LEFT).fetch("productSubGroup", JoinType.LEFT);
org.springframework.dao.InvalidDataAccessApiUsageException:
Collection of values [null] cannot be source of a fetch
Also tried to fool around with MapJoin, same exception:
MapJoin<FinishedProduct,Product,Double> map = root.joinMap("byproducts", JoinType.LEFT);
map.fetch("productSubGroup", JoinType.LEFT);
I guess I somehow need to refer to the map key, but no idea how.
These are a bit complex mappings that you have here and I am not sure if there is an easier way to accomplish this. Hopefully somebody will provide a better answer, but as an alternative there is always the ability to pre-load into the persistence context all the entity instances that you know will be fetched with n+1 selects.
So, before firing your query, just load all ProductSubGroups which are expected to be fetched:
select p.productSubGroup from Product p
where p in (select index(byproducts) from FinishedProduct)
Of course, repeat any other additional restrictions on FinishedProduct in the subquery which you have in your original query to avoid loading ProductSubGroups which you don't need.
As even better alternative (in my opinion), you may want to consider defining #BatchSize for Product.productSubGroup association. That way ProductSubGroups would be loaded in batches instead of one by one.
I've "Student" and "Class" in relation many to many. In between there is a linking table.
If I want to retrieve all the students with HQL in the following way, everything is fine:
Query queryObject = getSession().createQuery("from Student");
return queryObject.list();
With the above code if there are three students I get a list of three students.
If I use criterias, however, it's fine only as long as there are no associations in the linking table.
Criteria crit = getSession().createCriteria(Student.getClass());
return crit.list();
With the second code, I get three results only when the linking table is empty. However when I add an association, I get 6 results. Looking at the log, Hibernate generates multiple selects. How is it possible?
Can someone explain why does this happen? How can I fix the criteria in order to return the three records only once?
EDIT
In the Student class I mapped in this way:
#ManyToMany(fetch=FetchType.EAGER)
#JoinTable(
name="room_student_rel"
, joinColumns={
#JoinColumn(name="id_student")
}
, inverseJoinColumns={
#JoinColumn(name="id_room")
}
)
private List<Room> rooms;
In the room (class) I mapped in this way:
#OneToMany(fetch=FetchType.EAGER, mappedBy="room")
#Fetch(FetchMode.SELECT)
private List<RoomStudent> roomStudents;
To expand on my previous comment, Hibernate will try to select using an outer join when the FetchType is set to EAGER.
See 2.2.5.5 at the following:
http://docs.jboss.org/hibernate/annotations/3.5/reference/en/html_single/
For the implications of this see here:
Hibernate Criteria returns children multiple times with FetchType.EAGER
Now you can also specify a FetchMode (on your criteria.setFetchMode("assocFiled",FetchMode.LAZY) ) to set it to something other than JOIN or you can use something like criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY); to filter the duplicates.
Ideally you should avoid EAGER on your entity mappings and where eager fetching is required enable it explicitly on entity load using some hint or via FetchProfile and then deal with the duplicates if required.