Spring JPA Repository Sub-Query In Native Query - spring

I am trying to run a native query on a repository method so that it returns the results with some counts. It was too complicated to do with JPQL, so I opted for a native query instead.
Repository
#RepositoryRestResource(collectionResourceRel="projects", path="projects")
interface ProjectRepository extends BaseRepository<Project, Long>, ProjectRepositoryCustom {
#Query(
value="SELECT p.id, p.user_id, p.title, p.description, p.created_on, p.version,(SELECT COUNT(0) FROM projectparts WHERE project_id = p.id) AS parts,(SELECT COUNT(0) FROM requests WHERE project_id = p.id) AS requests FROM projects AS p ORDER BY ?#{#pageable}",
countQuery="SELECT COUNT(0) FROM projects",
nativeQuery=true
)
Page<Project> findAll(Pageable pageable)
}
The entity has 2 properties annotated with #Transient so that the info is not persisted to the database. All the data comes back fine except the 2 transient properties which return null for the values. When I copy the query from the console and paste it in MySQL Workbench, the results are as expected and I see the counts that I need. Anyhow, not sure if there is anything else that needs to be done in order to get this native query to work as an annotation. I hard coded a value in the sub-query SELECT 55 FROM... just to see if it was a problem with the count and it still returned as null. I ran the query in Workbench and it works fine.
I've tried changing the transient property type from Integer, Long, BigInteger, long, int... and none of that made a difference. Since I'm using Groovy, I also tried def to let Groovy infer the type and that didn't work either.
I also tried running the project from the terminal instead and it still didn't work. I've tried it on a Mac and Linux and had no luck with displaying the results of the counts.

This will not work. You could use an SQLConstructorExpression however the returned instances would be unmanaged which is a major drawback.
An better option is to create a simple DB view which holds the pieces of summary info for the Project. You can them map the Project entity to both it's table and the associated summary view using the #SecondaryTable functionality of JPA.
https://en.wikibooks.org/wiki/Java_Persistence/Tables#Example_mapping_annotations_for_an_entity_with_multiple_tables
An added benefit is that you can sort and query on the summary values as for any other property.
Updated mapping:
#Entity
#Table(name = "projects")
#SecondaryTable(name = "projects_summary_vw")
public class Project{
//use Integer rather than int to avoid issue outlined here:
//http://stackoverflow.com/a/37160701/1356423
#Column(name = "parts", table = "projects_summary_vw",
insertable="false", updateable="false")
private Integer partsCount;
#Column(name = "requests", table = "requestsCount"
insertable="false", updateable="false")
private Integer requestsCount;
//other mappings as required
}
No Custom query required:
#RepositoryRestResource(collectionResourceRel="projects",
path="projects")
interface ProjectRepository extends BaseRepository<Project, Long>,
ProjectRepositoryCustom {
}
An alternative non-JPA compliant solution may be to use some vendor specific extension rather than a view. Hibernate for example has an #Formula annotation which could be used:
https://docs.jboss.org/hibernate/orm/5.1/javadocs/org/hibernate/annotations/Formula.html
#Entity
#Table(name = "projects")
public class Project{
#Formula("my count query as native sql")
private Integer partsCount;
#Formula("my count query as native sql")
private Integer requestsCount;
//other mappings as required
}

Related

Spring data jdbc mapping not working if use not primary key

