JPA EntityGraph making a Cartesian Product with subgraph - sql

I am working with Spring Data JPA and Entity Graphs.
I have the following Entity structure:
Result entity has a list of SingleQuestionResponse entities, and the SingleQuestionResponse entity has a set of Answer entities (markedAnswers).
public class Result {
...
#OneToMany(cascade = CascadeType.PERSIST)
#JoinColumn(name = "result_id", nullable = false)
private List<SingleQuestionResponse> responses;
...
}
public class SingleQuestionResponse {
...
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(
name = "singlequestionresponses_answers",
joinColumns = #JoinColumn(name = "single_question_response_id"),
inverseJoinColumns = #JoinColumn(name = "answer_id")
)
private Set<Answer> markedAnswers;
...
}
and Answer just has simple-type fields.
Now, I would like to be able to fetch Result, along with all responses, and the markedAnswers in one query. For that I annotated the Result class with:
#NamedEntityGraph(name = "graph.Result.responsesWithQuestionsAndAnswersEager",
attributeNodes = #NamedAttributeNode(value = "responses", subgraph = "responsesWithMarkedAnswersAndQuestion"),
subgraphs = {
#NamedSubgraph(name = "responsesWithMarkedAnswersAndQuestion", attributeNodes = {
#NamedAttributeNode("markedAnswers"),
#NamedAttributeNode("question")
})
}
)
an example of usage is:
#EntityGraph("graph.Result.responsesWithQuestionsAndAnswersEager")
List<Result> findResultsByResultSetId(Long resultSetId);
I noticed, that calling the findResultsByResultSetId method (and other methods using this entity graph) results in responses (SingleQuestionResponse entities) being multiplied by the number of markedAnswers. What I mean by that is that result.getResponses() returns more SingleQuestionResponse objects than it should (it returns one response object per each markedAnswer).
I realize this is due to Hibernate making a Cartesian product with the join, but I have no idea how to fix it.
Can you help please? Thanks

You have to use the DISTINCT operator. With Spring Data JPA, this can be done by naming the method findDistinctResultsByResultSetId

Related

InvalidDataAccessApiUsageException - Parameter value did not match expected type

I need to get all users with a specific role from a database. For this I need to use JPA.
All roles are stored in a special set:
UserAccount.kt
#ManyToMany(cascade = [(CascadeType.MERGE)])
#JoinTable(
name = "user_authorities",
joinColumns = [JoinColumn(name = "user_id", referencedColumnName = "id")],
inverseJoinColumns = [JoinColumn(name = "authority_id", referencedColumnName = "id")]
)
var authoritySet: MutableSet<Authority> = hashSetOf()
I want to do it like this:
UserAccountService.kt
override fun getAllUsersByAuthorityName(name: String): List<UserAccountDto> {
return userAccountRepository.findUsersByAuthoritySet(mutableSetOf(authorityService.findAuthorityByAuthorityName(name))).map { it.toDto() }
}
UserAccountRepository.kt
#Query("select u from UserAccount u where u.authoritySet = ?1")
fun findUserAccountByAuthoritySet(authoritySet: MutableSet<Authority>): List<UserAccount>
But when calling these methods in tests, it gives an error:
Parameter value [Authority(id=1, authority='ROLE_ADMIN')] did not match expected type [java.util.Set (n/a)]; nested exception is java.lang.IllegalArgumentException: Parameter value [Authority(id=1, authority='ROLE_ADMIN')] did not match expected type [java.util.Set (n/a)]
Tell me how you can properly organize the search for users?
Can you show sites with examples of such requests?
First, I started using the JPA functionality and added the "In" particle to the method name in the repository.
In the second, I removed the #Query annotation
Third, I revisited my tests, and it turned out that I added roles after the saved users, and therefore my changes were not committed to the database. I fixed it.
Problem solved

CriteriaBuilder.size() and Hibernate's #Where annotation

