hibernate native query complex constructor mapping - java

Java, Spring Data JPA
I have 2 entities:
class Source {
Integer id;
String name;
}
class Item {
Integer id;
String name;
Integer sourceId;
}
I need statistic native query result like this:
select s.id source_id, s.name source_name, count(i.id) item_count
from source s
left join item i on s.id = i.source_id
group by s.id
And i want to have result in Java object MyResult:
class MyResult {
Source source;
Integer itemCount;
MyResult(Source source, Integer itemCount) {...}
}
The closest solution is using #SqlResultSetMapping like this:
#SqlResultSetMapping(
name = "MyResultMapping",
entities = {
#EntityResult(
entityClass = Source.class,
fields = {
#FieldResult(name = "id", column = "source_id"),
#FieldResult(name = "name", column = "source_name"),
}
),
...
???
}
)
OR
#SqlResultSetMapping(
name = "MyResultMapping",
classes = {
#ConstructorResult(
targetClass = MyResult.class,
columns = {
#ColumnResult(name = "???"),
???
}
)
}
)
With second variant i can use something like this:
MyResult(Integer sourceId, String sourceName, Integer itemsCount) {
this.source = new Source(sourceId, sourceName);
this.itemsCount = itemsCount;
}
but i want it to automate with #SqlResultSetMapping... (because my real objects more complex)

With Spring Data JPA it's better to use projections to achieve you need, for example:
public interface SourceWithItemCount {
Source getSource();
Integer getItemCount();
}
Then in your Source repository create HQL query method, like this:
public interface SourceRepo extends JpaRepository<Source, Integer> {
#Query("select s as source, count(i) like itemCount from Source s left join Item i on i.sourceId = s.id group by s"
List<SourceWithItemCount> getSourcesWithItemCount();
}
Important note is to use aliases for returned values (s as source etc.) it's allows Spring Data JPA to map them to projections properties.
Join on <condition> works from Hibernate version 5.1+ (if I'm not mistaken) so I recommend you to create classic one-to-many relation between your objects, like this, for example:
#Entity
class Source {
#Id private Integer id;
private String name;
#OneToMany #JoinColumn(name = "source_id") private List<Item> items;
}
#Entity
class Item {
#Id private Integer id;
private String name;
}
Then create JPQL query method supported by all versions of Hibernate (and other ORM providers):
#Query("select s as source, count(i) like itemCount from Source s left join s.items i group by s"
List<SourceWithItemCount> getSourcesWithItemCount();

Related

Convert postgresql join and group by query to JPA criteria API

I am having trouble to converting the following postgresql query (with a join and a group by) to JPA criteria API for a Spring Boot, JPA, Hibernate application:
select u.id, u.full_name, count(*) project_applications_count from users u
join project_applications pa on pa.created_by = u.id
group by u.id, u.full_name
having count(*) >= 1 and count(*) <= 5
The tables look like this:
create table project_applications (
id serial primary key,
...
city_id integer not null references cities (id),
created_by integer not null references users (id)
);
create table users (
id serial primary key,
...
full_name varchar(100) not null
);
And the entities look like this:
#Entity
#Table(name = "project_applications")
public class ProjectApplication {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#ManyToOne
#JoinColumn(name = "created_by")
private User createdBy;
...
}
#Entity
#Table(name = "users")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "full_name")
private String fullName;
...
}
I tried searching online for a solution but every exemple I found was using either a join or group by, but not both.
Using #akortex's idea with projections, I think something like this should work:
public class UserSummary {
private Long id;
private String fullName;
private Long count;
public UserSummary() {
}
public UserSummary(Long id, String fullName, Long count) {
this.id = id;
this.fullName = fullName;
this.count = count;
}
... (getters and setters)
}
public List<UserSummary> getSummaries(Integer minProjectAppsCount, Integer maxProjectAppsCount) {
CriteriaBuilder cb = _entityManager.getCriteriaBuilder();
CriteriaQuery<UserSummary> query = cb.createQuery(UserSummary.class);
Root<ProjectApplication> projectApp = query.from(ProjectApplication.class);
Join<ProjectApplication, User> userJoin = projectApp.join("createdBy", JoinType.INNER);
query.multiselect(userJoin.get("id"), userJoin.get("fullName"), cb.count(projectApp))
.groupBy(userJoin.get("id"), userJoin.get("fullName"));
List<Predicate> predicates = new ArrayList<>();
if (minProjectAppsCount != null ) {
Predicate p = cb.ge(cb.count(projectApp), minProjectAppsCount);
predicates.add(p);
}
if (maxProjectAppsCount != null ) {
Predicate p = cb.le(cb.count(projectApp), maxProjectAppsCount);
predicates.add(p);
}
query.having(predicates.toArray(new Predicate[0]));
return _entityManager.createQuery(query).getResultList();
}
You could potentially look into projections in order to achieve what you want.
For example consider the following projection and repository:
#Data
#AllArgsConstructor
public class ProjectApplicationSummary {
private Long id;
private String fullName;
private Long count;
}
And:
#Repository
public interface ProjectApplicationRepository extends JpaRepository<ProjectApplication, Long> {
#Query(
"""
SELECT new com.example.springdemo.entities.ProjectApplicationSummary(u.id, u.fullName, count(pa))
FROM User u, ProjectApplication pa
GROUP BY u.id, u.fullName
"""
)
List<ProjectApplicationSummary> getSummaries();
}
You will most likely need to tweak the query a bit (which revolves experimenting with JPQL) but other than that, the basic idea is there.
I'm not sure in my solution, but it should be similar. I took an idea from here. Maybe it helps you to resolve your problem.
public static Specification<User> getUsers() {
return Specification.where((root, query, criteriaBuilder) -> {
CriteriaQuery<User> criteriaQuery = criteriaBuilder.createQuery(User.class);
Subquery<Long> subQuery = criteriaQuery.subquery(Long.class);
Root<ProjectApplication> subRoot = subQuery.from(ProjectApplication.class);
subQuery
.select(criteriaBuilder.count(subRoot))
.where(criteriaBuilder.equal(root.get("id"), subRoot.get("createdBy").get("id")));
query
.multiselect(criteriaBuilder.construct(root.get("id"), root.get("fullName")))
.groupBy(root.get("id"), root.get("fullName"))
.having(criteriaBuilder.and(
criteriaBuilder.greaterThanOrEqualTo(subQuery.getSelection(), 1L),
criteriaBuilder.lessThanOrEqualTo(subQuery.getSelection(), 5L)));
return query.getRestriction();
});
}