I have 2 entities:
#Data
#Table("main_entities")
public class MainEntity {
#Id
private Long id;
private String anotherId;
#MappedCollection(idColumn = "main_entity_id")
private SecondEntity secondEntity;
}
#Data
#Table("second_entities")
public class SecondEntity {
#Id
private Long id;
private Long mainEntityId;
}
And exists the repository:
public interface MainEntityRepository extends CrudRepository<MainEntity, Long> {
#Query("SELECT * FROM main_entities WHERE another_id = :anotherId")
Optional<MainEntity> findByAnotherId(#Param("anotherId") String anotherId);
}
When I use the MainEntityRepository#findById(Long) - the SecondEntity is available, when I use the MainEntityRepository#findByAnotherId(String) - the SecondEntity is null
Update 2021.12.15:
if set the
#MappedCollection(idColumn = "main_entity_id")
private Set<SecondEntity> secondEntities;
Its allows to get the mapped collection via MainEntityRepository#findByAnotherId(String)
Spring Data JDBC loads 1:1 relationships with a single join and expects you to do the same when you specify a custom query.
In order to avoid ambiguities you have to use column aliases which prefix the columns with the property name of the 1:1 relation ship plus an _.
So your select should look like this:
SELECT M.ID, M.ANOTHER_ID, S.ID AS SECONDENTITY_ID, S.MAIN_ENTITY_ID AS SECONDENTITY_MAIN_ENTITY_ID
FROM MAIN_ENTITIES M
JOIN SECOND_ENTITIES S
ON M.ID = S.MAIN_ENTITY_ID
WHERE ANOTHER_ID = :anotherId
I created a complete example.
Side note: I recommend not to have an id on the non-aggregate-root entities, nor to have the reference back to the aggregate root in these entities. See Spring Data JDBC - How do I make Bidirectional Relationships?
so you want to fetch the second entity together with your main entity with your custom method?
I thinkt it has to do with the fetch type of your main entity. It is lazy by default and if you want to load both entitys you can try to set the fetch type to eager for the second entity field in your main entity. But be aware that this is not always the best option but rather a quick fix. See here for more information about fetch types.
You can also try using the join fetch as described in the accepted answer here to achieve your requested behaviour. I think that this would be the best solution.
I hope I got your question right if not please try to explain with further detail.

Dynamic JPA query

I have two entities Questions and UserAnswers. I need to make an api in spring boot which returns all the columns from both the entities based on some conditions.
Conditions are:
I will be give a comparator eg: >, <, =, >=, <=
A column name eg: last_answered_at, last_seen_at
A value of the above column eg: 28-09-2020 06:00:18
I will need to return an inner join of the two entities and filter based on the above conditions.
Sample sql query based on above conditions will be like:
SELECT q,ua from questions q INNER JOIN
user_answers ua on q.id = ua.question_id
WHERE ua.last_answered_at > 28-09-2020 06:00:18
The problem I am facing is that the column name and the comparator for the query needs to be dynamic.
Is there an efficient way to do this using spring boot and JPA as I do not want to make jpa query methods for all possible combinations of columns and operators as it can be a very large number and there will be extensive use of if else?
I have developed a library called spring-dynamic-jpa to make it easier to implement dynamic queries with JPA.
You can use it to write the query templates. The query template will be built into different query strings before execution depending on your parameters when you invoke the method.
This sounds like a clear custom implementation of a repository method. Firstly, I will make some assumptions about the implementation of your entities. Afterwards, I will present an idea on how to solve your challenge.
I assume that the entities look basically like this (getters, setters, equals, hachCode... ignored).
#Entity
#Table(name = "questions")
public class Question {
#Id
#GeneratedValue
private Long id;
private LocalDateTime lastAnsweredAt;
private LocalDateTime lastSeenAt;
// other attributes you mentioned...
#OneToMany(mappedBy = "question", cascade = CascadeType.ALL, orphanRemoval = true)
private List<UserAnswer> userAnswers = new ArrayList();
// Add and remove methods added to keep bidirectional relationship synchronised
public void addUserAnswer(UserAnswer userAnswer) {
userAnswers.add(userAnswer);
userAnswer.setQuestion(this);
}
public void removeUserAnswer(UserAnswer userAnswer) {
userAnswers.remove(userAnswer);
userAnswer.setQuestion(null);
}
}
#Entity
#Table(name = "user_answers")
public class UserAnswer {
#Id
#GeneratedValue
private Long id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "task_release_id")
private Question question;
}
I will write the code with the knowledge about the JPA of Hibernate. For other JPAs, it might work similarly or the same.
Hibernate often needs the name of attributes as a String. To circumvent the issue of undetected mistakes (especially when refactoring), I suggest the module hibernate-jpamodelgen (see the class names suffixed with an underscore). You can also use it to pass the names of the attributes as arguments to your repository method.
Repository methods try to communicate with the database. In JPA, there are different ways of implementing database requests: JPQL as a query language and the Criteria API (easier to refactor, less error prone). As I am a fan of the Criteria API, I will use the Criteria API together with the modelgen to tell the ORM Hibernate to talk to the database to retrieve the relevant objects.
public class QuestionRepositoryCustomImpl implements QuestionRepository {
#PersistenceContext
private EntityManager entityManager;
#Override
public List<Question> dynamicFind(String comparator, String attribute, String value) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Question> cq = cb.createQuery(Question.class);
// Root gets constructed for first, main class in the request (see return of method)
Root<Question> root = cq.from(Question.class);
// Join happens based on respective attribute within root
root.join(Question_.USER_ANSWER);
// The following ifs are not the nicest solution.
// The ifs check what comparator String contains and adds respective where clause to query
// This .where() is like WHERE in SQL
if("==".equals(comparator)) {
cq.where(cb.equal(root.get(attribute), value));
}
if(">".equals(comparator)) {
cq.where(cb.gt(root.get(attribute), value));
}
if(">=".equals(comparator)) {
cq.where(cb.ge(root.get(attribute), value));
}
if("<".equals(comparator)) {
cq.where(cb.lt(root.get(attribute), value));
}
if("<=".equals(comparator)) {
cq.where(cb.le(root.get(attribute), value));
}
// Finally, query gets created and result collected and returned as List
// Hint for READ_ONLY is added as lists are often just for read and performance is better.
return entityManager.createQuery(cq).setHint(QueryHints.READ_ONLY, true).getResultList();
}
}