I have the following setup:
#Entity
public class Function {
private String name;
#OneToMany(mappedBy = "function", cascade = CascadeType.ALL, orphanRemoval = true)
#Where(clause = "type = 'In'") // <=== seems to cause problems for CriteriaBuilder::size
private Set<Parameter> inParameters = new HashSet<>();
#OneToMany(mappedBy = "function", cascade = CascadeType.ALL, orphanRemoval = true)
#Where(clause = "type = 'Out'") // <=== seems to cause problems for CriteriaBuilder::size
private Set<Parameter> outParameters = new HashSet<>();
}
#Entity
public class Parameter {
private String name;
#Enumerated(EnumType.STRING)
private ParameterType type;
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "function_id")
private Function function;
}
The overall problem I am trying to solve is find all functions that have outParameters with an exact dynamic set of names. E.g. find all function with outParameters whose names are exactly ('outParam1', 'outParam2')
This seems to be an "exact relational division" problem in SQL, so there might be better solutions out there, but the way I've gone about doing it is like this:
List<String> paramNames = ...
Root<Function> func = criteria.from(Function.class);
Path outParams = func.get("outParameters");
Path paramName = func.join("outParameters").get("name");
...
// CriteriaBuilder Code
builder.and(
builder.or(paramNames.stream().map(name -> builder.like(builder.lower(paramName), builder.literal(name))).toArray(Predicate[]::new)),
builder.equal(builder.size(outParams), paramNames.size()));
The problem I get is that the builder.size() does not seem to take into account the #Where annotation. Because the "CriteriaBuilder code" is nested in a generic Specification that should work for any type of Entity, I am not able to simply add a query.where() clause.
The code works when a function has 0 input parameters, but it does not work when it has more. I have taken a look at the SQL that is generated and I can see that it's missing:
SELECT DISTINCT
function0_.id AS id1_37_,
function0_.name AS name4_37_,
FROM
functions function0_
LEFT OUTER JOIN parameters outparamet2_ ON function0_.id = outparamet2_.function_id
AND (outparamet2_.type = 'Out') -- <== where clause added here
WHERE (lower(outparamet2_.name)
LIKE lower(?)
OR lower(outparamet2_.name)
LIKE lower(?))
AND (
SELECT
count(outparamet4_.function_id)
FROM
parameters outparamet4_
WHERE
function0_.id = outparamet4_.function_id) = 2 -- <== where clause NOT added here
Any help appreciated (either with a different approach to the problem, or with a workaround to builder.size() not working).
The where annotation is in the function entity, in the subquery you have not used that entity so the operation is correct, try using the function entity as root of the subquery, or to implement the where manually.
For the next one, it would be recommended that you include the complete Criteria API code to be more precise in the answers.

Spring Data Jpa, select based on two column values