Using arbitrary query as projection in spring data rest project

How it is possible to use arbitrary sql query (I mean native sql query) in some repository? My actual problem is this:
#Data //lombok thing
#Entity
public class A extends AuditModel {
private long id;
private String name;
#OneToMany(mappedBy="a") //Comments.a is owning side of association, i.e. comments table does have column called a_id as foreign key
#ToString.Exclude
private Set<Comments> comments = new HashSet();
#OneToMany(mappedBy="a") //SimpleFile.a is owning side of association
private Set<SimpleFile> comments = new HashSet();
}
Than I have my repository, which exposes nice crud interface using HAL+json representation. I am trying to enrich it with some projection/view particularly due to web UI to load one page data in single request. I am aware of excerps and projections, but they seems not to be enough powerful.
#Repository
#RepositoryRestResource
#Transactional(readOnly = true)
public interface ARepository extends PagingAndSortingRepository<A, Long> {
Page<A> findByNameContaining(String namePart, Pageable pageable);
#Query(
value = "SELECT a.name,\n" +
"(SELECT CAST(count(ac.id) AS int) FROM COMMENTS ac WHERE ac.a_id = a.id),\n" +
"(SELECT listagg(asf.id) FROM SIMPLE_FILES asf WHERE asf.a_id = a.id)\n" +
"FROM AS a\n" +
"WHERE a.id = :id",
nativeQuery = true
)
Optional<ACustomPage42DTO> getByIdProjectedForScreen42(Long id);
}
I have also tried to use JPQL, but there I had problem with fetch join (as I am not familiar with JPQL). My last evaluation query was something like this:
#Query("SELECT new sk.qpp.qqq.documents.projections.ACustomPage42DTO(" +
"a " +
"(SELECT CAST(count(ac) AS int) FROM COMMENTS ac WHERE ac.a = a)" +
")\n" +
"FROM A a\n" +
"LEFT JOIN FETCH a.simpleFiles\n" +
"WHERE a.id = :id"
)
I would like to get some general advice about what approach is best to implement custom and complex query to be returned in DTO (ideally with some specific links to actions when needed).
PS: Implementing interface and returning simple (primitive) data works. Also using JPQL to create custom DAO instance works (with simple types and with single instance of type A for example). Method for using given query method does appear in search methods of given entity endpoint. I would like to have something more reasonable, so I would like to have projection as defined in spring data rest project.
I have my DTO object fully under my control. I prefer it to use #Value or #Data annotation from project lombok, but it is not a need. I have tried also these versions of DTO definition (using interface works for simple data and similarly class works for simple data).
interface ACustomPage42DTO {
String getName();
long getCommentsCount();
Object getAsdf();
}
Or using equivalent class with some bonus, like custom toString() method possible, or some custom getter for computed data:
#Value //lombok thing, imutable "POJO"
public class ACustomPage42DTO {
String name;
long commentsCount;
Set<SimpleFile> simpleFiles;
public ACustomPage42DTO(A a, long count) {
// constructor used by JPQL, if it works
name = a.getName();
this.commentsCount = count;
this.simpleFiles = a.getSimpleFiles(); // should be already fetched, due to fetch join in JPQL
}
}
Both working approaches can be called using "search" url, instead of projection. I see my method getByIdProjectedForScreen42 on url http://localhost:9091/api/a/search listing. I would like to use it like (I think that is the "right" way) http://localhost:8080/api/a?projection=ACustomPage42DTOProjection .
Question is quite broad and touches couple of aspects:
custom JPA repository method using #Query
selecting results in your #Query
mapping #Query results to an interface
exposing new repository method through #RepositoryRestResource
TLDR: wrote an example of what is talked about with couple of basic tests https://github.com/ivarprudnikov/test-spring-jpa-repository-query-exposed-through-http
custom JPA repository method using #Query
As you have mentioned it is quite straightforward, just annotate a method with #Query and make sure your return type corresponds to what is being returned from the query, eg:
public interface FooRepository extends JpaRepository<FooEntity, Long> {
#Query(nativeQuery = true, value = "select f from foo f where f.name = :myParam")
Optional<FooEntity> getInSomeAnotherWay(String myParam);
}
selecting results in your #Query
You have given an example already but I'll simplify to make it easier and shorter.
Given entities FooEntity.java and BarEntity.java:
#Entity
#Table(name = "foo")
public class FooEntity {
#Id
#Column(name = "id", unique = true, nullable = false)
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "name", nullable = false)
private String name;
#OneToMany(mappedBy = "foo")
private Set<BarEntity> bars = new HashSet<>();
// getter setters excluded for brevity
}
#Entity
#Table(name = "bar")
public class BarEntity {
#Id
#Column(name = "id", unique = true, nullable = false)
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "name", nullable = false)
private String name;
#ManyToOne(targetEntity = FooEntity.class)
#JoinColumn(name = "foo_id", nullable = false, foreignKey = #ForeignKey(name = "fk_bar_foo"))
private FooEntity foo;
// getter setters excluded for brevity
}
We want now to return custom result set which contains FooEntity.name and count of FooEntity.bars:
SELECT f.name as name, count(b.id) as barCount FROM foo f, bar b WHERE f.id = :id AND b.foo_id = :id
+-----------------+----------+
| name | barCount |
+-----------------+----------+
| Jonny tables | 1 |
+-----------------+----------+
mapping #Query results to an interface
To map above result set we need an interface where getters nicely reflect what is being selected:
public interface ProjectedFooResult {
String getName();
Long getBarCount();
}
Now we can rewrite our repository method to:
#Query(nativeQuery = true,
value = "SELECT f.name as name, count(b.id) as barCount FROM foo f, bar b WHERE f.id = :id AND b.foo_id = :id")
Optional<ProjectedFooResult> getByIdToProjected(Long id);
exposing new repository method through #RepositoryRestResource
I am not very familiar with this but after adding org.springframework.data:spring-data-rest-hal-browser dependency I got this nice interface that exposed available methods after repository was annotated with #RepositoryRestResource. For a given repository which contains above mentioned details:
#RepositoryRestResource(path = "foo")
public interface FooRepository extends JpaRepository<FooEntity, Long> {
#Query(nativeQuery = true, value = "SELECT f.name as name, count(b.id) as barCount FROM foo f, bar b WHERE f.id = :id AND b.foo_id = :id")
Optional<ProjectedFooResult> getByIdToProjected(Long id);
}
the method will be exposed through http://localhost:8080/foo/search/getByIdToProjected?id=1 when running locally.
As mentioned above the reference implementation is on Github https://github.com/ivarprudnikov/test-spring-jpa-repository-query-exposed-through-http
Additional helpful documentation for 'Custom Implementations for Spring Data Repositories'

