QueryDSL query failing when querying with entity with null ID - java

I have a DB with 40 columns, and a REST API in Spring Boot that can be used to query the DB with any combination of parameters, which necessitates the use of dynamic query creation, which is why I have chosen to use QueryDSL.
Before making the query, I convert the query params to the Entity object using an ObjectMapper, then do a count using something like:
queryFactory.select(qTableRequest.rowId.count()).from(qTableRequest).where(qTableRequest.eq(TableRequest))
which works fine when the #Id column of the Entity is not null, but fails with
TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing
when it's null.
Side note: I can't use Spring's Query by Example because there are cases where I need non-singular matching.
I tried to extract the non-null fields of the entity and construct the query using .and(), .or() etc. as a BooleanExpression and it works, but it requires a lot of boilerplate code and it would be nice to have a simple solution to query for the Entity directly.
My question is: is it possible to use an Entity in the query without an Id?
Entity:
#Entity
#Table(name = "table", schema = "schema")
#AllArgsConstructor
#NoArgsConstructor
#Getter
public class TableRequest {
#Id
#Column(name = "row_id")
public String rowId;
#Column(name = "order_id")
public String orderId;
... // 38 more rows
}
Query:
private BooleanExpression constructQuery(SearchRequest searchRequest) {
TableRequest searchRequestToEntity = objectMapper.convertValue(SearchRequest, TableRequest.class);
BooleanExpression queryParameters = qTableRequest.eq(searchRequestToEntity);
...
}
public SearchResponse search(SearchRequest searchRequest) {
BooleanExpression queryParameters = constructQuery(searchRequest);
long rowCount = queryFactory.select(qTableRequest.rowId.count())
.from(qTableRequest).where(queryParameters).fetchFirst();
...
}

Related

JPA populate transient field with SQL query result column

I'm working with this native SQL request :
SELECT o.*, COUNT(*) OVER() AS total_count
FROM observation o
WHERE ...
ORDER BY code
LIMIT 1000;
I need to put the total_count information in a transient field in this object :
#Entity
#Table(name = "observation")
public class Observation {
#Column(name = "code")
private String code;
...other fields
#Transient
private Long totalCount;
}
in order to get List<Observation> results = query.getResultList(); with the totalCount information.
I know that #SqlResultSetMapping could do the job with #ConstructorResult, but the class has a lot of fields and I prefer avoid creating the constructor with all fields.
Is there an easier way to achieve this ?

JPA Query to deleteById for a Composite Key declared using IdClass