Good Morning,
I have the following table:
_____________________
Name Status Num
A Good 6
B Bad 6
C Bad 7
I want to select all rows where "Status = 'Good' AND Num = '6'" OR "Status = 'Bad' AND Num = '7'"
So I would select rows with Names A and C from the above reference data.
I am hoping to be able to pass in two equal sized lists (ordered in the way I desire the query to be constructed), but have been unable to figure this out. The standard queries (SelectXByStatusAndNum) query generates SQL using 'IN' statements, and returns all 3 rows in the above data instead of just two.
Any insight appreciated
I believe the best way is to use a #Query annotation in the repository:
#Entity
#Table(name = "table_name")
public class TableName {
#Id
#Basic(optional = false)
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "ID")
private Long id;
#Column(name = "name")
private String name;
#Column(name = "status")
private String status;
#Column(name = "num")
private Integer num;
/**
* getters/setters, etc
**/
public interface TableNameRepository extends CrudRepository<TableName, Long> {
#Query("select t from TableName t where (status = :status1 and num = :num1 or status = :status2 and num = :num2)")
List<TableName> findByStatusAndNumOrStatusAndNum(#Param("status1") String status1,
#Param("num1") Integer num1, #Param("status2") String status2, #Param("num2") Integer num2);
}
It's a way to get values for a given parameters. In case of collections as parameters there is no in-box case due to RDBMS concept. You just can write some java code based on the key-valued parameter and collect results:
public interface TableNameRepository extends CrudRepository<TableName, Long> {
List<TableName> findByStatusAndNumIn(String status, Collection<Integer> nums);
}
List<TableName> result = new ArrayList<>();
List<TableName> itemGood = findByStatusAndNumIn("Good", numsGood);
List<TableName> itemBad = findByStatusAndNumIn("Bad", numsBad);
result.addAll(itemGood);
result.addAll(itemBad);
Instead of equal size lists, I suggest creating an object with both status and num and provide a list of those - I called it CustomCriteria below. Since there is no straightforward way to handle this with standard spring data query methods I believe the best way is to do this with criteria builder in a custom repository that builds the list of OR's from a list of AND predicates based on the criteria entries in the provided list. For example:
public List<YourTable> findMatching(List<CustomCriteria> customCriteriaList) {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<YourTable> criteriaQuery = criteriaBuilder.createQuery(YourTable.class);
Root<YourTable> itemRoot = criteriaQuery.from(YourTable.class);
List<Predicate> predicates = new ArrayList<>(customCriteriaList.size());
for (CustomCriteria customCriteria : customCriteriaList) {
Predicate predicateForNum
= criteriaBuilder.equal(itemRoot.get("num"), customCriteria.getNum());
Predicate predicateForStatus
= criteriaBuilder.equal(itemRoot.get("status"), customCriteria.getStatus());
predicates.add(criteriaBuilder.and(predicateForNum, predicateForStatus));
}
Predicate finalPredicate = criteriaBuilder.or(predicates.toArray(new Predicate[predicates.size()]));
criteriaQuery.where(finalPredicate);
return entityManager.createQuery(criteriaQuery).getResultList();
}

Retrieving entities by checking a filtered subset of childen

I have entities of the following structure:
#Entity
public Class MyObject {
...
public List<Status> statuses;
...
}
#Entity
public Class Status {
...
public Long creationTime;
public Double range;
#JoinColumn(name = "myObject",
nullable = false,
unique = false,
updatable = false)
#ManyToOne(optional = false)
#NotNull
private MyObject myObject;
...
}
I got n entities of the type MyObject which can have m statuses. Ownership lays on the side of the Status entity which cannot be changed. I am currently trying to filter the entities that are greater-equal than a certain range. My current query retrieves a result for every status that has a range that is greater-equal than the one given as a parameter:
final CriteriaBuilder cb = entityManager.getCriteriaBuilder();
final CriteriaQuery<MyObject> cq = cb.createQuery(MyObject.class);
final Root<MyObject> myObject = cq.from(MyObject.class);
final Root<Status> statuses = cq.from(Status.class);
final Predicate[] predicates = {
cb.equal(myObject.get(MyObject_.id), statuses.get(Status_.myObject).get(MyObject_.id)),
cb.ge(statuses.get(Status_.range), range)
};
cq.select(myObject).where(predicates);
return entityManager.createQuery(cq).getResultList();
Desired is, that I only get the entities of MyObject, for which the most current status that has a range is greater-equal than the one given as a paramter. I am pretty sure that I have to group the list of statuses first and sort by creation time but I am not really sure how to combine all of this, especially not in the Criteria API.
EDIT:
At least I was able to create a SQL query that does what I want:
SELECT * FROM MyObject O INNER JOIN (SELECT * FROM Status WHERE Status.CREATIONTIME IN (SELECT MAX(Status.CREATIONTIME) FROM Status WHERE (Status.RANGE IS NOT NULL AND Status.RANGE >= 30) GROUP BY Status.myObject)) S ON O.ID = S.myObject;
I am trying to rebuild this query using the Criteria API but don't really know how to include the embedded SELECT in the JOIN. Any help would be appreciated!

Hibernate Search does not work woth composite primary key using #IdClass