Efficient way to fetch list size

I have an entity like below. When I need to list comment size of company I'm calling totalComments() method. For this does hibernate go to the database and fetch entire comment data or just querying with count(*)? If hibernate fetch entire comment what is the efficient way for getting comment size?
#Entity
#Table(name = "companies")
public class Company extends ItemEntity {
#OneToMany(fetch = FetchType.LAZY)
#JoinTable(name="companies_comments",
joinColumns=#JoinColumn(name="company_id"),
inverseJoinColumns=#JoinColumn(name="comment_id"))
private Set<Comment> comments = new HashSet<>();
public void addComment(Comment comment) {
this.comments.add(comment);
}
public int totalComments() {
return this.comments.size();
}
}
You should drop the own method counter and create a specific (business) query to retrieve the size of the list, such as
public long getCommentsCount(Company c) {
String query = "SELECT COUNT(cm) FROM Company AS c JOIN c.comments AS cm WHERE c = :company";
return entityManager.createQuery(q, Long.class).setParameter("company", c).getSingleResult();
}
Some persistence provider may optimize performance when this kind of query is loaded as a #NamedQuery on entity, or when using CriteriaQuery API.
Depending on your database, you may need to change the return class to Number.class and convert to long.
If you want to tune even more your performance, use createNativeQuery method and write your own pure SQL, but keep in mind that changes on db schema requires to review theses queries.
I found the answer. If we don't adjust for getting collection size of entity hibernate loads every comment. We can solve this performance issue in two ways.
We can use #LazyCollection(LazyCollectionOption.EXTRA) like below. By LazyCollectionOption.EXTRA .size() and .contains() won't initialize the whole collection.
#OneToMany(fetch = FetchType.LAZY)
#LazyCollection(LazyCollectionOption.EXTRA)
#JoinTable(name="companies_comments",
joinColumns=#JoinColumn(name="company_id"),
inverseJoinColumns=#JoinColumn(name="comment_id"))
private Set<Comment> comments = new HashSet<>();
Or we can use #Formula annotation.
#Formula(SELECT COUNT(*) FROM companies_comments cc WHERE cc.company_id = id)
private int numberOfComments;
Edit after 8 months: For simplicity and performance perspective, we should create a JPA Query Method like below.
#Repository
public interface CommentRepository extends JpaRepository<Comment, Long> {
int countAllByCompany(Company company);
}
We should never use getComments().size() for this purpose, because this way all comments are loaded into memory and this may be cause performance issues.
It is also true when adding comments to the collection. We shouldn't use getComments().add(newComment). When we have OneToMany relation, all we have to do is set the company field of the comment like as newComment.setCompany(company), and perform the persist operation. Therefore, it is recommended to define OneToMany relationships bidirectional.