Question
If I have declared my (composite) primary key using #IdClass, how do I write my #Query to be able to issue a DELETE query using a Collection<MyIdClass> ?
Secondary question
Will the CASCADE actually trigger the deletion of the associated AnotherEntity despite using #Query?
Current model
#Entity
#Table(name = "myentity")
#JsonIgnoreProperties(ignoreUnknown = true)
#Data
#NoArgsConstructor
#AllArgsConstructor
#Builder
#IdClass(MyIdClass.class)
public class MyEntity {
#Id
#Column(updatable = false)
private String foo;
#Id
#Column(updatable = false)
private String bar;
#OneToOne(cascade = CascadeType.ALL, orphanRemoval = true)
#JoinColumn(name = "my_foreign_key", referencedColumnName = "external_pk")
private AnotherEntity anotherEntity;
}
#Data
#Builder
#NoArgsConstructor
#AllArgsConstructor
public class MyIdClass implements Serializable {
private String foo;
private String bar;
}
#Entity
#Table(name = "anotherentity")
#JsonIgnoreProperties(ignoreUnknown = true)
#Data
#SuperBuilder
#NoArgsConstructor
#AllArgsConstructor
public class AnotherEntity {
#Id
#Column(name = "external_pk", nullable = false, updatable = false)
private String externalPk;
}
What I've read
A few resources:
https://www.baeldung.com/spring-data-jpa-query
https://www.baeldung.com/spring-data-jpa-delete
https://stackoverflow.com/a/36765129/9768291
And I also found this SO question which seemed very close to what I'm looking for, but unfortunately there are no answers.
Goal
Something similar to:
#Repository
public interface MyCRUDRepository extends CrudRepository<MyEntity, MyIdClass> {
#Modifying
#Query("DELETE FROM myentity m WHERE m IN ?1") // how do I write this?
void deleteAllWithIds(Collection<MyIdClass> ids);
}
Ultimately, I want to do this to batch my DELETE requests to increase the performance.
Pitfalls I'm trying to avoid
I know there is a deleteAll(Iterable<? extends MyEntity>) but then I need to actually have those entities to begin with, which would require extra calls to the DB.
There is also deleteById(MyIdClass), but that actually always issues a findById before sending a single DELETE statement as a transaction: not good for the performance!
Potentially irrelevant precision
I'm not sure if that can help, but my JPA provider is EclipseLink. My understanding is that there are properties for batching requests, and that's ultimately what I'm aiming to use.
However, I'm not entirely sure what are the internal requirements for that batching to happen. For example, if I did a deleteById in a for-loop, would the alternating SELECT and DELETE statements prevent the batching from happening? The documentation is quite scarce about that.
If you're positive IdClass is a better choice than EmbeddedId in your situation, you could add an extra mapping to MyEntity :
#Embedded
#AttributeOverrides({
#AttributeOverride(name = "foo",
column = #Column(name = "foo", insertable = false, updatable = false)),
#AttributeOverride(name = "bar",
column = #Column(name = "bar", insertable = false, updatable = false))})
private MyIdClass id;
and use it in you repository:
#Modifying
#Query("DELETE FROM MyEntity me WHERE me.id in (:ids)")
void deleteByIdIn(#Param("ids") Collection<MyIdClass> ids);
This will generate a single query: delete from myentity where bar=? and foo=? [or bar=? and foo=?]..., resulting in this test to pass (with following table records insert into myentity(foo,bar) values ('foo1', 'bar1'),('foo2', 'bar2'),('foo3', 'bar3'),('foo4', 'bar4');):
#Test
#Transactional
void deleteByInWithQuery_multipleIds_allDeleted() {
assertEquals(4, ((Collection<MyEntity>) myEntityRepository.findAll()).size());
MyIdClass id1 = new MyIdClass("foo1", "bar1");
MyIdClass id2 = new MyIdClass("foo2", "bar2");
assertDoesNotThrow(() -> myEntityRepository.deleteByIdIn(List.of(id1, id2)));
assertEquals(2, ((Collection<MyEntity>) myEntityRepository.findAll()).size());
}
I think you are looking for something that will generate a query like this
delete from myentity where MyIdClass in (? , ? , ?)
You can try from this post, it may help you.
This answer provided great insight, but it seems like the approach only works for Hibernate. EclipseLink, which is the JPA Provider that I'm forced to use, would keep throwing an error at me, for the same code.
The only working solution I found is the following hack:
JPA Query for Spring #Repository
#Repository
public interface MyCRUDRepository extends CrudRepository<MyEntity, MyIdClass> {
#Modifying
#Query("DELETE FROM myentity m WHERE CONCAT(m.foo, '~', m.bar) IN :ids")
void deleteAllWithConcatenatedIds(#Param("ids") Collection<String> ids);
}
Associated index for the DB (Postgres)
DROP INDEX IF EXISTS concatenated_pk_index;
CREATE UNIQUE INDEX concatenated_pk_index ON myentity USING btree (( foo || '~' || bar ));
Explanation
Since EclipseLink refuses to properly treat my #IdClass, I had to adapt the service to concatenate the composite key into a single String. Then, in Postgres, you can actually create an index on that concatenation of different composite key columns.
Labeling the index as UNIQUE will greatly improve the performance of that query, but should only be done if you are sure that the concatenation will be unique (in my case it is since I'm using all the columns of the composite key).
The calling service then only has to do something like String.join("~", dto.getFoo(), dto.getBar()) and to collect all of those into the list that will be passed to the repository.

How to fetch exact matching records in Spring JPA #Query

I have two entities
1. Request
#Getter
#Setter
#Entity
#Table(name = "request")
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Request {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#Column(name = "status")
private String status;
#OneToMany(mappedBy = "requestId", fetch = FetchType.LAZY)
private Collection<Service> services;
}
2. Service
#Getter
#Setter
#Entity
#Table(name = "service")
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Service{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#Column(name = "request_id")
private Integer requestId;
#Column(name = "status")
private String status;
}
Now, to fetch requests having service(s) of specific status I wrote following #Query in RequestDao file.
#Query(value = "SELECT DISTINCT req FROM request req JOIN req.services srvs WHERE (srvs.status = :serviceStatus)")
List<Request> getAll(#Param("serviceStatus") String serviceStatus)
As result, I'm getting all services inside the 'request' object if any of the associated service matches the 'where' criteria.. while I'm expecting 'request' to only include those services which are matching with 'where' criteria.
Can anyone please help?
Thanks in advance.
That's not so easy because Hibernate/JPA try to guarantee that the entity model is in sync with the database state. You apparently want a projection that most probably should not be kept in sync with the database. If you really must do this, you can use the following query, but beware that this might cause deletion of service elements in the collection that don't match the criteria:
#Query(value = "SELECT DISTINCT req FROM request req JOIN FETCH req.services srvs WHERE (srvs.status = :serviceStatus)")
List<Request> getAll(#Param("serviceStatus") String serviceStatus);
This is usually handled by introducing DTOs 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(Request.class)
public interface RequestDto {
#IdMapping
Integer getId();
String getStatus();
#Mapping("services[status = :serviceStatus]")
Set<ServiceDto> getServices();
#EntityView(Service.class)
interface ServiceDto {
#IdMapping
Integer getId();
Integer getRequestId();
String getStatus();
}
}
Querying is a matter of applying the entity view to a query, the simplest being just a query by id.
RequestDto a = entityViewManager.find(entityManager, RequestDto.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
List<RequestDto> findAll(#OptionalParam("serviceStatus") String serviceStatus);
The best part is, it will only fetch the state that is actually necessary!
Try updating your query to:
#Query(value = "SELECT DISTINCT req FROM request req LEFT JOIN req.services srvs WHERE (srvs.status = :serviceStatus)")
List<Request> getAll(#Param("serviceStatus") String serviceStatus)

How to check if an entity exists in DB using methods from SimpleJpaRepository

I am trying to find out if any entity exists matching a few conditions using methods available in SimpleJpaRepository.
My entity looks like this:
#Entity
public class MyEntity{
#Id
private int id;
#ManyToOne
private TaskEntity taskEntity;
#Column
private String name;
...
And my method looks like this:
/** Check if any entity exists with the given taskId and name */
public boolean existsByTaskAndName(int taskId, String name){
MyEntity probe = new MyEntity();
probe.setTask(entityManager.getReference(TaskEntity.class, taskId));
probe.setName(name);
return exists(Example.of(probe, ExampleMatcher.matching().withIgnoreCase()));
}
I was expecting the sql query to look something like this:
select top 1 * from my_entity where task=#taskId and lower(name)=lower(#name)
But in reality, the SQL query contained inner joins with TaskEntity and all entities related to TaskEntity, and the where clauses contained comparison between all fields from TaskEntity and all fields from all other related entities. (in total 10+ inner joins and 100+ where clauses).
How can I write the method so that it only compares the columns "task" and "name", without any joins and without reading unnecessary objects?
I would do a JPQL Query like this:
select case when (count(*) > 0) then true else false end from MyEntity m where m.taskEntity.id = :id and upper(m.name) = upper(:name)
It would return you a nice boolean.

ListJoin and multiselect with criteriaquery with Polymorphism

I was wondering if anyone knows the cause of the Exception being thrown? I have the following entities below. Is it because some Employees returned aren't DriverEmployees and thus do not have routes?
#Entity
public class Employee {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", nullable = false)
private Integer id;
private String name;
}
#Entity
public class DriverEmployee extends Employee {
#OneToMany(fetch = FetchType.LAZY)
private List<Routes> routes;
}
#Entity
public class Routes {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", nullable = false)
private Integer id;
private String name;
private String description;
}
CriteriaQuery<Tuple> criteria = criteriaBuilder.createQuery(Tuple.class);
Root<Employee> employee = criteria.from(Employee.class);
Root<DriverEmployee> driverEmployee = criteriaBuilder.treat(employee, DriverEmployee.class);
ListJoin<DriverEmployee, Routes> routes = driverEmployee.joinList("routes");
// Want routes to be returned as a list in the multiselect.
criteria.multiselect(employee.get("name").alias("name"), routes.alias("routes"));
TypedQuery<Tuple> query = em.createQuery(criteria);
query.getResultList().forEach((t) -> {
process(t.get("name", String.class));
processList(t.get("routes", List.class).size());
});
The error I'm receiving is below. Anyone know how I can get the following to run successfully?
Caused by: java.lang.IllegalStateException: No data type for node: org.hibernate.hql.internal.ast.tree.IdentNode
+-[IDENT] IdentNode: 'routes' {originalText=routes}
In this current example you have tuple which contains one object of DriverEmployee and one object of Routes (I recommend you to rename this entity to Route and set #Table(name = "routes"). According to JavaDoc of multiselect():
If the type of the criteria query is CriteriaQuery (i.e., a criteria query object created by either the createTupleQuery method or by passing a Tuple class argument to the createQuery method), a Tuple object corresponding to the arguments of the multiselect method, in the specified order, will be instantiated and returned for each row that results from the query execution.
So it means that you CAN'T make a Tuple like Tuple<DriverEmployee, List<Routes>>
The way how you can reach such behavior of your current query is to do it by yourself. For instance:
your method with criteria:
CriteriaQuery<Tuple> criteria = criteriaBuilder.createQuery(Tuple.class);
Root<Employee> employee = criteria.from(Employee.class);
Root<DriverEmployee> driverEmployee = criteriaBuilder.treat(employee, DriverEmployee.class);
ListJoin<DriverEmployee, Routes> routes = driverEmployee.joinList("routes");
criteria.multiselect(employee.get("name").alias("name"), routes.alias("routes"));
TypedQuery<Tuple> query = em.createQuery(criteria);
List<Tuple> resultList = query.getResultList();
Map<String, List<Routes>> resultMap = getMapFromResultList(resultList);
resultMap.entrySet().forEach((name, routesList) -> {
process(name);
processList(routesList);
});
and the method of obtaining the map:
private Map<String, List<Routes>> getMapFromResultList(List<Tuple> tuples) {
Map<String, List<Routes>> map = new HashMap<>();
tuples.forEach(tuple -> {
String name = tuple.get("name", String.class);
Routes routes = tuple.get("routes", Routes.class);
map.computeIfPresent(name, (key, value) -> {
value.add(routes);
return value;
});
map.computeIfAbsent(name, (key) -> {
List<Routes> routesList = new ArrayList<>();
routesList.add(routes);
return routesList;
});
});
return map;
}
I guess the Hibernate JPA Criteria implememtation doesn't support that. If you really want to use the JPA Criteria API to do this, you are probably out of luck. In JPQL/HQL you could model this like SELECT e.name, r FROM DriverEmployee e LEFT JOIN e.routes r. On top of that you have to extract the values like Andrew Kolesnyk mentioned.
However, this is is the 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(DriverEmployee.class)
interface EmployeeDto {
#IdMapping
Integer getId();
String getName();
List<RoutesDto> getRoutes();
}
#EntityView(Routes.class)
interface RoutesDto {
#IdMapping
Integer getId();
String getName();
}
Querying is a matter of applying the entity view to a query, the simplest being just a query by id.
EmployeeDto dto = entityViewManager.find(entityManager, EmployeeDto.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 and it can also be saved back. Here a sample repository
#Repository
interface EmployeeRepository {
EmployeeDto findOne(Long id);
}
It will only fetch the mappings that you tell it to fetch.
Here you can see an example project: https://github.com/Blazebit/blaze-persistence/tree/master/examples/spring-data-webmvc

Categories