I've configured with hibernate-search annotation (4.1.1 version library) my class Intervento. So, I'm using jpa and in my case i can omit #DocumentId but I have a composite primary key...
#IdClass(it.domain.InterventoPK.class)
#Entity
#Indexed
#AnalyzerDef(name = "interventongram", tokenizer = #TokenizerDef(factory = StandardTokenizerFactory.class),
filters = {
#TokenFilterDef(factory = LowerCaseFilterFactory.class),
#TokenFilterDef(factory = StopFilterFactory.class, params = {
#Parameter(name = "words", value = "lucene/dictionary/stopwords.txt"),
#Parameter(name = "ignoreCase", value = "true"),
#Parameter(name = "enablePositionIncrements", value = "true")
}),
#TokenFilterDef(factory = ItalianLightStemFilterFactory.class),
#TokenFilterDef(factory = SynonymFilterFactory.class, params = {
#Parameter(name = "synonyms", value = "lucene/dictionary/synonyms.txt"),
#Parameter(name = "expand", value = "true")
}),
#TokenFilterDef(factory = SnowballPorterFilterFactory.class, params = {
#Parameter(name = "language", value = "Italian")
})
})
#Table(name = "intervento", catalog = "gestionale")
#XmlAccessorType(XmlAccessType.FIELD)
#XmlType(namespace = "Clinigo/it/domain", name = "Intervento")
#XmlRootElement(namespace = "Clinigo/it/domain")
public class Intervento implements Serializable {
private static final long serialVersionUID = 1L;
/**
*/
#Column(name = "idintervento", nullable = false)
#Basic(fetch = FetchType.EAGER)
#Id
#XmlElement
Integer idintervento;
/**
*/
#Column(name = "lingua_idlingua", nullable = false)
#Basic(fetch = FetchType.EAGER)
#Id
#XmlElement
Integer linguaIdlingua;
/**
*/
#Temporal(TemporalType.TIMESTAMP)
#Column(name = "version", nullable = false)
#Basic(fetch = FetchType.EAGER)
#XmlElement
Calendar version;
...
I'm getting....can you help me?
ERROR: HSEARCH000058: HSEARCH000116: Unexpected error during MassIndexer operation
java.lang.ClassCastException: it.domain.InterventoPK cannot be cast to java.lang.Integer
at org.hibernate.type.descriptor.java.IntegerTypeDescriptor.unwrap(IntegerTypeDescriptor.java:36)
at org.hibernate.type.descriptor.sql.IntegerTypeDescriptor$1.doBind(IntegerTypeDescriptor.java:57)
at org.hibernate.type.descriptor.sql.BasicBinder.bind(BasicBinder.java:92)
at org.hibernate.type.AbstractStandardBasicType.nullSafeSet(AbstractStandardBasicType.java:305)
at org.hibernate.type.AbstractStandardBasicType.nullSafeSet(AbstractStandardBasicType.java:300)
at org.hibernate.loader.Loader.bindPositionalParameters(Loader.java:1891)
at org.hibernate.loader.Loader.bindParameterValues(Loader.java:1862)
at org.hibernate.loader.Loader.prepareQueryStatement(Loader.java:1737)
at org.hibernate.loader.Loader.doQuery(Loader.java:828)
at org.hibernate.loader.Loader.doQueryAndInitializeNonLazyCollections(Loader.java:289)
at org.hibernate.loader.Loader.doList(Loader.java:2447)
at org.hibernate.loader.Loader.doList(Loader.java:2433)
at org.hibernate.loader.Loader.listIgnoreQueryCache(Loader.java:2263)
at org.hibernate.loader.Loader.list(Loader.java:2258)
at org.hibernate.loader.criteria.CriteriaLoader.list(CriteriaLoader.java:122)
at org.hibernate.internal.SessionImpl.list(SessionImpl.java:1535)
at org.hibernate.internal.CriteriaImpl.list(CriteriaImpl.java:374)
at org.hibernate.search.batchindexing.impl.IdentifierConsumerEntityProducer.loadList(IdentifierConsumerEntityProducer.java:150)
at org.hibernate.search.batchindexing.impl.IdentifierConsumerEntityProducer.loadAllFromQueue(IdentifierConsumerEntityProducer.java:117)
at org.hibernate.search.batchindexing.impl.IdentifierConsumerEntityProducer.run(IdentifierConsumerEntityProducer.java:94)
at org.hibernate.search.batchindexing.impl.OptionallyWrapInJTATransaction.run(OptionallyWrapInJTATransaction.java:84)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
at java.lang.Thread.run(Thread.java:619)
Hibernate Search does not handle composite id classes used with #IdClass. A workaround would be to use #EmbeddedId and place idintervento and linguaIdlingua into InterventoPK.
Seems also that you asked the same question on the Hibernate Search forum - https://forum.hibernate.org/viewtopic.php?f=9&t=1024512
You can convert your custom object / composite key to Lucene Understandable format by using bridges. For example for a class
#Entity
#Indexed
public class Person {
#EmbeddedId #DocumentId Embedded id
#FieldBridge(impl=PersonPkBridge.class)
private PersonPK id;
...
}
You can write the bridge as like this. These codes are from the book 'Hibernate Search In Action'. I found it very helpful.
Does your class declared as composite key (it.domain.InterventoPK.class, as declared via #IdClass class-level annotation) only contain two integer fields? Since you've also annotated two such Integer fields with #Id on your Intervento class, the composite key class must only contain those fields and they must have the same name. Also that composite PK class needs to be Serializable. From the docs:
"map multiple properties as #Id properties and declare an external class to be the identifier type. This class, which needs to be Serializable, is declared on the entity via the #IdClass annotation. The identifier type must contain the same properties as the identifier properties of the entity: each property name must be the same, its type must be the same as well if the entity property is of a basic type, its type must be the type of the primary key of the associated entity if the entity property is an association (either a #OneToOne or a #ManyToOne)."
http://docs.jboss.org/hibernate/annotations/3.5/reference/en/html_single/
(search "Composite identifier" in page)
I had already replied on your question on the Hibernate forums, but to complete my suggestion:
An alternative to changing your mapping is that you add add a #DocumentId on a new getter, and return any object - maybe even a string - which is a unique composite of the two ids components.
(This requires defining mapping on getters and setters however)
When using JPA you can avoid specifying the #DocumentId but you don't have to, you can still use the annotation to override the definition of identity which you want to apply on the index mapping.