Map any collection with NamedNativeQuery and SqlResultSetMapping

So I am writing native queries because they are very complex to get all the needed data and so on. I am currently facing a problem which is normally done by Hibernate/JPA etc.
Imagine the following:
#Entity
FooEntity{
#Id
public Long id;
#ManyToMany
public List<FeeEntity> feeEntities;
}
#Entity
FeeEntity{
#Id
public Long id;
#Column
public String name;
}
And some DTO
FooDTO{
private final Long id;
private final List<FeeDTO> someStrings;
public FooDTO(Long id, List<FeeDTO> feeDtos){
...
}
}
My #SqlResultSetMapping looks basically like
#SqlResultSetMapping(name = "FooDTO",
classes = #ConstructorResult(targetClass = FooDTO.class, columns = {
#ColumnResult(name = "id", type = Long.class),
//TODO #ColumnResult(name = "feeDtos", type = FeeDtos.class)
})
)
The named native query looks something like:
#NamedNativeQueries({
#NamedNativeQuery(
name = "FooData",
query = "SELECT MAINSELECT.ID AS id, " +
"???" +
" FROM Foo MAINSELECT WHERE ... " +
...,
resultSetMapping = "FooDTO")
})
How do I have to write the native query? Is this even possible without a subquery or do I have to do/execute a subquery for each datarow of the result? I was not able to find something on beloved google.
That's not possible.
The result of SQL query is always a table. So there are no nested tables and hence you cannot map it to a Collection.