How to query more than one columns but not all columns with #Query but still use the Domain Data Model to map with Spring Data JDBC?

My Data model is
#Getter
#Setter
public class Customer {
#Id private ID id;
#CreatedDate protected Instant createdAt;
#LastModifiedDate protected Instant updatedAt;
#CreatedBy protected String createdBy;
#LastModifiedBy protected String updatedBy;
#Version protected Long version;
private UUID orderId;
private String offer;
}
My Repository is
public interface CustomerRepository extends CrudRepository<Customer, UUID> {
#Query(
"SELECT ID, Offer FROM Customer WHERE orderId = :orderId ")
List<Customer> findCustomerByOrderId(
#Param("orderId") UUID orderId);
}
This will result in an exception saying 'orderId column not found [42122-190]'. So Spring expects you to always query all the columns. I understand that with JPA we have a strong mapping between the Entities and the Data Schema. But the whole point of spring data JDBC is avoiding the tight coupling between POJO's data model and database schema. Why not the EntityRowMapper is just mapping NULL to the properties which are not part of the query?
Is there a way to tell the RowMapper used, to ignore properties which are not part of the query? Creating separate RowMapper for these simple queries seems a lot of unnecessary work.
I still can work around this by changing the query like
#Query(
"SELECT ID, Offer, OrderId, null as CreatedAt, null as CreatedBy, null as UpdatedAt, null as UpdatedBy, null as Version FROM Customer WHERE orderId = :orderId ")
But this will still serialize the entire object with null values. Am I missing something obvious here?
Note This is not Spring Data JPA. Its Spring Data JDBC.
Edit
Looking more into it, the exception is from h2 database lib.
Caused by: org.h2.jdbc.JdbcSQLException: Column "orderid" not found [42122-190]
at org.h2.message.DbException.getJdbcSQLException(DbException.java:345)
at org.h2.message.DbException.get(DbException.java:179)
at org.h2.message.DbException.get(DbException.java:155)
at org.h2.jdbc.JdbcResultSet.getColumnIndex(JdbcResultSet.java:3129)
at org.h2.jdbc.JdbcResultSet.get(JdbcResultSet.java:3217)
at org.h2.jdbc.JdbcResultSet.getObject(JdbcResultSet.java:522)
at com.zaxxer.hikari.pool.HikariProxyResultSet.getObject(HikariProxyResultSet.java)
at org.springframework.data.jdbc.core.EntityRowMapper.readFrom(EntityRowMapper.java:127)
You can't at least right now.
There are three solutions to this, two of which you already pointed out:
extend your select statement with , NULL as <column-name> for all the missing columns.
I'm not sure if
But this will still serialize the entire object with null values.
means that this isn't working for you in some way.
specify a RowMapper.
You could use a class containing exactly the fields returned by the query. It could even have getters for the other columns if you want an interface implemented by both your normal entity and the partial entity.
You write:
But the whole point of spring data JDBC is to avoid the tight coupling between pojo's data model and database schema.
This is not quite right.
An important goal of Spring Data JDBC is to not have a run time connection between entities and table rows.
This would require proxies or similar and brings a lot of complexity.
But the structural mapping between entities and table is probably going to be stronger (and certainly is right now) since all the variants of mappings available in JPA bring complexity.
And the main goal in Spring Data JDBC is to be conceptually simpler than JPA.
You also ask
Why not the EntityRowMapper is just mapping NULL to the properties which are not part of the query?
I'm not sure if I actively thought about it when I coded it but I don't like the idea of defaulting to NULL because this would make it easy to accidentally not load a column because you have a typo in an alias.
But I'm not against alternative solutions.
If you have an idea please create a feature request.