DTO Query with JPA CriteriaQuery and Hibernate using entity as a DTO constructor creates many selects

hibernate 5.2.10.Final
jpa 2.1
I want to map a projection query to a DTO (Data Transfer Object) with JPA Criteria Query and Hibernate. I specify a constructor that will be applied to the results of the query execution.
If the constructor is for entire entity class, I have multiple of selects instead one(it is a long running process for thousands of records). If the constructor is for a set of params of the Entity then I see only one select in the console. I can't understand where I've mistaken or is it a bug?
public class ServiceDAO {
public List<ServicesDTO> getAllServicesByFilter(ServicesFilter filter) {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<ServicesDTO> criteria = cb.createQuery(ServicesDTO.class);
Root<ServicesEntity> serviceEntity = criteria.from(ServicesEntity.class);
// here is only one select to get list of services
criteria.select(cb.construct(ServicesDTO.class, serviceEntity.get("active"), serviceEntity.get("providerId"), serviceEntity.get("serviceId")));
// in this case I have multiple selects
//criteria.select(cb.construct(ServicesDTO.class, serviceEntity));
if(filter != null) {
List<Predicate> pcl = new ArrayList<Predicate>();
if(filter.getActive() != null)
pcl.add(cb.equal(serviceEntity.get("active"), filter.getActive()));
if(filter.getProviderId() != null)
pcl.add(cb.equal(serviceEntity.get("providerId"), filter.getProviderId()));
if(filter.getServiceId() != null)
pcl.add(cb.equal(serviceEntity.get("serviceId"), filter.getServiceId()));
criteria.where(pcl.toArray(new Predicate[pcl.size()]));
}
return entityManager.createQuery(criteria).getResultList();
}
}
-
public class ServicesDTO implements Serializable {
private static final long serialVersionUID = 1L;
private Boolean active;
private Integer providerId;
private Integer serviceId;
public ServicesDTO() {}
public ServicesDTO(Boolean active, String providerId, Integer serviceId) {
this.active = active;
this.providerId = Integer.parseInt(providerId);
this.serviceId = serviceId;
}
public ServicesDTO(ServicesEntity service) {
if(service != null) {
this.active = service.isActive();
this.providerId = Integer.parseInt(service.getProviderId());
this.serviceId = service.getServiceId();
}
// getters & setters
}
-
#Entity
#Table
public class ServicesEntity {
#Id
#Column(name = "id", unique = true)
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
#Column(name = "serviceId", nullable = false)
private int serviceId;
#Column(nullable = false)
private String providerId;
#ManyToOne(fetch=FetchType.LAZY)
#JoinColumn(name="categoryId")
private Categories categoryId;
private boolean active;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "service", cascade = CascadeType.ALL)
private List<Service_Area_Ref> areas = new ArrayList<Service_Area_Ref>();
#ManyToOne(fetch=FetchType.LAZY, optional = true)
#JoinColumn(name="parentCatId")
private Categories parentCatId;
public ServicesEntity() {}
public ServicesEntity(int serviceId) {
this.serviceId = serviceId;
}
// getters & setters
// equals & hashcode
}
Yea, so it does. There is probably not much of a use case for that. Given
#Entity
public class A {
#Id #GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
private Integer value;
public class ADto {
private Integer va;
public ADto(A a) {
this.va = a.getValue();
}
public ADto(Integer va) {
this.va = va;
}
Then
tx.begin();
A a1 = new A();
a1.setValue(1);
A a2 = new A();
a1.setValue(2);
em.persist(a1);
em.persist(a2);
tx.commit();
em.clear();
System.out.println("As usual");
em.createQuery("select new dto.ADto(a.value) from A a where a.value <= 2", ADto.class).getResultList();
System.out.println("As A");
em.createQuery("select new dto.ADto(a) from A a where a.value <= 2", ADto.class).getResultList();
gives you
create table A (id integer generated by default as identity (start with 1), value integer, primary key (id))
create table B (id integer generated by default as identity (start with 1), value integer, primary key (id))
insert into A (id, value) values (default, ?)
insert into A (id, value) values (default, ?)
As usual
select a0_.value as col_0_0_ from A a0_ where a0_.value<=2
As A
select a0_.id as col_0_0_ from A a0_ where a0_.value<=2
select a0_.id as id1_0_0_, a0_.value as value2_0_0_ from A a0_ where a0_.id=?
select a0_.id as id1_0_0_, a0_.value as value2_0_0_ from A a0_ where a0_.id=?
And you don't like the fact that entity A is selected each time for a new ADto instance. It's probably done that way because you could have created a DTO with multiple entities, not just A, like A, B, and C and so how would JPA/Hibernate do that conveniently in a single select statement? While it could select all the attributes and then keep track of which attributes belong to which entities and then construct them and pass them to your DTO so you can deconstruct them that seems like a lot of work for a rare thing. It's probably more efficient and better all around if you select the attributes you want and make a constructor out of whatever that is, as in the first case.
I am using Hibernate 5.3 and also encounter this behaviour. But I found that if using JPA Tuple as a DTO container and multiselect, this problem will not happen. So my final solution is use Tuple to query the result set first and then convert it to DTO manually , something likes:
CriteriaQuery<Tuple> criteria = cb.createTupleQuery();
.......
criteria.multiselect(serviceEntity);
List<ServicesDTO> result = entityManager.createQuery(criteria).getResultList().stream()
.map(t->new ServicesDTO(t.get(0,ServicesEntity.class)))
.collect(toList());

How to use hibernate query on two tables (join table)?

I have these tables: cont(id_cont, user, pass)
emp(emp_id, name, cont_id_cont (fk))
#Entity
#Table(name = "emp", catalog = "", uniqueConstraints = {
#UniqueConstraint(columnNames = "cont_id_cont") })
public class Emp implements java.io.Serializable{
private int id_emp;
private ContUser contUser;
private String name;
and
#Entity
#Table(name = "cont", catalog = "", uniqueConstraints = {
#UniqueConstraint(columnNames = "pass") })
public class Cont implements java.io.Serializable{
private int id_cont;
private String user;
private String pass;
private Set<Emp> empForCont = new HashSet<Emp>(0);
}
Now: I want this query:
select cont.user, emp.name, emp.cont_id_cont from cont
inner join emp on cont.id_cont= emp.cont_id_cont where cont.user = 'gbs04405';
This style of accessing data is not what ORM is intended for. If you are using Hibernate, you should (usually) access data through objects. To make this work, instead of embedding SQL like constraints, define relations #OneToMany, #ManyToOne, and/or #ManyToMany between objects where necessary.
In addition, you should consider using HQL (or JPQL) instead of pure SQL, to achieve what you want.
It should be something like this:
SELECT e FROM Emp e JOIN e.contUser u WHERE u.user = :userstring
You can check here for further JPQL syntax.

Categories