Spring Data: "delete by" is supported?

I am using Spring JPA for database access. I am able to find examples such as findByName and countByName, for which I dont have to write any method implementation. I am hoping to find examples for delete a group of records based on some condition.
Does Spring JPA support deleteByName-like delete? Any pointer is appreciated.
Regards and thanks.
Deprecated answer (Spring Data JPA <=1.6.x):
#Modifying annotation to the rescue. You will need to provide your custom SQL behaviour though.
public interface UserRepository extends JpaRepository<User, Long> {
#Modifying
#Query("delete from User u where u.firstName = ?1")
void deleteUsersByFirstName(String firstName);
}
Update:
In modern versions of Spring Data JPA (>=1.7.x) query derivation for delete, remove and count operations is accessible.
public interface UserRepository extends CrudRepository<User, Long> {
Long countByFirstName(String firstName);
Long deleteByFirstName(String firstName);
List<User> removeByFirstName(String firstName);
}
Derivation of delete queries using given method name is supported starting with version 1.6.0.RC1 of Spring Data JPA. The keywords remove and delete are supported. As return value one can choose between the number or a list of removed entities.
Long removeByLastname(String lastname);
List<User> deleteByLastname(String lastname);
2 ways:-
1st one Custom Query
#Modifying
#Query("delete from User where firstName = :firstName")
void deleteUsersByFirstName(#Param("firstName") String firstName);
2nd one JPA Query by method
List<User> deleteByLastname(String lastname);
When you go with query by method (2nd way) it will first do a get call
select * from user where last_name = :firstName
Then it will load it in a List
Then it will call delete id one by one
delete from user where id = 18
delete from user where id = 19
First fetch the list of object, then for loop to delete id one by one
But, the 1st option (custom query),
It's just a single query
It will delete wherever the value exists.
Since in 2nd option it is making multiple DB query, try to use the first option.
Go through this link too https://www.baeldung.com/spring-data-jpa-deleteby
If you take a look at the source code of Spring Data JPA, and particularly the PartTreeJpaQuery class, you will see that is tries to instantiate PartTree.
Inside that class the following regular expression
private static final Pattern PREFIX_TEMPLATE = Pattern.compile("^(find|read|get|count|query)(\\p{Lu}.*?)??By")
should indicate what is allowed and what's not.
Of course if you try to add such a method you will actually see that is does not work and you get the full stacktrace.
I should note that I was using looking at version 1.5.0.RELEASE of Spring Data JPA
If you will use pre defined delete methods as directly provided by spring JPA then below two queries will be execute by the framework.
First collect data(like id and other column) using by execute select query with delete query where clause.
then after getting resultSet of first query, second delete queries will be execute for all id(one by one)
Note : This is not optimized way for your application because many queries will be execute for single MYSQL delete query.
This is another optimized way for delete query code because only one delete query will execute by using below customized methods.
#NamedNativeQueries({
#NamedNativeQuery(name = "Abc.deleteByCreatedTimeBetween",
query = "DELETE FROM abc WHERE create_time BETWEEN ?1 AND ?2")
,
#NamedNativeQuery(name = "Abc.getByMaxId",
query = "SELECT max(id) from abc")
})
#Entity
public class Abc implements Serializable {
}
#Repository
public interface AbcRepository extends CrudRepository {
int getByMaxId();
#Transactional
#Modifying
void deleteByCreatedTimeBetween(String startDate, String endDate);
}
It works just
import org.springframework.transaction.annotation.Transactional;
#Transactional
Long removeAddressByCity(String city);
Yes , deleteBy method is supported
To use it you need to annotate method with #Transactional
here follows my 2 cents. You can also use native queries, like:
#Modifying
#Query(value="delete from rreo r where r.cod_ibge = ?1 and r.exercicio= ?2", nativeQuery = true)
void deleteByParameters(Integer codIbge, Integer exercicio);
#Query(value = "delete from addresses u where u.ADDRESS_ID LIKE %:addressId%", nativeQuery = true)
void deleteAddressByAddressId(#Param("addressId") String addressId);

Resources